mirror of
https://github.com/tabidachinokaze/Electro.git
synced 2026-03-01 03:49:43 +08:00
chore: 依赖库版本升级
This commit is contained in:
@@ -1,14 +1,15 @@
|
||||
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
||||
import org.jetbrains.kotlin.konan.properties.Properties
|
||||
import java.io.FileInputStream
|
||||
|
||||
plugins {
|
||||
alias(libs.plugins.agp)
|
||||
alias(libs.plugins.kotlin)
|
||||
alias(libs.plugins.android.application)
|
||||
alias(libs.plugins.kotlin.android)
|
||||
alias(libs.plugins.hilt)
|
||||
alias(libs.plugins.serialization)
|
||||
alias(libs.plugins.kotlin.serialization)
|
||||
alias(libs.plugins.ksp)
|
||||
alias(libs.plugins.google.services)
|
||||
id("kotlin-kapt")
|
||||
alias(libs.plugins.kotlin.compose)
|
||||
}
|
||||
|
||||
val properties = Properties().apply {
|
||||
@@ -17,12 +18,12 @@ val properties = Properties().apply {
|
||||
|
||||
android {
|
||||
namespace = "cn.tabidachi.electro"
|
||||
compileSdk = 34
|
||||
compileSdk = 36
|
||||
|
||||
defaultConfig {
|
||||
applicationId = "cn.tabidachi.electro"
|
||||
minSdk = 28
|
||||
targetSdk = 34
|
||||
targetSdk = 36
|
||||
versionCode = 5
|
||||
versionName = "1.0.5"
|
||||
|
||||
@@ -50,11 +51,27 @@ android {
|
||||
}
|
||||
buildTypes {
|
||||
release {
|
||||
buildConfigField("String", "APP_CENTER_SECRET", properties.getProperty("appCenter.secret"))
|
||||
buildConfigField("String", "ELECTRO_SERVER_HOST", properties.getProperty("electro.server.host.release"))
|
||||
buildConfigField(
|
||||
"String",
|
||||
"APP_CENTER_SECRET",
|
||||
properties.getProperty("appCenter.secret")
|
||||
)
|
||||
buildConfigField(
|
||||
"String",
|
||||
"ELECTRO_SERVER_HOST",
|
||||
properties.getProperty("electro.server.host.release")
|
||||
)
|
||||
buildConfigField("String", "MINIO_URL", properties.getProperty("minio.url.release"))
|
||||
buildConfigField("String", "MINIO_ACCESS_KEY", properties.getProperty("minio.accessKey.release"))
|
||||
buildConfigField("String", "MINIO_SECRET_KEY", properties.getProperty("minio.secretKey.release"))
|
||||
buildConfigField(
|
||||
"String",
|
||||
"MINIO_ACCESS_KEY",
|
||||
properties.getProperty("minio.accessKey.release")
|
||||
)
|
||||
buildConfigField(
|
||||
"String",
|
||||
"MINIO_SECRET_KEY",
|
||||
properties.getProperty("minio.secretKey.release")
|
||||
)
|
||||
isMinifyEnabled = true
|
||||
proguardFiles(
|
||||
getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro"
|
||||
@@ -62,11 +79,27 @@ android {
|
||||
signingConfig = signingConfigs.getByName("release")
|
||||
}
|
||||
debug {
|
||||
buildConfigField("String", "APP_CENTER_SECRET", properties.getProperty("appCenter.secret"))
|
||||
buildConfigField("String", "ELECTRO_SERVER_HOST", properties.getProperty("electro.server.host.debug"))
|
||||
buildConfigField(
|
||||
"String",
|
||||
"APP_CENTER_SECRET",
|
||||
properties.getProperty("appCenter.secret")
|
||||
)
|
||||
buildConfigField(
|
||||
"String",
|
||||
"ELECTRO_SERVER_HOST",
|
||||
properties.getProperty("electro.server.host.debug")
|
||||
)
|
||||
buildConfigField("String", "MINIO_URL", properties.getProperty("minio.url.debug"))
|
||||
buildConfigField("String", "MINIO_ACCESS_KEY", properties.getProperty("minio.accessKey.debug"))
|
||||
buildConfigField("String", "MINIO_SECRET_KEY", properties.getProperty("minio.secretKey.debug"))
|
||||
buildConfigField(
|
||||
"String",
|
||||
"MINIO_ACCESS_KEY",
|
||||
properties.getProperty("minio.accessKey.debug")
|
||||
)
|
||||
buildConfigField(
|
||||
"String",
|
||||
"MINIO_SECRET_KEY",
|
||||
properties.getProperty("minio.secretKey.debug")
|
||||
)
|
||||
isMinifyEnabled = false
|
||||
proguardFiles(
|
||||
getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro"
|
||||
@@ -82,9 +115,6 @@ android {
|
||||
compose = true
|
||||
buildConfig = true
|
||||
}
|
||||
composeOptions {
|
||||
kotlinCompilerExtensionVersion = libs.versions.compose.compiler.get()
|
||||
}
|
||||
packaging {
|
||||
resources {
|
||||
excludes += "/META-INF/{AL2.0,LGPL2.1}"
|
||||
@@ -103,9 +133,15 @@ android {
|
||||
}
|
||||
}
|
||||
|
||||
kotlin {
|
||||
compilerOptions {
|
||||
jvmTarget = JvmTarget.JVM_17
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
||||
implementation(libs.core.ktx)
|
||||
implementation(libs.androidx.core.ktx)
|
||||
implementation(libs.lifecycle.runtime.ktx)
|
||||
implementation(libs.lifecycle.runtime.compose)
|
||||
implementation(libs.activity.compose)
|
||||
@@ -114,7 +150,7 @@ dependencies {
|
||||
implementation(libs.bundles.accompanist)
|
||||
implementation(libs.datastore.preferences)
|
||||
implementation(libs.hilt.android)
|
||||
kapt(libs.hilt.compiler)
|
||||
ksp(libs.hilt.compiler)
|
||||
implementation(libs.bundles.navigation.compose)
|
||||
implementation(libs.bundles.ktor)
|
||||
implementation(platform(libs.coil.bom))
|
||||
@@ -124,13 +160,14 @@ dependencies {
|
||||
ksp(libs.room.compiler)
|
||||
implementation(libs.compose.constraintlayout)
|
||||
implementation(libs.minio)
|
||||
implementation(libs.google.webrtc)
|
||||
implementation(platform(libs.firebase.bom))
|
||||
implementation(libs.bundles.firebase)
|
||||
implementation(libs.bundles.appcenter)
|
||||
implementation(libs.tabler.icons)
|
||||
implementation(files("libs/AMap3DMap_9.6.0_AMapSearch_9.5.0_AMapLocation_6.2.0_20230116.jar"))
|
||||
implementation(libs.okhttp)
|
||||
implementation(libs.webrtc.android)
|
||||
implementation(libs.kotlinx.serialization.json)
|
||||
// test
|
||||
testImplementation(libs.junit)
|
||||
androidTestImplementation(libs.androidx.test.ext.junit)
|
||||
@@ -141,7 +178,3 @@ dependencies {
|
||||
debugImplementation(libs.compose.ui.test.manifest)
|
||||
|
||||
}
|
||||
|
||||
kapt {
|
||||
correctErrorTypes = true
|
||||
}
|
||||
|
||||
@@ -41,7 +41,7 @@ import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import cn.tabidachi.electro.ext.longTimeFormat
|
||||
import cn.tabidachi.electro.ui.theme.ElectroTheme
|
||||
import coil.compose.AsyncImage
|
||||
import coil3.compose.AsyncImage
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.Job
|
||||
|
||||
@@ -69,9 +69,9 @@ class CallActivity : ComponentActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
override fun onNewIntent(intent: Intent?) {
|
||||
override fun onNewIntent(intent: Intent) {
|
||||
super.onNewIntent(intent)
|
||||
intent?.getStringExtra("notification_id")?.let {
|
||||
intent.getStringExtra("notification_id")?.let {
|
||||
NotificationManagerCompat.from(this).cancel(it.toInt())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
package cn.tabidachi.electro
|
||||
|
||||
import android.app.Application
|
||||
import coil.Coil
|
||||
import coil.ImageLoader
|
||||
import coil.decode.GifDecoder
|
||||
import coil.decode.VideoFrameDecoder
|
||||
import coil.request.CachePolicy
|
||||
import coil3.ImageLoader
|
||||
import coil3.PlatformContext
|
||||
import coil3.SingletonImageLoader
|
||||
import coil3.gif.AnimatedImageDecoder
|
||||
import coil3.network.ktor3.KtorNetworkFetcherFactory
|
||||
import coil3.svg.SvgDecoder
|
||||
import coil3.video.VideoFrameDecoder
|
||||
import com.amap.api.maps.MapsInitializer
|
||||
import com.microsoft.appcenter.AppCenter
|
||||
import com.microsoft.appcenter.analytics.Analytics
|
||||
@@ -14,18 +16,9 @@ import com.microsoft.appcenter.distribute.Distribute
|
||||
import dagger.hilt.android.HiltAndroidApp
|
||||
|
||||
@HiltAndroidApp
|
||||
class ElectroApplication : Application() {
|
||||
class ElectroApplication : Application(), SingletonImageLoader.Factory {
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
Coil.setImageLoader(
|
||||
ImageLoader.Builder(this)
|
||||
.memoryCachePolicy(CachePolicy.ENABLED)
|
||||
.diskCachePolicy(CachePolicy.ENABLED)
|
||||
.components {
|
||||
add(VideoFrameDecoder.Factory())
|
||||
add(GifDecoder.Factory())
|
||||
}.build()
|
||||
)
|
||||
AppCenter.start(
|
||||
this,
|
||||
BuildConfig.APP_CENTER_SECRET,
|
||||
@@ -36,4 +29,14 @@ class ElectroApplication : Application() {
|
||||
MapsInitializer.updatePrivacyShow(this, true, true)
|
||||
MapsInitializer.updatePrivacyAgree(this, true)
|
||||
}
|
||||
}
|
||||
|
||||
override fun newImageLoader(context: PlatformContext): ImageLoader {
|
||||
return ImageLoader.Builder(this)
|
||||
.components {
|
||||
add(KtorNetworkFetcherFactory())
|
||||
add(AnimatedImageDecoder.Factory())
|
||||
add(SvgDecoder.Factory())
|
||||
add(VideoFrameDecoder.Factory())
|
||||
}.build()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -108,9 +108,9 @@ class ElectroViewModel @Inject constructor(
|
||||
class DownloadState {
|
||||
val progress = MutableStateFlow(0f)
|
||||
val success = MutableStateFlow(false)
|
||||
val progressListener: suspend (bytesSentTotal: Long, contentLength: Long) -> Unit =
|
||||
{ bytesSentTotal: Long, contentLength: Long ->
|
||||
progress.value = bytesSentTotal.toFloat() / contentLength.toFloat()
|
||||
val progressListener: suspend (bytesSentTotal: Long, contentLength: Long?) -> Unit =
|
||||
{ bytesSentTotal: Long, contentLength: Long? ->
|
||||
contentLength?.let { progress.value = bytesSentTotal.toFloat() / it.toFloat() }
|
||||
}
|
||||
val onSuccess = {
|
||||
success.value = true
|
||||
|
||||
@@ -2,9 +2,9 @@ package cn.tabidachi.electro
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.viewModels
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.ui.Modifier
|
||||
@@ -15,7 +15,7 @@ import cn.tabidachi.electro.ui.theme.ElectroTheme
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
|
||||
@AndroidEntryPoint
|
||||
class LocationActivity : AppCompatActivity() {
|
||||
class LocationActivity : ComponentActivity() {
|
||||
private val electroViewModel: ElectroViewModel by viewModels()
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
@@ -2,8 +2,8 @@ package cn.tabidachi.electro
|
||||
|
||||
import android.app.Activity
|
||||
import android.os.Bundle
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.viewModels
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.runtime.getValue
|
||||
@@ -21,7 +21,7 @@ import com.microsoft.appcenter.distribute.ReleaseDetails
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
|
||||
@AndroidEntryPoint
|
||||
class MainActivity : AppCompatActivity(), DistributeListener {
|
||||
class MainActivity : ComponentActivity(), DistributeListener {
|
||||
private val electroViewModel: ElectroViewModel by viewModels()
|
||||
private var updateDialogVisible by mutableStateOf(false)
|
||||
private var releaseDetails by mutableStateOf<ReleaseDetails?>(null)
|
||||
|
||||
@@ -11,18 +11,18 @@ import android.hardware.HardwareBuffer
|
||||
import android.media.ImageReader
|
||||
import android.os.Build
|
||||
import androidx.annotation.RequiresApi
|
||||
import coil.size.Size
|
||||
import coil.transform.Transformation
|
||||
import coil3.size.Size
|
||||
import coil3.transform.Transformation
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.S)
|
||||
class BlurTransformation(
|
||||
private val radius: Float,
|
||||
private val sampling: Float
|
||||
) : Transformation {
|
||||
) : Transformation() {
|
||||
override val cacheKey: String = "${BlurTransformation::class.java.name}-$radius-$sampling"
|
||||
|
||||
@SuppressLint("WrongConstant")
|
||||
override suspend fun transform(input: Bitmap, size: Size): Bitmap {
|
||||
override suspend fun transform(input: coil3.Bitmap, size: Size): coil3.Bitmap {
|
||||
val renderNode = RenderNode("RenderEffect")
|
||||
val hardwareRenderer = HardwareRenderer()
|
||||
val imageReader = ImageReader.newInstance(
|
||||
|
||||
@@ -176,7 +176,7 @@ class Repository(
|
||||
url: String,
|
||||
onSuccess: () -> Unit = {},
|
||||
onFailure: () -> Unit = {},
|
||||
progressListener: suspend (Long, Long) -> Unit = { _: Long, _: Long -> }
|
||||
progressListener: suspend (Long, Long?) -> Unit = { _: Long, _: Long? -> }
|
||||
) = withContext(Dispatchers.IO) {
|
||||
kotlin.runCatching {
|
||||
val readChannel = ktor.download(url, progressListener)
|
||||
|
||||
@@ -451,7 +451,7 @@ class Ktor(
|
||||
|
||||
suspend fun download(
|
||||
url: String,
|
||||
progressListener: suspend (bytesSentTotal: Long, contentLength: Long) -> Unit
|
||||
progressListener: suspend (bytesSentTotal: Long, contentLength: Long?) -> Unit
|
||||
): ByteReadChannel {
|
||||
return client.get(url) {
|
||||
onDownload(progressListener)
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package cn.tabidachi.electro.ext
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.ContextWrapper
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.net.Uri
|
||||
@@ -87,4 +89,15 @@ fun Context.checkPermission(permission: String): Boolean {
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
tailrec fun Context.findActivity(): Activity? {
|
||||
return when (this) {
|
||||
is ContextWrapper -> when (this) {
|
||||
is Activity -> this
|
||||
else -> baseContext.findActivity()
|
||||
}
|
||||
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
@@ -129,7 +129,7 @@ class DownloadMessageItem(
|
||||
downloading = false
|
||||
}
|
||||
) { sent, length ->
|
||||
progress = sent.toFloat() / length.toFloat()
|
||||
length?.let { progress = sent.toFloat() / it.toFloat() }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -37,6 +37,7 @@ import cn.tabidachi.electro.ui.map.DragDropSelectPointScreen
|
||||
import cn.tabidachi.electro.ui.pair.PairScreen
|
||||
import cn.tabidachi.electro.ui.profile.ProfileScreen
|
||||
import cn.tabidachi.electro.ui.search.SearchScreen
|
||||
import cn.tabidachi.electro.ui.server.serverRoute
|
||||
import cn.tabidachi.electro.ui.sessions.SessionsScreen
|
||||
import cn.tabidachi.electro.ui.settings.SettingsScreen
|
||||
|
||||
@@ -106,12 +107,12 @@ fun ElectroNavGraph(
|
||||
composable(
|
||||
ElectroDestinations.PAIR_ROUTE,
|
||||
arguments = listOf(
|
||||
navArgument(ElectroDestinationArgs.UID_ARG) {
|
||||
navArgument(Args.UID.toString()) {
|
||||
type = NavType.LongType
|
||||
},
|
||||
)
|
||||
) { entry ->
|
||||
val uid = entry.arguments?.getLong(ElectroDestinationArgs.UID_ARG)
|
||||
val uid = entry.arguments?.getLong(Args.UID.toString())
|
||||
PairScreen(uid!!, navigationActions, navHostController)
|
||||
}
|
||||
composable(ElectroDestinations.CREATE_GROUP_ROUTE) {
|
||||
@@ -123,12 +124,12 @@ fun ElectroNavGraph(
|
||||
composable(
|
||||
ElectroDestinations.GROUP_ROUTE,
|
||||
arguments = listOf(
|
||||
navArgument(ElectroDestinationArgs.SID_ARG) {
|
||||
navArgument(Args.SID.toString()) {
|
||||
type = NavType.LongType
|
||||
},
|
||||
)
|
||||
) { entry ->
|
||||
val sid = entry.arguments?.getLong(ElectroDestinationArgs.SID_ARG)
|
||||
val sid = entry.arguments?.getLong(Args.SID.toString())
|
||||
sid?.let {
|
||||
GroupScreen(sid = sid, navigationActions = navigationActions)
|
||||
} ?: navHostController.navigateUp()
|
||||
@@ -136,12 +137,12 @@ fun ElectroNavGraph(
|
||||
composable(
|
||||
ElectroDestinations.GROUP_DETAIL_ROUTE,
|
||||
arguments = listOf(
|
||||
navArgument(ElectroDestinationArgs.SID_ARG) {
|
||||
navArgument(Args.SID.toString()) {
|
||||
type = NavType.LongType
|
||||
},
|
||||
)
|
||||
) { entry ->
|
||||
val sid = entry.arguments?.getLong(ElectroDestinationArgs.SID_ARG)
|
||||
val sid = entry.arguments?.getLong(Args.SID.toString())
|
||||
val backStackEntry = remember {
|
||||
navHostController.getBackStackEntry(ElectroDestinations.GROUP_ROUTE)
|
||||
}
|
||||
@@ -156,12 +157,12 @@ fun ElectroNavGraph(
|
||||
composable(
|
||||
ElectroDestinations.GROUP_EDIT_ROUTE,
|
||||
arguments = listOf(
|
||||
navArgument(ElectroDestinationArgs.SID_ARG) {
|
||||
navArgument(Args.SID.toString()) {
|
||||
type = NavType.LongType
|
||||
},
|
||||
)
|
||||
) { entry ->
|
||||
val sid = entry.arguments?.getLong(ElectroDestinationArgs.SID_ARG)
|
||||
val sid = entry.arguments?.getLong(Args.SID.toString())
|
||||
val backStackEntry = remember {
|
||||
navHostController.getBackStackEntry(ElectroDestinations.GROUP_ROUTE)
|
||||
}
|
||||
@@ -176,7 +177,7 @@ fun ElectroNavGraph(
|
||||
composable(
|
||||
ElectroDestinations.INVITE_ROUTE,
|
||||
arguments = listOf(
|
||||
navArgument(ElectroDestinationArgs.SID_ARG) {
|
||||
navArgument(Args.SID.toString()) {
|
||||
type = NavType.LongType
|
||||
},
|
||||
)
|
||||
@@ -184,19 +185,23 @@ fun ElectroNavGraph(
|
||||
val backStackEntry = remember {
|
||||
navHostController.getBackStackEntry(ElectroDestinations.GROUP_ROUTE)
|
||||
}
|
||||
it.arguments?.getLong(ElectroDestinationArgs.SID_ARG)?.let {
|
||||
InviteScreen(sid = it, navigationActions = navigationActions, hiltViewModel(backStackEntry))
|
||||
it.arguments?.getLong(Args.SID.toString())?.let {
|
||||
InviteScreen(
|
||||
sid = it,
|
||||
navigationActions = navigationActions,
|
||||
hiltViewModel(backStackEntry)
|
||||
)
|
||||
} ?: navHostController.navigateUp()
|
||||
}
|
||||
composable(
|
||||
ElectroDestinations.GROUP_ADMIN_ROUTE,
|
||||
arguments = listOf(
|
||||
navArgument(ElectroDestinationArgs.SID_ARG) {
|
||||
navArgument(Args.SID.toString()) {
|
||||
type = NavType.LongType
|
||||
},
|
||||
)
|
||||
) { entry ->
|
||||
val sid = entry.arguments?.getLong(ElectroDestinationArgs.SID_ARG)
|
||||
val sid = entry.arguments?.getLong(Args.SID.toString())
|
||||
val backStackEntry = remember {
|
||||
navHostController.getBackStackEntry(ElectroDestinations.GROUP_ROUTE)
|
||||
}
|
||||
@@ -217,12 +222,12 @@ fun ElectroNavGraph(
|
||||
composable(
|
||||
ElectroDestinations.CHANNEL_ROUTE,
|
||||
arguments = listOf(
|
||||
navArgument(ElectroDestinationArgs.SID_ARG) {
|
||||
navArgument(Args.SID.toString()) {
|
||||
type = NavType.LongType
|
||||
},
|
||||
)
|
||||
) { entry ->
|
||||
val sid = entry.arguments?.getLong(ElectroDestinationArgs.SID_ARG)
|
||||
val sid = entry.arguments?.getLong(Args.SID.toString())
|
||||
sid?.let {
|
||||
ChannelScreen(sid = sid, navigationActions = navigationActions)
|
||||
} ?: navHostController.navigateUp()
|
||||
@@ -230,12 +235,12 @@ fun ElectroNavGraph(
|
||||
composable(
|
||||
ElectroDestinations.CHANNEL_ADMIN_ROUTE,
|
||||
arguments = listOf(
|
||||
navArgument(ElectroDestinationArgs.SID_ARG) {
|
||||
navArgument(Args.SID.toString()) {
|
||||
type = NavType.LongType
|
||||
},
|
||||
)
|
||||
) { entry ->
|
||||
val sid = entry.arguments?.getLong(ElectroDestinationArgs.SID_ARG)
|
||||
val sid = entry.arguments?.getLong(Args.SID.toString())
|
||||
val backStackEntry = remember {
|
||||
navHostController.getBackStackEntry(ElectroDestinations.CHANNEL_ROUTE)
|
||||
}
|
||||
@@ -250,12 +255,12 @@ fun ElectroNavGraph(
|
||||
composable(
|
||||
ElectroDestinations.CHANNEL_DETAIL_ROUTE,
|
||||
arguments = listOf(
|
||||
navArgument(ElectroDestinationArgs.SID_ARG) {
|
||||
navArgument(Args.SID.toString()) {
|
||||
type = NavType.LongType
|
||||
},
|
||||
)
|
||||
) { entry ->
|
||||
val sid = entry.arguments?.getLong(ElectroDestinationArgs.SID_ARG)
|
||||
val sid = entry.arguments?.getLong(Args.SID.toString())
|
||||
val backStackEntry = remember {
|
||||
navHostController.getBackStackEntry(ElectroDestinations.CHANNEL_ROUTE)
|
||||
}
|
||||
@@ -270,7 +275,7 @@ fun ElectroNavGraph(
|
||||
composable(
|
||||
ElectroDestinations.CHANNEL_INVITE_ROUTE,
|
||||
arguments = listOf(
|
||||
navArgument(ElectroDestinationArgs.SID_ARG) {
|
||||
navArgument(Args.SID.toString()) {
|
||||
type = NavType.LongType
|
||||
},
|
||||
)
|
||||
@@ -278,19 +283,23 @@ fun ElectroNavGraph(
|
||||
val backStackEntry = remember {
|
||||
navHostController.getBackStackEntry(ElectroDestinations.CHANNEL_ROUTE)
|
||||
}
|
||||
it.arguments?.getLong(ElectroDestinationArgs.SID_ARG)?.let {
|
||||
ChannelInviteScreen(sid = it, navigationActions = navigationActions, hiltViewModel(backStackEntry))
|
||||
it.arguments?.getLong(Args.SID.toString())?.let {
|
||||
ChannelInviteScreen(
|
||||
sid = it,
|
||||
navigationActions = navigationActions,
|
||||
hiltViewModel(backStackEntry)
|
||||
)
|
||||
} ?: navHostController.navigateUp()
|
||||
}
|
||||
composable(
|
||||
ElectroDestinations.CHANNEL_EDIT_ROUTE,
|
||||
arguments = listOf(
|
||||
navArgument(ElectroDestinationArgs.SID_ARG) {
|
||||
navArgument(Args.SID.toString()) {
|
||||
type = NavType.LongType
|
||||
},
|
||||
)
|
||||
) { entry ->
|
||||
val sid = entry.arguments?.getLong(ElectroDestinationArgs.SID_ARG)
|
||||
val sid = entry.arguments?.getLong(Args.SID.toString())
|
||||
val backStackEntry = remember {
|
||||
navHostController.getBackStackEntry(ElectroDestinations.CHANNEL_ROUTE)
|
||||
}
|
||||
@@ -302,5 +311,6 @@ fun ElectroNavGraph(
|
||||
)
|
||||
} ?: navHostController.navigateUp()
|
||||
}
|
||||
serverRoute(navigationActions = navigationActions)
|
||||
}
|
||||
}
|
||||
@@ -2,94 +2,69 @@ package cn.tabidachi.electro.ui
|
||||
|
||||
import androidx.navigation.NavGraph.Companion.findStartDestination
|
||||
import androidx.navigation.NavHostController
|
||||
import cn.tabidachi.electro.ui.ElectroDestinationArgs.IS_OFFER_ARG
|
||||
import cn.tabidachi.electro.ui.ElectroDestinationArgs.SID_ARG
|
||||
import cn.tabidachi.electro.ui.ElectroDestinationArgs.UID_ARG
|
||||
import cn.tabidachi.electro.ui.ElectroScreens.AUTH_SCREEN
|
||||
import cn.tabidachi.electro.ui.ElectroScreens.CALL_SCREEN
|
||||
import cn.tabidachi.electro.ui.ElectroScreens.CHANNEL_ADMIN_SCREEN
|
||||
import cn.tabidachi.electro.ui.ElectroScreens.CHANNEL_CREATE_SCREEN
|
||||
import cn.tabidachi.electro.ui.ElectroScreens.CHANNEL_DETAIL_SCREEN
|
||||
import cn.tabidachi.electro.ui.ElectroScreens.CHANNEL_EDIT_SCREEN
|
||||
import cn.tabidachi.electro.ui.ElectroScreens.CHANNEL_INVITE_SCREEN
|
||||
import cn.tabidachi.electro.ui.ElectroScreens.CHANNEL_SCREEN
|
||||
import cn.tabidachi.electro.ui.ElectroScreens.CHATGPT_SCREEN
|
||||
import cn.tabidachi.electro.ui.ElectroScreens.DIALOGS_SCREEN
|
||||
import cn.tabidachi.electro.ui.ElectroScreens.CHAT_SCREEN
|
||||
import cn.tabidachi.electro.ui.ElectroScreens.CONTACT_SCREEN
|
||||
import cn.tabidachi.electro.ui.ElectroScreens.CREATE_GROUP_SCREEN
|
||||
import cn.tabidachi.electro.ui.ElectroScreens.GROUP_ADMIN_SCREEN
|
||||
import cn.tabidachi.electro.ui.ElectroScreens.GROUP_DETAIL_SCREEN
|
||||
import cn.tabidachi.electro.ui.ElectroScreens.GROUP_EDIT_SCREEN
|
||||
import cn.tabidachi.electro.ui.ElectroScreens.GROUP_SCREEN
|
||||
import cn.tabidachi.electro.ui.ElectroScreens.INVITE_SCREEN
|
||||
import cn.tabidachi.electro.ui.ElectroScreens.PAIR_SCREEN
|
||||
import cn.tabidachi.electro.ui.ElectroScreens.MESSAGE_SCREEN
|
||||
import cn.tabidachi.electro.ui.ElectroScreens.PROFILE_SCREEN
|
||||
import cn.tabidachi.electro.ui.ElectroScreens.SEARCH_SCREEN
|
||||
import cn.tabidachi.electro.ui.ElectroScreens.SETTINGS_SCREEN
|
||||
import cn.tabidachi.electro.ui.ElectroScreens.SPLASH_SCREEN
|
||||
import cn.tabidachi.electro.ui.ElectroScreens.VIDEO_CALL_SCREEN
|
||||
|
||||
private object ElectroScreens {
|
||||
const val AUTH_SCREEN = "auth"
|
||||
const val SPLASH_SCREEN = "splash"
|
||||
const val MESSAGE_SCREEN = "message"
|
||||
const val SETTINGS_SCREEN = "settings"
|
||||
const val PROFILE_SCREEN = "profile"
|
||||
const val CONTACT_SCREEN = "contact"
|
||||
const val CHAT_SCREEN = "chat"
|
||||
const val DIALOGS_SCREEN = "chats"
|
||||
const val PAIR_SCREEN = "direct"
|
||||
const val CREATE_GROUP_SCREEN = "create_group"
|
||||
const val CHATGPT_SCREEN = "chatgpt_screen"
|
||||
const val SEARCH_SCREEN = "search"
|
||||
const val GROUP_SCREEN = "group"
|
||||
const val GROUP_DETAIL_SCREEN = "group_detail"
|
||||
const val INVITE_SCREEN = "invite"
|
||||
const val VIDEO_CALL_SCREEN = "video_call"
|
||||
const val CALL_SCREEN = "call"
|
||||
const val GROUP_EDIT_SCREEN = "group_edit"
|
||||
const val GROUP_ADMIN_SCREEN = "group_admin"
|
||||
const val CHANNEL_SCREEN = "channel"
|
||||
const val CHANNEL_CREATE_SCREEN = "channel_create"
|
||||
const val CHANNEL_ADMIN_SCREEN = "channel_admin"
|
||||
const val CHANNEL_DETAIL_SCREEN = "channel_detail"
|
||||
const val CHANNEL_INVITE_SCREEN = "channel_invite"
|
||||
const val CHANNEL_EDIT_SCREEN = "channel_edit"
|
||||
|
||||
enum class Screen {
|
||||
AUTH,
|
||||
SPLASH,
|
||||
MESSAGE,
|
||||
SETTINGS,
|
||||
PROFILE,
|
||||
CONTACT,
|
||||
CHAT,
|
||||
DIALOGS,
|
||||
PAIR,
|
||||
CREATE_GROUP,
|
||||
CHATGPT,
|
||||
SEARCH,
|
||||
GROUP,
|
||||
GROUP_DETAIL,
|
||||
INVITE,
|
||||
VIDEO_CALL,
|
||||
CALL,
|
||||
GROUP_EDIT,
|
||||
GROUP_ADMIN,
|
||||
CHANNEL,
|
||||
CHANNEL_CREATE,
|
||||
CHANNEL_ADMIN,
|
||||
CHANNEL_DETAIL,
|
||||
CHANNEL_INVITE,
|
||||
CHANNEL_EDIT,
|
||||
SERVER
|
||||
}
|
||||
|
||||
object ElectroDestinationArgs {
|
||||
const val SID_ARG = "sid"
|
||||
const val UID_ARG = "uid"
|
||||
const val IS_OFFER_ARG = "answer_offer"
|
||||
enum class Args {
|
||||
SID,
|
||||
UID,
|
||||
IS_OFFER,
|
||||
}
|
||||
|
||||
object ElectroDestinations {
|
||||
const val GROUP_EDIT_ROUTE = "$GROUP_EDIT_SCREEN?$SID_ARG={$SID_ARG}"
|
||||
const val CALL_ROUTE = "$CALL_SCREEN?$UID_ARG={$UID_ARG}?$IS_OFFER_ARG={$IS_OFFER_ARG}"
|
||||
const val VIDEO_CALL_ROUTE = "$VIDEO_CALL_SCREEN?$UID_ARG={$UID_ARG}"
|
||||
const val INVITE_ROUTE = "$INVITE_SCREEN?$SID_ARG={$SID_ARG}"
|
||||
const val GROUP_DETAIL_ROUTE = "$GROUP_DETAIL_SCREEN?$SID_ARG={$SID_ARG}"
|
||||
const val GROUP_ROUTE = "$GROUP_SCREEN?$SID_ARG={$SID_ARG}"
|
||||
const val CREATE_GROUP_ROUTE = CREATE_GROUP_SCREEN
|
||||
const val PAIR_ROUTE = "$PAIR_SCREEN?$UID_ARG={$UID_ARG}"
|
||||
const val DIALOGS_ROUTE = DIALOGS_SCREEN
|
||||
const val CONTACT_ROUTE = CONTACT_SCREEN
|
||||
const val SETTINGS_ROUTE = SETTINGS_SCREEN
|
||||
const val MESSAGE_ROUTE = MESSAGE_SCREEN
|
||||
const val SPLASH_ROUTE = SPLASH_SCREEN
|
||||
const val AUTH_ROUTE = AUTH_SCREEN
|
||||
const val PROFILE_ROUTE = PROFILE_SCREEN
|
||||
const val CHATGPT_ROUTE = CHATGPT_SCREEN
|
||||
const val SEARCH_ROUTE = SEARCH_SCREEN
|
||||
const val GROUP_ADMIN_ROUTE = "$GROUP_ADMIN_SCREEN?$SID_ARG={$SID_ARG}"
|
||||
const val CHANNEL_ROUTE = "$CHANNEL_SCREEN?$SID_ARG={$SID_ARG}"
|
||||
const val CHANNEL_CREATE_ROUTE = CHANNEL_CREATE_SCREEN
|
||||
const val CHANNEL_ADMIN_ROUTE = "$CHANNEL_ADMIN_SCREEN?$SID_ARG={$SID_ARG}"
|
||||
const val CHANNEL_DETAIL_ROUTE = "$CHANNEL_DETAIL_SCREEN?$SID_ARG={$SID_ARG}"
|
||||
const val CHANNEL_INVITE_ROUTE = "$CHANNEL_INVITE_SCREEN?$SID_ARG={$SID_ARG}"
|
||||
const val CHANNEL_EDIT_ROUTE = "$CHANNEL_EDIT_SCREEN?$SID_ARG={$SID_ARG}"
|
||||
val GROUP_EDIT_ROUTE = "${Screen.GROUP_EDIT}?${Args.SID}={${Args.SID}}"
|
||||
val CALL_ROUTE = "${Screen.CALL}?${Args.UID}={${Args.UID}}?${Args.IS_OFFER}={${Args.IS_OFFER}}"
|
||||
val VIDEO_CALL_ROUTE = "${Screen.VIDEO_CALL}?${Args.UID}={${Args.UID}}"
|
||||
val INVITE_ROUTE = "${Screen.INVITE}?${Args.SID}={${Args.SID}}"
|
||||
val GROUP_DETAIL_ROUTE = "${Screen.GROUP_DETAIL}?${Args.SID}={${Args.SID}}"
|
||||
val GROUP_ROUTE = "${Screen.GROUP}?${Args.SID}={${Args.SID}}"
|
||||
val CREATE_GROUP_ROUTE = "${Screen.CREATE_GROUP}"
|
||||
val PAIR_ROUTE = "${Screen.PAIR}?${Args.UID}={${Args.UID}}"
|
||||
val DIALOGS_ROUTE = "${Screen.DIALOGS}"
|
||||
val CONTACT_ROUTE = "${Screen.CONTACT}"
|
||||
val SETTINGS_ROUTE = "${Screen.SETTINGS}"
|
||||
val MESSAGE_ROUTE = "${Screen.MESSAGE}"
|
||||
val SPLASH_ROUTE = "${Screen.SPLASH}"
|
||||
val AUTH_ROUTE = "${Screen.AUTH}"
|
||||
val PROFILE_ROUTE = "${Screen.PROFILE}"
|
||||
val CHATGPT_ROUTE = "${Screen.CHATGPT}"
|
||||
val SEARCH_ROUTE = "${Screen.SEARCH}"
|
||||
val GROUP_ADMIN_ROUTE = "${Screen.GROUP_ADMIN}?${Args.SID}={${Args.SID}}"
|
||||
val CHANNEL_ROUTE = "${Screen.CHANNEL}?${Args.SID}={${Args.SID}}"
|
||||
val CHANNEL_CREATE_ROUTE = "${Screen.CHANNEL_CREATE}"
|
||||
val CHANNEL_ADMIN_ROUTE = "${Screen.CHANNEL_ADMIN}?${Args.SID}={${Args.SID}}"
|
||||
val CHANNEL_DETAIL_ROUTE = "${Screen.CHANNEL_DETAIL}?${Args.SID}={${Args.SID}}"
|
||||
val CHANNEL_INVITE_ROUTE = "${Screen.CHANNEL_INVITE}?${Args.SID}={${Args.SID}}"
|
||||
val CHANNEL_EDIT_ROUTE = "${Screen.CHANNEL_EDIT}?${Args.SID}={${Args.SID}}"
|
||||
val SERVER_ROUTE = "${Screen.SERVER}"
|
||||
}
|
||||
|
||||
class ElectroNavigationActions(
|
||||
@@ -135,35 +110,35 @@ class ElectroNavigationActions(
|
||||
}
|
||||
|
||||
fun navigateToPair(target: Long) {
|
||||
navHostController.navigate("$PAIR_SCREEN?$UID_ARG=$target")
|
||||
navHostController.navigate("${Screen.PAIR}?${Args.UID}=$target")
|
||||
}
|
||||
|
||||
fun navigateToCreateGroup() {
|
||||
navHostController.navigate(CREATE_GROUP_SCREEN)
|
||||
navHostController.navigate(Screen.CREATE_GROUP.name)
|
||||
}
|
||||
|
||||
fun navigateToSearch() {
|
||||
navHostController.navigate(SEARCH_SCREEN)
|
||||
navHostController.navigate(Screen.SEARCH.name)
|
||||
}
|
||||
|
||||
fun navigateToGroup(sid: Long) {
|
||||
navHostController.navigate("$GROUP_SCREEN?$SID_ARG=$sid")
|
||||
navHostController.navigate("${Screen.GROUP}?${Args.SID}=$sid")
|
||||
}
|
||||
|
||||
fun navigateToGroupDetail(sid: Long) {
|
||||
navHostController.navigate("$GROUP_DETAIL_SCREEN?$SID_ARG=$sid")
|
||||
navHostController.navigate("${Screen.GROUP_DETAIL}?${Args.SID}=$sid")
|
||||
}
|
||||
|
||||
fun navigateToGroupEdit(sid: Long) {
|
||||
navHostController.navigate("$GROUP_EDIT_SCREEN?$SID_ARG=$sid")
|
||||
navHostController.navigate("${Screen.GROUP_EDIT}?${Args.SID}=$sid")
|
||||
}
|
||||
|
||||
fun navigateToInvite(sid: Long) {
|
||||
navHostController.navigate("$INVITE_SCREEN?$SID_ARG=$sid")
|
||||
navHostController.navigate("${Screen.INVITE}?${Args.SID}=$sid")
|
||||
}
|
||||
|
||||
fun navigateToCall(target: Long, isOffer: Boolean) {
|
||||
navHostController.navigate("$CALL_SCREEN?$UID_ARG=$target?$IS_OFFER_ARG=$isOffer") {
|
||||
navHostController.navigate("${Screen.CALL}?${Args.UID}=$target?${Args.IS_OFFER}=$isOffer") {
|
||||
launchSingleTop = true
|
||||
restoreState = true
|
||||
}
|
||||
@@ -174,30 +149,34 @@ class ElectroNavigationActions(
|
||||
}
|
||||
|
||||
fun navigateToGroupAdmin(sid: Long) {
|
||||
navHostController.navigate("$GROUP_ADMIN_SCREEN?$SID_ARG=$sid")
|
||||
navHostController.navigate("${Screen.GROUP_ADMIN}?${Args.SID}=$sid")
|
||||
}
|
||||
|
||||
fun navigateToChannel(sid: Long) {
|
||||
navHostController.navigate("$CHANNEL_SCREEN?$SID_ARG=$sid")
|
||||
navHostController.navigate("${Screen.CHANNEL}?${Args.SID}=$sid")
|
||||
}
|
||||
|
||||
fun navigateToCreateChannel() {
|
||||
navHostController.navigate(CHANNEL_CREATE_SCREEN)
|
||||
navHostController.navigate(Screen.CHANNEL_CREATE.name)
|
||||
}
|
||||
|
||||
fun navigateToChannelAdmin(sid: Long) {
|
||||
navHostController.navigate("$CHANNEL_ADMIN_SCREEN?$SID_ARG=$sid")
|
||||
navHostController.navigate("${Screen.CHANNEL_ADMIN}?${Args.SID}=$sid")
|
||||
}
|
||||
|
||||
fun navigateToChannelDetail(sid: Long) {
|
||||
navHostController.navigate("$CHANNEL_DETAIL_SCREEN?$SID_ARG=$sid")
|
||||
navHostController.navigate("${Screen.CHANNEL_DETAIL}?${Args.SID}=$sid")
|
||||
}
|
||||
|
||||
fun navigateToChannelInvite(sid: Long) {
|
||||
navHostController.navigate("$CHANNEL_INVITE_SCREEN?$SID_ARG=$sid")
|
||||
navHostController.navigate("${Screen.CHANNEL_INVITE}?${Args.SID}=$sid")
|
||||
}
|
||||
|
||||
fun navigateToChannelEdit(sid: Long) {
|
||||
navHostController.navigate("$CHANNEL_EDIT_SCREEN?$SID_ARG=$sid")
|
||||
navHostController.navigate("${Screen.CHANNEL_EDIT}?${Args.SID}=$sid")
|
||||
}
|
||||
|
||||
fun navigateToServer() {
|
||||
navHostController.navigate("${Screen.SERVER}")
|
||||
}
|
||||
}
|
||||
@@ -15,9 +15,14 @@ import androidx.compose.foundation.layout.statusBars
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Cloud
|
||||
import androidx.compose.material.icons.filled.CloudCircle
|
||||
import androidx.compose.material.icons.filled.Lan
|
||||
import androidx.compose.material.icons.filled.Language
|
||||
import androidx.compose.material.icons.outlined.Cloud
|
||||
import androidx.compose.material.icons.rounded.ArrowForward
|
||||
import androidx.compose.material.icons.rounded.PersonAdd
|
||||
import androidx.compose.material.icons.twotone.Cloud
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.DropdownMenu
|
||||
@@ -35,9 +40,6 @@ import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
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.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
@@ -69,6 +71,11 @@ fun AuthScreen(
|
||||
Text(text = stringResource(id = it.id))
|
||||
}
|
||||
}
|
||||
IconButton(onClick = {
|
||||
navigationActions.navigateToServer()
|
||||
}) {
|
||||
Icon(imageVector = Icons.Outlined.Cloud, contentDescription = null)
|
||||
}
|
||||
IconButton(onClick = {
|
||||
authViewModel.languageMenuExpandedChange(true)
|
||||
}) {
|
||||
|
||||
@@ -53,17 +53,21 @@ import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.layout.onSizeChanged
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalView
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.IntSize
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.view.WindowCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import androidx.core.view.WindowInsetsControllerCompat
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import cn.tabidachi.electro.R
|
||||
import cn.tabidachi.electro.data.database.entity.User
|
||||
import cn.tabidachi.electro.ext.findActivity
|
||||
import cn.tabidachi.electro.ui.common.VideoRenderer
|
||||
import coil.compose.AsyncImage
|
||||
import com.google.accompanist.systemuicontroller.rememberSystemUiController
|
||||
import coil3.compose.AsyncImage
|
||||
import org.webrtc.IceCandidate
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class)
|
||||
@@ -74,6 +78,7 @@ fun CallScreen(
|
||||
action: String,
|
||||
onCallEnd: () -> Unit = {},
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val viewModel: CallViewModel = hiltViewModel()
|
||||
val viewState by viewModel.viewState.collectAsState()
|
||||
val localVideoTrack by viewModel.factory.localVideoTrack.collectAsState(null)
|
||||
@@ -102,10 +107,23 @@ fun CallScreen(
|
||||
view.keepScreenOn = false
|
||||
}
|
||||
})
|
||||
val systemUiController = rememberSystemUiController()
|
||||
LaunchedEffect(key1 = viewState.barsVisible, block = {
|
||||
systemUiController.isSystemBarsVisible = viewState.barsVisible
|
||||
})
|
||||
DisposableEffect(Unit) {
|
||||
val window = context.findActivity()?.window ?: return@DisposableEffect onDispose { }
|
||||
val insetsController = WindowCompat.getInsetsController(window, window.decorView)
|
||||
insetsController.apply {
|
||||
hide(WindowInsetsCompat.Type.statusBars())
|
||||
hide(WindowInsetsCompat.Type.navigationBars())
|
||||
systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
|
||||
}
|
||||
onDispose {
|
||||
insetsController.apply {
|
||||
show(WindowInsetsCompat.Type.statusBars())
|
||||
show(WindowInsetsCompat.Type.navigationBars())
|
||||
systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_DEFAULT
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
) {
|
||||
|
||||
@@ -33,7 +33,7 @@ import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import cn.tabidachi.electro.R
|
||||
import cn.tabidachi.electro.ui.ElectroNavigationActions
|
||||
import coil.compose.AsyncImage
|
||||
import coil3.compose.AsyncImage
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
|
||||
@@ -45,7 +45,7 @@ import androidx.compose.ui.unit.dp
|
||||
import cn.tabidachi.electro.R
|
||||
import cn.tabidachi.electro.ui.ElectroNavigationActions
|
||||
import cn.tabidachi.electro.ui.common.ImageTopAppBar
|
||||
import coil.compose.AsyncImage
|
||||
import coil3.compose.AsyncImage
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
|
||||
@@ -38,7 +38,7 @@ import cn.tabidachi.electro.R
|
||||
import cn.tabidachi.electro.ext.regex
|
||||
import cn.tabidachi.electro.ui.ElectroNavigationActions
|
||||
import cn.tabidachi.electro.ui.common.SimpleTextField
|
||||
import coil.compose.AsyncImage
|
||||
import coil3.compose.AsyncImage
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
|
||||
@@ -39,7 +39,7 @@ import cn.tabidachi.electro.R
|
||||
import cn.tabidachi.electro.ui.ElectroNavigationActions
|
||||
import cn.tabidachi.electro.ui.common.MessageColumn
|
||||
import cn.tabidachi.electro.ui.common.MessageViewModel
|
||||
import coil.compose.AsyncImage
|
||||
import coil3.compose.AsyncImage
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
|
||||
@@ -48,7 +48,7 @@ import androidx.compose.ui.unit.dp
|
||||
import cn.tabidachi.electro.R
|
||||
import cn.tabidachi.electro.ui.ElectroNavigationActions
|
||||
import cn.tabidachi.electro.ui.common.SimpleTextField
|
||||
import coil.compose.AsyncImage
|
||||
import coil3.compose.AsyncImage
|
||||
import com.mr0xf00.easycrop.CropResult
|
||||
import com.mr0xf00.easycrop.crop
|
||||
import com.mr0xf00.easycrop.rememberImageCropper
|
||||
|
||||
@@ -50,9 +50,9 @@ import cn.tabidachi.electro.ui.common.attachment.AudioAttachment
|
||||
import cn.tabidachi.electro.ui.common.attachment.FileAttachment
|
||||
import cn.tabidachi.electro.ui.common.attachment.LocationAttachment
|
||||
import cn.tabidachi.electro.ui.common.attachment.VoiceAttachment
|
||||
import coil.compose.AsyncImage
|
||||
import coil.decode.VideoFrameDecoder
|
||||
import coil.request.ImageRequest
|
||||
import coil3.compose.AsyncImage
|
||||
import coil3.request.ImageRequest
|
||||
import coil3.video.VideoFrameDecoder
|
||||
|
||||
@OptIn(ExperimentalAnimationApi::class)
|
||||
@Composable
|
||||
|
||||
@@ -37,9 +37,9 @@ import cn.tabidachi.electro.model.attachment.VideoAttachment
|
||||
import cn.tabidachi.electro.model.attachment.VoiceAttachment
|
||||
import cn.tabidachi.electro.model.attachment.WebRTCAttachment
|
||||
import cn.tabidachi.electro.ui.common.attachment.AudioAttachment
|
||||
import coil.compose.AsyncImage
|
||||
import coil.decode.VideoFrameDecoder
|
||||
import coil.request.ImageRequest
|
||||
import coil3.compose.AsyncImage
|
||||
import coil3.request.ImageRequest
|
||||
import coil3.video.VideoFrameDecoder
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
@@ -64,7 +64,7 @@ fun AttachmentRow(
|
||||
it.hashCode()
|
||||
}
|
||||
) { attachment ->
|
||||
Box(modifier = Modifier.animateItemPlacement()) {
|
||||
Box(modifier = Modifier.animateItem()) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.clip(ItemShape)
|
||||
|
||||
@@ -12,7 +12,6 @@ import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.rounded.Done
|
||||
import androidx.compose.material.icons.rounded.Mic
|
||||
import androidx.compose.material.icons.rounded.Send
|
||||
import androidx.compose.material.ripple.rememberRipple
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.FilledIconButton
|
||||
import androidx.compose.material3.FilledTonalIconButton
|
||||
@@ -20,6 +19,7 @@ import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.LocalContentColor
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.ripple
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.remember
|
||||
@@ -67,46 +67,47 @@ fun AttachmentSelector(
|
||||
}
|
||||
}
|
||||
if (isProcessing) {
|
||||
CircularProgressIndicator(strokeWidth = 3.dp, strokeCap = StrokeCap.Round, modifier = Modifier.size(24.dp))
|
||||
CircularProgressIndicator(
|
||||
strokeWidth = 3.dp,
|
||||
strokeCap = StrokeCap.Round,
|
||||
modifier = Modifier.size(24.dp)
|
||||
)
|
||||
} else
|
||||
if (isEditing) {
|
||||
FilledIconButton(onClick = onEdited) {
|
||||
Icon(imageVector = Icons.Rounded.Done, contentDescription = null)
|
||||
}
|
||||
} else if (sendButtonEnabled) {
|
||||
IconButton(onClick = onSend) {
|
||||
Icon(
|
||||
imageVector = Icons.Rounded.Send, contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
} else {
|
||||
Box(
|
||||
modifier = modifier
|
||||
.size(40.0.dp)
|
||||
.clip(CircleShape)
|
||||
.combinedClickable(
|
||||
onClick = {
|
||||
|
||||
}, onLongClick = onRecording,
|
||||
enabled = !isRecording,
|
||||
role = Role.Button,
|
||||
interactionSource = remember {
|
||||
MutableInteractionSource()
|
||||
},
|
||||
indication = rememberRipple(
|
||||
bounded = false,
|
||||
radius = 40.0.dp / 2
|
||||
)
|
||||
),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.primary) {
|
||||
if (isEditing) {
|
||||
FilledIconButton(onClick = onEdited) {
|
||||
Icon(imageVector = Icons.Rounded.Done, contentDescription = null)
|
||||
}
|
||||
} else if (sendButtonEnabled) {
|
||||
IconButton(onClick = onSend) {
|
||||
Icon(
|
||||
imageVector = Icons.Rounded.Mic, contentDescription = null,
|
||||
imageVector = Icons.Rounded.Send, contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
} else {
|
||||
Box(
|
||||
modifier = modifier
|
||||
.size(40.0.dp)
|
||||
.clip(CircleShape)
|
||||
.combinedClickable(
|
||||
onClick = {
|
||||
|
||||
}, onLongClick = onRecording,
|
||||
enabled = !isRecording,
|
||||
role = Role.Button,
|
||||
interactionSource = remember {
|
||||
MutableInteractionSource()
|
||||
},
|
||||
indication = remember { ripple(bounded = false, radius = 40.0.dp / 2) }
|
||||
),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.primary) {
|
||||
Icon(
|
||||
imageVector = Icons.Rounded.Mic, contentDescription = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,6 @@ import android.app.Application
|
||||
import android.content.Intent
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.BitmapFactory
|
||||
import android.graphics.drawable.BitmapDrawable
|
||||
import android.media.MediaMetadataRetriever
|
||||
import android.media.MediaRecorder
|
||||
import android.net.Uri
|
||||
@@ -41,11 +40,11 @@ import androidx.compose.ui.unit.dp
|
||||
import androidx.core.view.ContentInfoCompat
|
||||
import androidx.core.view.OnReceiveContentListener
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import cn.tabidachi.electro.coil.BlurTransformation
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import cn.tabidachi.electro.LocationActivity
|
||||
import cn.tabidachi.electro.R
|
||||
import cn.tabidachi.electro.coil.BlurTransformation
|
||||
import cn.tabidachi.electro.data.Repository
|
||||
import cn.tabidachi.electro.data.database.entity.Message
|
||||
import cn.tabidachi.electro.data.database.entity.MessageSendRequest
|
||||
@@ -67,10 +66,14 @@ import cn.tabidachi.electro.model.attachment.WebRTCAttachment
|
||||
import cn.tabidachi.electro.model.attachment.convert
|
||||
import cn.tabidachi.electro.model.attachment.deserialize
|
||||
import cn.tabidachi.electro.model.attachment.serialize
|
||||
import coil.executeBlocking
|
||||
import coil.imageLoader
|
||||
import coil.request.ImageRequest
|
||||
import coil.size.Scale
|
||||
import coil3.executeBlocking
|
||||
import coil3.imageLoader
|
||||
import coil3.request.ErrorResult
|
||||
import coil3.request.ImageRequest
|
||||
import coil3.request.SuccessResult
|
||||
import coil3.request.transformations
|
||||
import coil3.size.Scale
|
||||
import coil3.toBitmap
|
||||
import com.amap.api.maps.model.LatLng
|
||||
import com.amap.api.services.core.PoiItemV2
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
@@ -393,8 +396,8 @@ class MessageViewModel @Inject constructor(
|
||||
uriDetail?.size?.let(this::size::set)
|
||||
width = bounds.outWidth
|
||||
height = bounds.outHeight
|
||||
thumb = uri.let(::loadInSize).let(::blur).let(::blur).let(::blur).let(::blur)
|
||||
.let(::quality)
|
||||
thumb = uri.let(::loadInSize)?.let(::blur)?.let(::blur)?.let(::blur)?.let(::blur)
|
||||
?.let(::quality)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -446,29 +449,35 @@ class MessageViewModel @Inject constructor(
|
||||
return options
|
||||
}
|
||||
|
||||
private fun loadInSize(data: Any): Bitmap {
|
||||
private fun loadInSize(data: Any): Bitmap? {
|
||||
val request = ImageRequest.Builder(application)
|
||||
.data(data)
|
||||
.size(320, 320)
|
||||
.scale(Scale.FIT)
|
||||
.build()
|
||||
return (application.imageLoader.executeBlocking(request).drawable as BitmapDrawable).bitmap.copy(
|
||||
Bitmap.Config.ARGB_8888,
|
||||
true
|
||||
)
|
||||
val result = application.imageLoader.executeBlocking(request)
|
||||
return when (result) {
|
||||
is ErrorResult -> null
|
||||
is SuccessResult -> result.image.toBitmap().copy(
|
||||
Bitmap.Config.ARGB_8888,
|
||||
true
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun blur(bitmap: Bitmap): Bitmap {
|
||||
private fun blur(bitmap: Bitmap): Bitmap? {
|
||||
val request = ImageRequest.Builder(application)
|
||||
.data(bitmap)
|
||||
.transformations(BlurTransformation(25f, 1f))
|
||||
.build()
|
||||
return (application.imageLoader.executeBlocking(request).drawable.also {
|
||||
println("Drawable $it")
|
||||
} as BitmapDrawable).bitmap.copy(
|
||||
Bitmap.Config.ARGB_8888,
|
||||
true
|
||||
)
|
||||
val result = application.imageLoader.executeBlocking(request)
|
||||
return when (result) {
|
||||
is ErrorResult -> null
|
||||
is SuccessResult -> result.image.toBitmap().copy(
|
||||
Bitmap.Config.ARGB_8888,
|
||||
true
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun quality(bitmap: Bitmap): ByteArray {
|
||||
|
||||
@@ -18,7 +18,7 @@ import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import cn.tabidachi.electro.ext.timeFormat
|
||||
import coil.compose.AsyncImage
|
||||
import coil3.compose.AsyncImage
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package cn.tabidachi.electro.ui.common
|
||||
|
||||
import androidx.compose.animation.core.LinearOutSlowInEasing
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.LocalIndication
|
||||
import androidx.compose.foundation.clickable
|
||||
@@ -19,9 +20,6 @@ import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.rounded.KeyboardArrowDown
|
||||
import androidx.compose.material.pullrefreshx.PullRefreshIndicator
|
||||
import androidx.compose.material.pullrefreshx.pullRefresh
|
||||
import androidx.compose.material.pullrefreshx.rememberPullRefreshState
|
||||
import androidx.compose.material3.DropdownMenu
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
@@ -30,6 +28,9 @@ import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.SmallFloatingActionButton
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.pulltorefresh.PullToRefreshDefaults
|
||||
import androidx.compose.material3.pulltorefresh.pullToRefresh
|
||||
import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
@@ -39,6 +40,7 @@ import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalClipboardManager
|
||||
import androidx.compose.ui.res.stringResource
|
||||
@@ -50,7 +52,7 @@ import cn.tabidachi.electro.ui.ElectroNavigationActions
|
||||
import cn.tabidachi.electro.ui.component.PopupMenu
|
||||
import cn.tabidachi.electro.ui.component.popupMenuAnchor
|
||||
import cn.tabidachi.electro.ui.component.rememberPopupState
|
||||
import coil.compose.AsyncImage
|
||||
import coil3.compose.AsyncImage
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@@ -63,12 +65,6 @@ fun MessageColumn(
|
||||
isMultiSession: Boolean = false,
|
||||
canSendMessage: Boolean = true,
|
||||
) {
|
||||
val refreshState = rememberPullRefreshState(
|
||||
refreshing = viewModel.isRefresh,
|
||||
onRefresh = {
|
||||
viewModel.onRefresh()
|
||||
}
|
||||
)
|
||||
val listState = rememberLazyListState()
|
||||
val messages = viewModel.messages
|
||||
val messageSendingQueue = viewModel.uploadMessages
|
||||
@@ -79,10 +75,14 @@ fun MessageColumn(
|
||||
mutableStateOf<DownloadMessageItem?>(null)
|
||||
}
|
||||
val popupState = rememberPopupState()
|
||||
val refreshState = rememberPullToRefreshState()
|
||||
val scaleFraction = {
|
||||
if (viewModel.isRefresh) 1f
|
||||
else LinearOutSlowInEasing.transform(refreshState.distanceFraction).coerceIn(0f, 1f)
|
||||
}
|
||||
Column(modifier = modifier) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.pullRefresh(state = refreshState)
|
||||
.weight(1f)
|
||||
.fillMaxWidth()
|
||||
) {
|
||||
@@ -91,7 +91,11 @@ fun MessageColumn(
|
||||
reverseLayout = true,
|
||||
modifier = Modifier
|
||||
.matchParentSize()
|
||||
|
||||
.pullToRefresh(
|
||||
state = refreshState,
|
||||
isRefreshing = viewModel.isRefresh,
|
||||
onRefresh = viewModel::onRefresh
|
||||
)
|
||||
) {
|
||||
items(messageSendingQueue) { item ->
|
||||
val (menu, onMenuChange) = remember {
|
||||
@@ -162,35 +166,41 @@ fun MessageColumn(
|
||||
viewModel.messages.firstOrNull { it.message.mid == item.message.reply }
|
||||
AttachmentMessage(
|
||||
item = item,
|
||||
replyContent = replyItem?.let {
|
||||
{
|
||||
ReplyContent(
|
||||
item = it,
|
||||
scope = scope,
|
||||
color = when (item.type) {
|
||||
BubbleType.Incoming -> MaterialTheme.colorScheme.secondary
|
||||
BubbleType.Outgoing -> MaterialTheme.colorScheme.primary
|
||||
},
|
||||
onScrollTo = {
|
||||
listState.animateScrollToItem(
|
||||
viewModel.messages.indexOf(
|
||||
replyItem
|
||||
)
|
||||
replyContent = {
|
||||
if (replyItem != null) ReplyContent(
|
||||
item = replyItem,
|
||||
scope = scope,
|
||||
color = when (item.type) {
|
||||
BubbleType.Incoming -> MaterialTheme.colorScheme.secondary
|
||||
BubbleType.Outgoing -> MaterialTheme.colorScheme.primary
|
||||
},
|
||||
onScrollTo = {
|
||||
listState.animateScrollToItem(
|
||||
viewModel.messages.indexOf(
|
||||
replyItem
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
PullRefreshIndicator(
|
||||
refreshing = viewModel.isRefresh,
|
||||
state = refreshState,
|
||||
modifier = Modifier.align(Alignment.TopCenter)
|
||||
)
|
||||
Box(
|
||||
Modifier
|
||||
.align(Alignment.TopCenter)
|
||||
.graphicsLayer {
|
||||
scaleX = scaleFraction()
|
||||
scaleY = scaleFraction()
|
||||
}
|
||||
) {
|
||||
PullToRefreshDefaults.Indicator(
|
||||
state = refreshState,
|
||||
isRefreshing = viewModel.isRefresh
|
||||
)
|
||||
}
|
||||
androidx.compose.animation.AnimatedVisibility(
|
||||
visible = firstVisibleItem > 1 && listState.isScrollingUp(),
|
||||
modifier = Modifier.align(Alignment.BottomEnd)
|
||||
|
||||
@@ -52,7 +52,7 @@ fun MessageTextField(
|
||||
),
|
||||
cursorBrush = SolidColor(MaterialTheme.colorScheme.primary),
|
||||
decorationBox = {
|
||||
TextFieldDefaults.TextFieldDecorationBox(
|
||||
TextFieldDefaults.DecorationBox(
|
||||
value = text,
|
||||
innerTextField = it,
|
||||
enabled = true,
|
||||
|
||||
@@ -27,7 +27,7 @@ fun MotionTopAppBar(
|
||||
navigationIcon: @Composable () -> Unit = {},
|
||||
actions: @Composable RowScope.() -> Unit = {},
|
||||
windowInsets: WindowInsets = TopAppBarDefaults.windowInsets,
|
||||
colors: TopAppBarColors = TopAppBarDefaults.smallTopAppBarColors(),
|
||||
colors: TopAppBarColors = TopAppBarDefaults.topAppBarColors(),
|
||||
scrollBehavior: TopAppBarScrollBehavior? = null,
|
||||
progress: Float
|
||||
) {
|
||||
|
||||
@@ -36,7 +36,7 @@ fun SearchTextField(
|
||||
singleLine = true,
|
||||
cursorBrush = SolidColor(MaterialTheme.colorScheme.primary),
|
||||
decorationBox = {
|
||||
TextFieldDefaults.TextFieldDecorationBox(
|
||||
TextFieldDefaults.DecorationBox(
|
||||
value = value,
|
||||
innerTextField = it,
|
||||
enabled = true,
|
||||
|
||||
@@ -42,7 +42,7 @@ fun SimpleTextField(
|
||||
visualTransformation = visualTransformation,
|
||||
cursorBrush = SolidColor(MaterialTheme.colorScheme.primary),
|
||||
decorationBox = {
|
||||
TextFieldDefaults.TextFieldDecorationBox(
|
||||
TextFieldDefaults.DecorationBox(
|
||||
value = value,
|
||||
innerTextField = it,
|
||||
enabled = true,
|
||||
@@ -69,11 +69,11 @@ fun SimpleTextField(
|
||||
*/
|
||||
|
||||
if (isError) {
|
||||
TextFieldDefaults.OutlinedBorderContainerBox(
|
||||
TextFieldDefaults.Container(
|
||||
enabled = true,
|
||||
isError = isError,
|
||||
interactionSource = interactionSource,
|
||||
colors = TextFieldDefaults.outlinedTextFieldColors()
|
||||
colors = TextFieldDefaults.colors()
|
||||
)
|
||||
}
|
||||
}, shape = shape,
|
||||
|
||||
@@ -48,9 +48,9 @@ import cn.tabidachi.electro.ui.common.attachment.AudioAttachment
|
||||
import cn.tabidachi.electro.ui.common.attachment.FileAttachment
|
||||
import cn.tabidachi.electro.ui.common.attachment.LocationAttachment
|
||||
import cn.tabidachi.electro.ui.common.attachment.VoiceAttachment
|
||||
import coil.compose.AsyncImage
|
||||
import coil.decode.VideoFrameDecoder
|
||||
import coil.request.ImageRequest
|
||||
import coil3.compose.AsyncImage
|
||||
import coil3.request.ImageRequest
|
||||
import coil3.video.VideoFrameDecoder
|
||||
|
||||
@OptIn(ExperimentalAnimationApi::class)
|
||||
@Composable
|
||||
|
||||
@@ -54,7 +54,7 @@ import androidx.navigation.NavHostController
|
||||
import cn.tabidachi.electro.R
|
||||
import cn.tabidachi.electro.ext.regex
|
||||
import cn.tabidachi.electro.ui.ElectroNavigationActions
|
||||
import coil.compose.AsyncImage
|
||||
import coil3.compose.AsyncImage
|
||||
|
||||
@OptIn(
|
||||
ExperimentalMaterial3Api::class,
|
||||
@@ -96,7 +96,7 @@ fun ContactScreen(
|
||||
value = viewState.filter,
|
||||
onValueChange = viewModel::onQueryValueChange,
|
||||
decorationBox = {
|
||||
TextFieldDefaults.TextFieldDecorationBox(
|
||||
TextFieldDefaults.DecorationBox(
|
||||
value = viewState.filter,
|
||||
innerTextField = it,
|
||||
enabled = true,
|
||||
@@ -110,7 +110,7 @@ fun ContactScreen(
|
||||
},
|
||||
contentPadding = PaddingValues(),
|
||||
container = {},
|
||||
colors = TextFieldDefaults.textFieldColors(),
|
||||
colors = TextFieldDefaults.colors(),
|
||||
trailingIcon = {
|
||||
IconButton(onClick = {
|
||||
viewModel.changeSearchState(false)
|
||||
|
||||
@@ -34,7 +34,7 @@ import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import cn.tabidachi.electro.R
|
||||
import cn.tabidachi.electro.ui.ElectroNavigationActions
|
||||
import coil.compose.AsyncImage
|
||||
import coil3.compose.AsyncImage
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
|
||||
@@ -45,7 +45,7 @@ import androidx.compose.ui.unit.dp
|
||||
import cn.tabidachi.electro.R
|
||||
import cn.tabidachi.electro.ui.ElectroNavigationActions
|
||||
import cn.tabidachi.electro.ui.common.ImageTopAppBar
|
||||
import coil.compose.AsyncImage
|
||||
import coil3.compose.AsyncImage
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
|
||||
@@ -48,7 +48,7 @@ import androidx.compose.ui.unit.dp
|
||||
import cn.tabidachi.electro.R
|
||||
import cn.tabidachi.electro.ui.ElectroNavigationActions
|
||||
import cn.tabidachi.electro.ui.common.SimpleTextField
|
||||
import coil.compose.AsyncImage
|
||||
import coil3.compose.AsyncImage
|
||||
import com.mr0xf00.easycrop.CropResult
|
||||
import com.mr0xf00.easycrop.crop
|
||||
import com.mr0xf00.easycrop.rememberImageCropper
|
||||
|
||||
@@ -39,7 +39,7 @@ import cn.tabidachi.electro.R
|
||||
import cn.tabidachi.electro.ui.ElectroNavigationActions
|
||||
import cn.tabidachi.electro.ui.common.MessageColumn
|
||||
import cn.tabidachi.electro.ui.common.MessageViewModel
|
||||
import coil.compose.AsyncImage
|
||||
import coil3.compose.AsyncImage
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
|
||||
@@ -38,7 +38,7 @@ import cn.tabidachi.electro.R
|
||||
import cn.tabidachi.electro.ext.regex
|
||||
import cn.tabidachi.electro.ui.ElectroNavigationActions
|
||||
import cn.tabidachi.electro.ui.common.SimpleTextField
|
||||
import coil.compose.AsyncImage
|
||||
import coil3.compose.AsyncImage
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
|
||||
@@ -17,7 +17,7 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import coil.compose.AsyncImage
|
||||
import coil3.compose.AsyncImage
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
|
||||
@@ -38,7 +38,7 @@ import cn.tabidachi.electro.ui.ElectroNavigationActions
|
||||
import cn.tabidachi.electro.ui.common.MessageColumn
|
||||
import cn.tabidachi.electro.ui.common.MessageViewModel
|
||||
import cn.tabidachi.electro.ui.common.SimpleListItem
|
||||
import coil.compose.AsyncImage
|
||||
import coil3.compose.AsyncImage
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
|
||||
@@ -66,7 +66,7 @@ import cn.tabidachi.electro.data.database.entity.SessionType
|
||||
import cn.tabidachi.electro.model.UserQuery
|
||||
import cn.tabidachi.electro.ui.ElectroNavigationActions
|
||||
import cn.tabidachi.electro.ui.common.SearchTextField
|
||||
import coil.compose.AsyncImage
|
||||
import coil3.compose.AsyncImage
|
||||
import com.google.accompanist.pager.pagerTabIndicatorOffset
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@@ -146,12 +146,13 @@ fun SearchScreen(
|
||||
userScrollEnabled = true,
|
||||
reverseLayout = false,
|
||||
contentPadding = PaddingValues(0.dp),
|
||||
beyondBoundsPageCount = 0,
|
||||
beyondViewportPageCount = 0,
|
||||
pageSize = PageSize.Fill,
|
||||
flingBehavior = PagerDefaults.flingBehavior(state = pagerState),
|
||||
key = null,
|
||||
pageNestedScrollConnection = PagerDefaults.pageNestedScrollConnection(
|
||||
Orientation.Horizontal
|
||||
state = pagerState,
|
||||
orientation = Orientation.Horizontal
|
||||
),
|
||||
pageContent = {
|
||||
when (SearchTab.entries[it]) {
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
package cn.tabidachi.electro.ui.server
|
||||
|
||||
data class ServerState(
|
||||
val url: String = "",
|
||||
val port: String = "",
|
||||
val minioUrl: String = "",
|
||||
val minioPort: String = "",
|
||||
val dialogVisible: Boolean = false,
|
||||
val dialogType: ServerDialogType = ServerDialogType.ElectroUrl,
|
||||
val dialogValue: String = ""
|
||||
)
|
||||
|
||||
data class ServerActions(
|
||||
val onClick: () -> Unit = {},
|
||||
val showDialog: (ServerDialogType) -> Unit = {},
|
||||
val hideDialog: () -> Unit = {},
|
||||
val onSave: () -> Unit = {},
|
||||
val onDialogValueChange: (String) -> Unit = {},
|
||||
val onNavigateUp: () -> Unit = {}
|
||||
)
|
||||
@@ -0,0 +1,39 @@
|
||||
package cn.tabidachi.electro.ui.server
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import cn.tabidachi.electro.ui.ElectroNavigationActions
|
||||
|
||||
class ServerCoordinator(
|
||||
val viewModel: ServerViewModel, val navigationActions: ElectroNavigationActions
|
||||
) {
|
||||
val state = viewModel.state
|
||||
|
||||
fun doStuff() {
|
||||
// TODO Handle UI Action
|
||||
}
|
||||
|
||||
fun showDialog(type: ServerDialogType) = viewModel.showDialog(type)
|
||||
|
||||
fun hideDialog() = viewModel.hideDialog()
|
||||
|
||||
fun onSave() = viewModel.onSave()
|
||||
|
||||
fun onDialogValueChange(value: String) = viewModel.onDialogValueChange(value)
|
||||
|
||||
fun onNavigateUp() = navigationActions.navigateUp()
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun rememberServerCoordinator(
|
||||
viewModel: ServerViewModel = hiltViewModel(),
|
||||
navigationActions: ElectroNavigationActions = ElectroNavigationActions(rememberNavController())
|
||||
): ServerCoordinator {
|
||||
return remember(viewModel, navigationActions) {
|
||||
ServerCoordinator(
|
||||
viewModel = viewModel, navigationActions = navigationActions
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package cn.tabidachi.electro.ui.server
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.navigation.NavGraphBuilder
|
||||
import androidx.navigation.compose.composable
|
||||
import cn.tabidachi.electro.ui.ElectroDestinations
|
||||
import cn.tabidachi.electro.ui.ElectroNavigationActions
|
||||
|
||||
@Composable
|
||||
fun ServerRoute(
|
||||
coordinator: ServerCoordinator = rememberServerCoordinator(),
|
||||
) {
|
||||
// State observing and declarations
|
||||
val state by coordinator.state.collectAsStateWithLifecycle(ServerState())
|
||||
|
||||
// UI Actions
|
||||
val actions = rememberServerActions(coordinator)
|
||||
|
||||
// UI Rendering
|
||||
ServerScreen(state, actions)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun rememberServerActions(coordinator: ServerCoordinator): ServerActions {
|
||||
return remember(coordinator) {
|
||||
ServerActions(
|
||||
onClick = coordinator::doStuff,
|
||||
showDialog = coordinator::showDialog,
|
||||
hideDialog = coordinator::hideDialog,
|
||||
onSave = coordinator::onSave,
|
||||
onDialogValueChange = coordinator::onDialogValueChange,
|
||||
onNavigateUp = coordinator::onNavigateUp
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun NavGraphBuilder.serverRoute(navigationActions: ElectroNavigationActions) {
|
||||
composable(ElectroDestinations.SERVER_ROUTE) {
|
||||
ServerRoute(coordinator = rememberServerCoordinator(navigationActions = navigationActions))
|
||||
}
|
||||
}
|
||||
154
app/src/main/java/cn/tabidachi/electro/ui/server/ServerScreen.kt
Normal file
154
app/src/main/java/cn/tabidachi/electro/ui/server/ServerScreen.kt
Normal file
@@ -0,0 +1,154 @@
|
||||
package cn.tabidachi.electro.ui.server
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.rounded.ArrowBack
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.ListItem
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun ServerScreen(
|
||||
state: ServerState,
|
||||
actions: ServerActions,
|
||||
) {
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = {
|
||||
Text(text = "服务器地址设置")
|
||||
}, navigationIcon = {
|
||||
IconButton(
|
||||
onClick = actions.onNavigateUp
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Rounded.ArrowBack,
|
||||
contentDescription = null
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(top = it.calculateTopPadding())
|
||||
) {
|
||||
Text(
|
||||
text = "Electro 服务器",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier.padding(vertical = 8.dp, horizontal = 16.dp)
|
||||
)
|
||||
ListItem(
|
||||
headlineContent = {
|
||||
Text(text = "服务器地址")
|
||||
}, supportingContent = {
|
||||
Text(text = state.url)
|
||||
}, modifier = Modifier.clickable {
|
||||
actions.showDialog(ServerDialogType.ElectroUrl)
|
||||
}
|
||||
)
|
||||
ListItem(
|
||||
headlineContent = {
|
||||
Text(text = "端口")
|
||||
}, supportingContent = {
|
||||
Text(text = state.port)
|
||||
}, modifier = Modifier.clickable {
|
||||
actions.showDialog(ServerDialogType.ElectroPort)
|
||||
}
|
||||
)
|
||||
Text(
|
||||
text = "MinIO 服务器",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier.padding(vertical = 8.dp, horizontal = 16.dp)
|
||||
)
|
||||
ListItem(
|
||||
headlineContent = {
|
||||
Text(text = "服务器地址")
|
||||
}, supportingContent = {
|
||||
Text(text = state.minioUrl)
|
||||
}, modifier = Modifier.clickable {
|
||||
actions.showDialog(ServerDialogType.MinioUrl)
|
||||
}
|
||||
)
|
||||
ListItem(
|
||||
headlineContent = {
|
||||
Text(text = "端口")
|
||||
}, supportingContent = {
|
||||
Text(text = state.minioPort)
|
||||
}, modifier = Modifier.clickable {
|
||||
actions.showDialog(ServerDialogType.MinioPort)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
if (state.dialogVisible) AlertDialog(
|
||||
onDismissRequest = actions.hideDialog,
|
||||
confirmButton = {
|
||||
TextButton(
|
||||
onClick = actions.onSave
|
||||
) {
|
||||
Text(text = "保存")
|
||||
}
|
||||
}, dismissButton = {
|
||||
TextButton(
|
||||
onClick = actions.hideDialog
|
||||
) {
|
||||
Text(text = "取消")
|
||||
}
|
||||
}, title = {
|
||||
Text(text = state.dialogType.title)
|
||||
}, text = {
|
||||
OutlinedTextField(
|
||||
value = state.dialogValue,
|
||||
onValueChange = actions.onDialogValueChange,
|
||||
label = {
|
||||
Text(text = state.dialogType.label)
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
enum class ServerDialogType(
|
||||
val title: String,
|
||||
val label: String,
|
||||
) {
|
||||
ElectroUrl("Electro 服务器地址", "地址"),
|
||||
ElectroPort("Electro 服务器端口", "端口"),
|
||||
MinioUrl("MinIO 服务器地址", "地址"),
|
||||
MinioPort("MinIO 服务器端口", "端口")
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Preview(name = "Server")
|
||||
private fun ServerScreenPreview() {
|
||||
ServerScreen(
|
||||
state = ServerState(
|
||||
url = "http://electro.tabidachi.moe",
|
||||
port = "23333",
|
||||
minioUrl = "http://minio.tabidachi.moe",
|
||||
minioPort = "9000",
|
||||
dialogVisible = true,
|
||||
dialogValue = "http://www.tabidachi.moe"
|
||||
),
|
||||
actions = ServerActions()
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
package cn.tabidachi.electro.ui.server
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import cn.tabidachi.electro.data.network.Ktor
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import io.ktor.client.plugins.defaultRequest
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class ServerViewModel @Inject constructor(
|
||||
val ktor: Ktor
|
||||
) : ViewModel() {
|
||||
|
||||
private val _state: MutableStateFlow<ServerState> = MutableStateFlow(ServerState())
|
||||
|
||||
val state: StateFlow<ServerState> = _state.asStateFlow()
|
||||
|
||||
fun showDialog(type: ServerDialogType) {
|
||||
_state.update {
|
||||
it.copy(
|
||||
dialogType = type,
|
||||
dialogVisible = true,
|
||||
dialogValue = when (type) {
|
||||
ServerDialogType.ElectroUrl -> _state.value.url
|
||||
ServerDialogType.ElectroPort -> _state.value.port
|
||||
ServerDialogType.MinioUrl -> _state.value.minioUrl
|
||||
ServerDialogType.MinioPort -> _state.value.minioPort
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun hideDialog() = _state.update { it.copy(dialogVisible = false, dialogValue = "") }
|
||||
|
||||
fun onSave() {
|
||||
when (_state.value.dialogType) {
|
||||
ServerDialogType.ElectroUrl -> _state.update { it.copy(url = it.dialogValue) }
|
||||
ServerDialogType.ElectroPort -> _state.update { it.copy(port = it.dialogValue) }
|
||||
ServerDialogType.MinioUrl -> _state.update { it.copy(minioUrl = it.dialogValue) }
|
||||
ServerDialogType.MinioPort -> _state.update { it.copy(minioPort = it.dialogValue) }
|
||||
}
|
||||
|
||||
hideDialog()
|
||||
|
||||
ktor.client.config {
|
||||
defaultRequest {
|
||||
this.host = _state.value.url
|
||||
this.port = _state.value.port.toInt()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun onDialogValueChange(value: String) {
|
||||
_state.update {
|
||||
it.copy(dialogValue = value)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -40,7 +40,7 @@ import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import cn.tabidachi.electro.R
|
||||
import cn.tabidachi.electro.ui.sessions.components.UserCard
|
||||
import coil.compose.AsyncImage
|
||||
import coil3.compose.AsyncImage
|
||||
|
||||
@Composable
|
||||
fun SessionsDrawerSheet(
|
||||
|
||||
@@ -15,9 +15,9 @@ import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.rounded.Menu
|
||||
import androidx.compose.material.icons.rounded.Search
|
||||
import androidx.compose.material.pullrefreshx.PullRefreshIndicator
|
||||
import androidx.compose.material.pullrefreshx.pullRefresh
|
||||
import androidx.compose.material.pullrefreshx.rememberPullRefreshState
|
||||
//import androidx.compose.material.pullrefreshx.PullRefreshIndicator
|
||||
//import androidx.compose.material.pullrefreshx.pullRefresh
|
||||
//import androidx.compose.material.pullrefreshx.rememberPullRefreshState
|
||||
import androidx.compose.material3.DrawerValue
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
@@ -59,11 +59,11 @@ fun SessionsPreview(modifier: Modifier = Modifier) {
|
||||
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior()
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
val refreshState = rememberPullRefreshState(
|
||||
/*val refreshState = rememberPullRefreshState(
|
||||
refreshing = viewState.isRefresh,
|
||||
onRefresh = {
|
||||
}
|
||||
)
|
||||
)*/
|
||||
val dialogs = remember {
|
||||
mutableStateListOf<Dialog>()
|
||||
}
|
||||
@@ -131,7 +131,7 @@ fun SessionsPreview(modifier: Modifier = Modifier) {
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.pullRefresh(state = refreshState)
|
||||
//.pullRefresh(state = refreshState)
|
||||
.fillMaxSize()
|
||||
.padding(top = it.calculateTopPadding())
|
||||
) {
|
||||
@@ -170,11 +170,11 @@ fun SessionsPreview(modifier: Modifier = Modifier) {
|
||||
Spacer(modifier = Modifier.navigationBarsPadding())
|
||||
}
|
||||
}
|
||||
PullRefreshIndicator(
|
||||
/*PullRefreshIndicator(
|
||||
refreshing = viewState.isRefresh,
|
||||
state = refreshState,
|
||||
modifier = Modifier.align(Alignment.TopCenter)
|
||||
)
|
||||
)*/
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package cn.tabidachi.electro.ui.sessions
|
||||
|
||||
import androidx.compose.animation.core.LinearOutSlowInEasing
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
@@ -15,9 +16,6 @@ import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.rounded.Menu
|
||||
import androidx.compose.material.icons.rounded.Search
|
||||
import androidx.compose.material.pullrefreshx.PullRefreshIndicator
|
||||
import androidx.compose.material.pullrefreshx.pullRefresh
|
||||
import androidx.compose.material.pullrefreshx.rememberPullRefreshState
|
||||
import androidx.compose.material3.DrawerValue
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
@@ -28,6 +26,9 @@ import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.material3.pulltorefresh.PullToRefreshDefaults
|
||||
import androidx.compose.material3.pulltorefresh.pullToRefresh
|
||||
import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState
|
||||
import androidx.compose.material3.rememberDrawerState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
@@ -37,6 +38,7 @@ import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.painterResource
|
||||
@@ -70,14 +72,13 @@ fun SessionsScreen(
|
||||
LaunchedEffect(Unit) {
|
||||
viewModel.findUser()
|
||||
}
|
||||
val refreshState = rememberPullRefreshState(
|
||||
refreshing = viewState.isRefresh,
|
||||
onRefresh = {
|
||||
viewModel.onRefresh()
|
||||
}
|
||||
)
|
||||
val refreshState = rememberPullToRefreshState()
|
||||
val context = LocalContext.current
|
||||
val dialogs by viewModel.dialogs.collectAsState(initial = emptyList())
|
||||
val scaleFraction = {
|
||||
if (viewState.isRefresh) 1f
|
||||
else LinearOutSlowInEasing.transform(refreshState.distanceFraction).coerceIn(0f, 1f)
|
||||
}
|
||||
ModalNavigationDrawer(
|
||||
drawerState = drawerState,
|
||||
drawerContent = {
|
||||
@@ -92,15 +93,19 @@ fun SessionsScreen(
|
||||
DrawerSheetItem.CONTACT -> {
|
||||
navigationActions.navigateToContact()
|
||||
}
|
||||
|
||||
DrawerSheetItem.NEW_GROUP -> {
|
||||
navigationActions.navigateToCreateGroup()
|
||||
}
|
||||
|
||||
DrawerSheetItem.NEW_CHANNEL -> {
|
||||
navigationActions.navigateToCreateChannel()
|
||||
}
|
||||
|
||||
DrawerSheetItem.FAVORITE -> {
|
||||
context.toast("功能未实现")
|
||||
}
|
||||
|
||||
DrawerSheetItem.SETTINGS -> {
|
||||
navigationActions.navigateToSettings()
|
||||
}
|
||||
@@ -131,7 +136,8 @@ fun SessionsScreen(
|
||||
}
|
||||
}, scrollBehavior = scrollBehavior
|
||||
)
|
||||
}, floatingActionButton = {
|
||||
},
|
||||
floatingActionButton = {
|
||||
SessionsFloatingActionButton(
|
||||
buttonState = buttonState,
|
||||
onFabItemClicked = {
|
||||
@@ -150,11 +156,18 @@ fun SessionsScreen(
|
||||
}
|
||||
}
|
||||
)
|
||||
}, modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection)
|
||||
},
|
||||
modifier = Modifier
|
||||
.nestedScroll(scrollBehavior.nestedScrollConnection)
|
||||
.pullToRefresh(
|
||||
state = refreshState,
|
||||
isRefreshing = viewState.isRefresh,
|
||||
onRefresh = viewModel::onRefresh
|
||||
)
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.pullRefresh(state = refreshState)
|
||||
//.pullRefresh(state = refreshState)
|
||||
.fillMaxSize()
|
||||
.padding(top = it.calculateTopPadding())
|
||||
) {
|
||||
@@ -171,7 +184,9 @@ fun SessionsScreen(
|
||||
Image(
|
||||
painter = painterResource(id = R.drawable.chatgpt),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(48.dp).clip(CircleShape)
|
||||
modifier = Modifier
|
||||
.size(48.dp)
|
||||
.clip(CircleShape)
|
||||
)
|
||||
}
|
||||
)
|
||||
@@ -210,11 +225,19 @@ fun SessionsScreen(
|
||||
Spacer(modifier = Modifier.navigationBarsPadding())
|
||||
}
|
||||
}
|
||||
PullRefreshIndicator(
|
||||
refreshing = viewState.isRefresh,
|
||||
state = refreshState,
|
||||
modifier = Modifier.align(Alignment.TopCenter)
|
||||
)
|
||||
Box(
|
||||
Modifier
|
||||
.align(Alignment.TopCenter)
|
||||
.graphicsLayer {
|
||||
scaleX = scaleFraction()
|
||||
scaleY = scaleFraction()
|
||||
}
|
||||
) {
|
||||
PullToRefreshDefaults.Indicator(
|
||||
state = refreshState,
|
||||
isRefreshing = viewState.isRefresh
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@ import cn.tabidachi.electro.PreferenceConstant
|
||||
import cn.tabidachi.electro.data.database.entity.User
|
||||
import cn.tabidachi.electro.ext.dataStore
|
||||
import cn.tabidachi.electro.ui.theme.DarkLight
|
||||
import coil.compose.AsyncImage
|
||||
import coil3.compose.AsyncImage
|
||||
import kotlinx.coroutines.flow.filterNotNull
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@@ -35,7 +35,7 @@ import cn.tabidachi.electro.R
|
||||
import cn.tabidachi.electro.ui.ElectroNavigationActions
|
||||
import cn.tabidachi.electro.ui.common.ImageTopAppBar
|
||||
import cn.tabidachi.electro.ui.common.SimpleListItem
|
||||
import coil.compose.AsyncImage
|
||||
import coil3.compose.AsyncImage
|
||||
import com.mr0xf00.easycrop.CropResult
|
||||
import com.mr0xf00.easycrop.crop
|
||||
import com.mr0xf00.easycrop.rememberImageCropper
|
||||
|
||||
@@ -2,7 +2,6 @@ package cn.tabidachi.electro.ui.settings
|
||||
|
||||
import android.app.Application
|
||||
import android.graphics.Bitmap
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
@@ -175,7 +174,7 @@ class SettingsViewModel @Inject constructor(
|
||||
fun onLanguageChange(language: Language) {
|
||||
viewModelScope.launch {
|
||||
val languageTags = LocaleListCompat.forLanguageTags(language.tag)
|
||||
AppCompatDelegate.setApplicationLocales(languageTags)
|
||||
//AppCompatDelegate.setApplicationLocales(languageTags)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -53,12 +53,19 @@ fun AnimatedColorScheme(colorScheme: ColorScheme, content: @Composable (ColorSch
|
||||
colors[26].value,
|
||||
colors[27].value,
|
||||
colors[28].value,
|
||||
colors[29].value,
|
||||
colors[30].value,
|
||||
colors[31].value,
|
||||
colors[32].value,
|
||||
colors[33].value,
|
||||
colors[34].value,
|
||||
colors[35].value,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
class ColorSchemeViewModel : ViewModel() {
|
||||
val colors = Array(29) { Animatable(Color.Unspecified) }
|
||||
val colors = Array(36) { Animatable(Color.Unspecified) }
|
||||
}
|
||||
|
||||
private fun ColorScheme.toColorArray(): Array<Color> = arrayOf(
|
||||
@@ -91,6 +98,13 @@ private fun ColorScheme.toColorArray(): Array<Color> = arrayOf(
|
||||
outline,
|
||||
outlineVariant,
|
||||
scrim,
|
||||
surfaceBright,
|
||||
surfaceDim,
|
||||
surfaceContainer,
|
||||
surfaceContainerHigh,
|
||||
surfaceContainerHighest,
|
||||
surfaceContainerLow,
|
||||
surfaceContainerLowest,
|
||||
)
|
||||
|
||||
private val ColorSchemeAnimationSpec = tween<Color>(1000)
|
||||
@@ -10,13 +10,10 @@ import androidx.compose.material3.dynamicDarkColorScheme
|
||||
import androidx.compose.material3.dynamicLightColorScheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.SideEffect
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import cn.tabidachi.electro.R
|
||||
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
||||
import com.google.accompanist.permissions.rememberMultiplePermissionsState
|
||||
import com.google.accompanist.systemuicontroller.rememberSystemUiController
|
||||
|
||||
@OptIn(ExperimentalPermissionsApi::class)
|
||||
@Composable
|
||||
@@ -59,11 +56,6 @@ fun ElectroTheme(
|
||||
}
|
||||
})
|
||||
|
||||
val systemUiController = rememberSystemUiController()
|
||||
SideEffect {
|
||||
systemUiController.setSystemBarsColor(color = Color.Transparent, darkIcons = !darkMode)
|
||||
systemUiController.isNavigationBarContrastEnforced = false
|
||||
}
|
||||
AnimatedColorScheme(colorScheme = colorScheme) {
|
||||
MaterialTheme(
|
||||
colorScheme = it,
|
||||
|
||||
@@ -1,111 +0,0 @@
|
||||
/*
|
||||
* Copyright 2022 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package androidx.compose.material.pullrefreshx
|
||||
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
|
||||
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
|
||||
import androidx.compose.ui.input.nestedscroll.NestedScrollSource.Companion.Drag
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.platform.debugInspectorInfo
|
||||
import androidx.compose.ui.platform.inspectable
|
||||
import androidx.compose.ui.unit.Velocity
|
||||
|
||||
/**
|
||||
* PullRefresh modifier to be used in conjunction with [PullRefreshState]. Provides a connection
|
||||
* to the nested scroll system. Based on Android's SwipeRefreshLayout.
|
||||
*
|
||||
* @sample androidx.compose.material.samples.PullRefreshSample
|
||||
*
|
||||
* @param state The [PullRefreshState] associated with this pull-to-refresh component.
|
||||
* The state will be updated by this modifier.
|
||||
* @param enabled If not enabled, all scroll delta and fling velocity will be ignored.
|
||||
*/
|
||||
// TODO(b/244423199): Move pullRefresh into its own material library similar to material-ripple.
|
||||
@ExperimentalMaterial3Api
|
||||
fun Modifier.pullRefresh(
|
||||
state: PullRefreshState,
|
||||
enabled: Boolean = true
|
||||
) = inspectable(inspectorInfo = debugInspectorInfo {
|
||||
name = "pullRefresh"
|
||||
properties["state"] = state
|
||||
properties["enabled"] = enabled
|
||||
}) {
|
||||
Modifier.pullRefresh(state::onPull, { state.onRelease() }, enabled)
|
||||
}
|
||||
|
||||
/**
|
||||
* A modifier for building pull-to-refresh components. Provides a connection to the nested scroll
|
||||
* system.
|
||||
*
|
||||
* @sample androidx.compose.material.samples.CustomPullRefreshSample
|
||||
*
|
||||
* @param onPull Callback for dispatching vertical scroll delta, takes float pullDelta as argument.
|
||||
* Positive delta (pulling down) is dispatched only if the child does not consume it (i.e. pulling
|
||||
* down despite being at the top of a scrollable component), whereas negative delta (swiping up) is
|
||||
* dispatched first (in case it is needed to push the indicator back up), and then whatever is not
|
||||
* consumed is passed on to the child.
|
||||
* @param onRelease Callback for when drag is released, takes float flingVelocity as argument.
|
||||
* @param enabled If not enabled, all scroll delta and fling velocity will be ignored and neither
|
||||
* [onPull] nor [onRelease] will be invoked.
|
||||
*/
|
||||
@ExperimentalMaterial3Api
|
||||
fun Modifier.pullRefresh(
|
||||
onPull: (pullDelta: Float) -> Float,
|
||||
onRelease: suspend (flingVelocity: Float) -> Unit,
|
||||
enabled: Boolean = true
|
||||
) = inspectable(inspectorInfo = debugInspectorInfo {
|
||||
name = "pullRefresh"
|
||||
properties["onPull"] = onPull
|
||||
properties["onRelease"] = onRelease
|
||||
properties["enabled"] = enabled
|
||||
}) {
|
||||
Modifier.nestedScroll(PullRefreshNestedScrollConnection(onPull, onRelease, enabled))
|
||||
}
|
||||
|
||||
private class PullRefreshNestedScrollConnection(
|
||||
private val onPull: (pullDelta: Float) -> Float,
|
||||
private val onRelease: suspend (flingVelocity: Float) -> Unit,
|
||||
private val enabled: Boolean
|
||||
) : NestedScrollConnection {
|
||||
|
||||
override fun onPreScroll(
|
||||
available: Offset,
|
||||
source: NestedScrollSource
|
||||
): Offset = when {
|
||||
!enabled -> Offset.Zero
|
||||
source == Drag && available.y < 0 -> Offset(0f, onPull(available.y)) // Swiping up
|
||||
else -> Offset.Zero
|
||||
}
|
||||
|
||||
override fun onPostScroll(
|
||||
consumed: Offset,
|
||||
available: Offset,
|
||||
source: NestedScrollSource
|
||||
): Offset = when {
|
||||
!enabled -> Offset.Zero
|
||||
source == Drag && available.y > 0 -> Offset(0f, onPull(available.y)) // Pulling down
|
||||
else -> Offset.Zero
|
||||
}
|
||||
|
||||
override suspend fun onPreFling(available: Velocity): Velocity {
|
||||
onRelease(available.y)
|
||||
return Velocity.Zero
|
||||
}
|
||||
}
|
||||
@@ -1,221 +0,0 @@
|
||||
/*
|
||||
* Copyright 2022 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package androidx.compose.material.pullrefreshx
|
||||
|
||||
import androidx.compose.animation.Crossfade
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.Canvas
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.geometry.Rect
|
||||
import androidx.compose.ui.geometry.center
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.Path
|
||||
import androidx.compose.ui.graphics.PathFillType
|
||||
import androidx.compose.ui.graphics.StrokeCap
|
||||
import androidx.compose.ui.graphics.drawscope.DrawScope
|
||||
import androidx.compose.ui.graphics.drawscope.Stroke
|
||||
import androidx.compose.ui.graphics.drawscope.rotate
|
||||
import androidx.compose.ui.semantics.contentDescription
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.unit.dp
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
import kotlin.math.pow
|
||||
|
||||
/**
|
||||
* The default indicator for Compose pull-to-refresh, based on Android's SwipeRefreshLayout.
|
||||
*
|
||||
* @sample androidx.compose.material.samples.PullRefreshSample
|
||||
*
|
||||
* @param refreshing A boolean representing whether a refresh is occurring.
|
||||
* @param state The [PullRefreshState] which controls where and how the indicator will be drawn.
|
||||
* @param modifier Modifiers for the indicator.
|
||||
* @param backgroundColor The color of the indicator's background.
|
||||
* @param contentColor The color of the indicator's arc and arrow.
|
||||
* @param scale A boolean controlling whether the indicator's size scales with pull progress or not.
|
||||
*/
|
||||
@Composable
|
||||
@ExperimentalMaterial3Api
|
||||
// TODO(b/244423199): Consider whether the state parameter should be replaced with lambdas to
|
||||
// enable people to use this indicator with custom pull-to-refresh components.
|
||||
fun PullRefreshIndicator(
|
||||
refreshing: Boolean,
|
||||
state: PullRefreshState,
|
||||
modifier: Modifier = Modifier,
|
||||
backgroundColor: Color = MaterialTheme.colorScheme.surface,
|
||||
// contentColor: Color = contentColorFor(backgroundColor),
|
||||
contentColor: Color = MaterialTheme.colorScheme.primary,
|
||||
scale: Boolean = false
|
||||
) {
|
||||
val showElevation by remember(refreshing, state) {
|
||||
derivedStateOf { refreshing || state.position > 0.5f }
|
||||
}
|
||||
|
||||
Surface(
|
||||
modifier = modifier
|
||||
.size(IndicatorSize)
|
||||
.pullRefreshIndicatorTransform(state, scale),
|
||||
shape = SpinnerShape,
|
||||
color = backgroundColor,
|
||||
tonalElevation = if (showElevation) Elevation else 0.dp,
|
||||
) {
|
||||
Crossfade(
|
||||
targetState = refreshing,
|
||||
animationSpec = tween(durationMillis = CrossfadeDurationMs)
|
||||
) { refreshing ->
|
||||
Box(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
val spinnerSize = (ArcRadius + StrokeWidth).times(2)
|
||||
|
||||
if (refreshing) {
|
||||
CircularProgressIndicator(
|
||||
color = contentColor,
|
||||
strokeWidth = StrokeWidth,
|
||||
modifier = Modifier.size(spinnerSize),
|
||||
)
|
||||
} else {
|
||||
CircularArrowIndicator(state, contentColor, Modifier.size(spinnerSize))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Modifier.size MUST be specified.
|
||||
*/
|
||||
@Composable
|
||||
@ExperimentalMaterial3Api
|
||||
private fun CircularArrowIndicator(
|
||||
state: PullRefreshState,
|
||||
color: Color,
|
||||
modifier: Modifier,
|
||||
) {
|
||||
val path = remember { Path().apply { fillType = PathFillType.EvenOdd } }
|
||||
|
||||
Canvas(modifier.semantics { contentDescription = "Refreshing" }) {
|
||||
val values = ArrowValues(state.progress)
|
||||
|
||||
rotate(degrees = values.rotation) {
|
||||
val arcRadius = ArcRadius.toPx() + StrokeWidth.toPx() / 2f
|
||||
val arcBounds = Rect(
|
||||
size.center.x - arcRadius,
|
||||
size.center.y - arcRadius,
|
||||
size.center.x + arcRadius,
|
||||
size.center.y + arcRadius
|
||||
)
|
||||
drawArc(
|
||||
color = color,
|
||||
alpha = values.alpha,
|
||||
startAngle = values.startAngle,
|
||||
sweepAngle = values.endAngle - values.startAngle,
|
||||
useCenter = false,
|
||||
topLeft = arcBounds.topLeft,
|
||||
size = arcBounds.size,
|
||||
style = Stroke(
|
||||
width = StrokeWidth.toPx(),
|
||||
cap = StrokeCap.Square
|
||||
)
|
||||
)
|
||||
drawArrow(path, arcBounds, color, values)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Immutable
|
||||
private class ArrowValues(
|
||||
val alpha: Float,
|
||||
val rotation: Float,
|
||||
val startAngle: Float,
|
||||
val endAngle: Float,
|
||||
val scale: Float
|
||||
)
|
||||
|
||||
private fun ArrowValues(progress: Float): ArrowValues {
|
||||
// Discard first 40% of progress. Scale remaining progress to full range between 0 and 100%.
|
||||
val adjustedPercent = max(min(1f, progress) - 0.4f, 0f) * 5 / 3
|
||||
// How far beyond the threshold pull has gone, as a percentage of the threshold.
|
||||
val overshootPercent = abs(progress) - 1.0f
|
||||
// Limit the overshoot to 200%. Linear between 0 and 200.
|
||||
val linearTension = overshootPercent.coerceIn(0f, 2f)
|
||||
// Non-linear tension. Increases with linearTension, but at a decreasing rate.
|
||||
val tensionPercent = linearTension - linearTension.pow(2) / 4
|
||||
|
||||
// Calculations based on SwipeRefreshLayout specification.
|
||||
val alpha = progress.coerceIn(0f, 1f)
|
||||
val endTrim = adjustedPercent * MaxProgressArc
|
||||
val rotation = (-0.25f + 0.4f * adjustedPercent + tensionPercent) * 0.5f
|
||||
val startAngle = rotation * 360
|
||||
val endAngle = (rotation + endTrim) * 360
|
||||
val scale = min(1f, adjustedPercent)
|
||||
|
||||
return ArrowValues(alpha, rotation, startAngle, endAngle, scale)
|
||||
}
|
||||
|
||||
private fun DrawScope.drawArrow(arrow: Path, bounds: Rect, color: Color, values: ArrowValues) {
|
||||
arrow.reset()
|
||||
arrow.moveTo(0f, 0f) // Move to left corner
|
||||
arrow.lineTo(x = ArrowWidth.toPx() * values.scale, y = 0f) // Line to right corner
|
||||
|
||||
// Line to tip of arrow
|
||||
arrow.lineTo(
|
||||
x = ArrowWidth.toPx() * values.scale / 2,
|
||||
y = ArrowHeight.toPx() * values.scale
|
||||
)
|
||||
|
||||
val radius = min(bounds.width, bounds.height) / 2f
|
||||
val inset = ArrowWidth.toPx() * values.scale / 2f
|
||||
arrow.translate(
|
||||
Offset(
|
||||
x = radius + bounds.center.x - inset,
|
||||
y = bounds.center.y + StrokeWidth.toPx() / 2f
|
||||
)
|
||||
)
|
||||
arrow.close()
|
||||
rotate(degrees = values.endAngle) {
|
||||
drawPath(path = arrow, color = color, alpha = values.alpha)
|
||||
}
|
||||
}
|
||||
|
||||
private const val CrossfadeDurationMs = 100
|
||||
private const val MaxProgressArc = 0.8f
|
||||
|
||||
private val IndicatorSize = 40.dp
|
||||
private val SpinnerShape = CircleShape
|
||||
private val ArcRadius = 7.5.dp
|
||||
private val StrokeWidth = 2.5.dp
|
||||
private val ArrowWidth = 10.dp
|
||||
private val ArrowHeight = 5.dp
|
||||
private val Elevation = 6.dp
|
||||
@@ -1,65 +0,0 @@
|
||||
/*
|
||||
* Copyright 2022 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package androidx.compose.material.pullrefreshx
|
||||
|
||||
import androidx.compose.animation.core.LinearOutSlowInEasing
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.composed
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.ui.layout.onSizeChanged
|
||||
import androidx.compose.ui.platform.debugInspectorInfo
|
||||
|
||||
/**
|
||||
* A modifier for translating the position and scaling the size of a pull-to-refresh indicator
|
||||
* based on the given [PullRefreshState].
|
||||
*
|
||||
* @sample androidx.compose.material.samples.PullRefreshIndicatorTransformSample
|
||||
*
|
||||
* @param state The [PullRefreshState] which determines the position of the indicator.
|
||||
* @param scale A boolean controlling whether the indicator's size scales with pull progress or not.
|
||||
*/
|
||||
@ExperimentalMaterial3Api
|
||||
// TODO: Consider whether the state parameter should be replaced with lambdas.
|
||||
fun Modifier.pullRefreshIndicatorTransform(
|
||||
state: PullRefreshState,
|
||||
scale: Boolean = false,
|
||||
) = composed(inspectorInfo = debugInspectorInfo {
|
||||
name = "pullRefreshIndicatorTransform"
|
||||
properties["state"] = state
|
||||
properties["scale"] = scale
|
||||
}) {
|
||||
var height by remember { mutableStateOf(0) }
|
||||
|
||||
Modifier
|
||||
.onSizeChanged { height = it.height }
|
||||
.graphicsLayer {
|
||||
translationY = state.position - height
|
||||
|
||||
if (scale && !state.refreshing) {
|
||||
val scaleFraction = LinearOutSlowInEasing
|
||||
.transform(state.position / state.threshold)
|
||||
.coerceIn(0f, 1f)
|
||||
scaleX = scaleFraction
|
||||
scaleY = scaleFraction
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,200 +0,0 @@
|
||||
/*
|
||||
* Copyright 2022 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package androidx.compose.material.pullrefreshx
|
||||
|
||||
import androidx.compose.animation.core.animate
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.SideEffect
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.rememberUpdatedState
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.pow
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
/**
|
||||
* Creates a [PullRefreshState] that is remembered across compositions.
|
||||
*
|
||||
* Changes to [refreshing] will result in [PullRefreshState] being updated.
|
||||
*
|
||||
* @sample androidx.compose.material.samples.PullRefreshSample
|
||||
*
|
||||
* @param refreshing A boolean representing whether a refresh is currently occurring.
|
||||
* @param onRefresh The function to be called to trigger a refresh.
|
||||
* @param refreshThreshold The threshold below which, if a release
|
||||
* occurs, [onRefresh] will be called.
|
||||
* @param refreshingOffset The offset at which the indicator will be drawn while refreshing. This
|
||||
* offset corresponds to the position of the bottom of the indicator.
|
||||
*/
|
||||
@Composable
|
||||
@ExperimentalMaterial3Api
|
||||
fun rememberPullRefreshState(
|
||||
refreshing: Boolean,
|
||||
onRefresh: () -> Unit,
|
||||
refreshThreshold: Dp = PullRefreshDefaults.RefreshThreshold,
|
||||
refreshingOffset: Dp = PullRefreshDefaults.RefreshingOffset,
|
||||
): PullRefreshState {
|
||||
require(refreshThreshold > 0.dp) { "The refresh trigger must be greater than zero!" }
|
||||
|
||||
val scope = rememberCoroutineScope()
|
||||
val onRefreshState = rememberUpdatedState(onRefresh)
|
||||
val thresholdPx: Float
|
||||
val refreshingOffsetPx: Float
|
||||
|
||||
with(LocalDensity.current) {
|
||||
thresholdPx = refreshThreshold.toPx()
|
||||
refreshingOffsetPx = refreshingOffset.toPx()
|
||||
}
|
||||
|
||||
// refreshThreshold and refreshingOffset should not be changed after instantiation, so any
|
||||
// changes to these values are ignored.
|
||||
val state = remember(scope) {
|
||||
PullRefreshState(scope, onRefreshState, refreshingOffsetPx, thresholdPx)
|
||||
}
|
||||
|
||||
SideEffect {
|
||||
state.setRefreshing(refreshing)
|
||||
}
|
||||
|
||||
return state
|
||||
}
|
||||
|
||||
/**
|
||||
* A state object that can be used in conjunction with [pullRefresh] to add pull-to-refresh
|
||||
* behaviour to a scroll component. Based on Android's SwipeRefreshLayout.
|
||||
*
|
||||
* Provides [progress], a float representing how far the user has pulled as a percentage of the
|
||||
* refreshThreshold. Values of one or less indicate that the user has not yet pulled past the
|
||||
* threshold. Values greater than one indicate how far past the threshold the user has pulled.
|
||||
*
|
||||
* Can be used in conjunction with [pullRefreshIndicatorTransform] to implement Android-like
|
||||
* pull-to-refresh behaviour with a custom indicator.
|
||||
*
|
||||
* Should be created using [rememberPullRefreshState].
|
||||
*/
|
||||
@ExperimentalMaterial3Api
|
||||
class PullRefreshState internal constructor(
|
||||
private val animationScope: CoroutineScope,
|
||||
private val onRefreshState: State<() -> Unit>,
|
||||
private val refreshingOffset: Float,
|
||||
internal val threshold: Float
|
||||
) {
|
||||
/**
|
||||
* A float representing how far the user has pulled as a percentage of the refreshThreshold.
|
||||
*
|
||||
* If the component has not been pulled at all, progress is zero. If the pull has reached
|
||||
* halfway to the threshold, progress is 0.5f. A value greater than 1 indicates that pull has
|
||||
* gone beyond the refreshThreshold - e.g. a value of 2f indicates that the user has pulled to
|
||||
* two times the refreshThreshold.
|
||||
*/
|
||||
val progress get() = adjustedDistancePulled / threshold
|
||||
|
||||
internal val refreshing get() = _refreshing
|
||||
internal val position get() = _position
|
||||
|
||||
private val adjustedDistancePulled by derivedStateOf { distancePulled * DragMultiplier }
|
||||
|
||||
private var _refreshing by mutableStateOf(false)
|
||||
private var _position by mutableStateOf(0f)
|
||||
private var distancePulled by mutableStateOf(0f)
|
||||
|
||||
internal fun onPull(pullDelta: Float): Float {
|
||||
if (this._refreshing) return 0f // Already refreshing, do nothing.
|
||||
|
||||
val newOffset = (distancePulled + pullDelta).coerceAtLeast(0f)
|
||||
val dragConsumed = newOffset - distancePulled
|
||||
distancePulled = newOffset
|
||||
_position = calculateIndicatorPosition()
|
||||
return dragConsumed
|
||||
}
|
||||
|
||||
internal fun onRelease() {
|
||||
if (!this._refreshing) {
|
||||
if (adjustedDistancePulled > threshold) {
|
||||
onRefreshState.value()
|
||||
} else {
|
||||
animateIndicatorTo(0f)
|
||||
}
|
||||
}
|
||||
distancePulled = 0f
|
||||
}
|
||||
|
||||
internal fun setRefreshing(refreshing: Boolean) {
|
||||
if (this._refreshing != refreshing) {
|
||||
this._refreshing = refreshing
|
||||
this.distancePulled = 0f
|
||||
animateIndicatorTo(if (refreshing) refreshingOffset else 0f)
|
||||
}
|
||||
}
|
||||
|
||||
private fun animateIndicatorTo(offset: Float) = animationScope.launch {
|
||||
animate(initialValue = _position, targetValue = offset) { value, _ ->
|
||||
_position = value
|
||||
}
|
||||
}
|
||||
|
||||
private fun calculateIndicatorPosition(): Float = when {
|
||||
// If drag hasn't gone past the threshold, the position is the adjustedDistancePulled.
|
||||
adjustedDistancePulled <= threshold -> adjustedDistancePulled
|
||||
else -> {
|
||||
// How far beyond the threshold pull has gone, as a percentage of the threshold.
|
||||
val overshootPercent = abs(progress) - 1.0f
|
||||
// Limit the overshoot to 200%. Linear between 0 and 200.
|
||||
val linearTension = overshootPercent.coerceIn(0f, 2f)
|
||||
// Non-linear tension. Increases with linearTension, but at a decreasing rate.
|
||||
val tensionPercent = linearTension - linearTension.pow(2) / 4
|
||||
// The additional offset beyond the threshold.
|
||||
val extraOffset = threshold * tensionPercent
|
||||
threshold + extraOffset
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Default parameter values for [rememberPullRefreshState].
|
||||
*/
|
||||
@ExperimentalMaterial3Api
|
||||
object PullRefreshDefaults {
|
||||
/**
|
||||
* If the indicator is below this threshold offset when it is released, a refresh
|
||||
* will be triggered.
|
||||
*/
|
||||
val RefreshThreshold = 80.dp
|
||||
|
||||
/**
|
||||
* The offset at which the indicator should be rendered whilst a refresh is occurring.
|
||||
*/
|
||||
val RefreshingOffset = 56.dp
|
||||
}
|
||||
|
||||
/**
|
||||
* The distance pulled is multiplied by this value to give us the adjusted distance pulled, which
|
||||
* is used in calculating the indicator position (when the adjusted distance pulled is less than
|
||||
* the refresh threshold, it is the indicator position, otherwise the indicator position is
|
||||
* derived from the progress).
|
||||
*/
|
||||
private const val DragMultiplier = 0.5f
|
||||
@@ -1,9 +1,12 @@
|
||||
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||
plugins {
|
||||
alias(libs.plugins.agp) apply false
|
||||
alias(libs.plugins.kotlin) apply false
|
||||
alias(libs.plugins.android.application) apply false
|
||||
alias(libs.plugins.android.library) apply false
|
||||
alias(libs.plugins.kotlin.android) apply false
|
||||
alias(libs.plugins.hilt) apply false
|
||||
alias(libs.plugins.serialization) apply false
|
||||
alias(libs.plugins.kotlin.serialization) apply false
|
||||
alias(libs.plugins.ksp) apply false
|
||||
alias(libs.plugins.google.services) apply false
|
||||
alias(libs.plugins.kotlin.jvm) apply false
|
||||
alias(libs.plugins.kotlin.compose) apply false
|
||||
}
|
||||
@@ -1,40 +1,43 @@
|
||||
[versions]
|
||||
agp = "8.1.1"
|
||||
kotlin = "1.9.10"
|
||||
core-ktx = "1.10.1"
|
||||
agp = "8.12.2"
|
||||
kotlin = "2.2.10"
|
||||
core-ktx = "1.17.0"
|
||||
junit = "4.13.2"
|
||||
androidx-test-ext-junit = "1.1.5"
|
||||
espresso-core = "3.5.1"
|
||||
lifecycle-runtime-ktx = "2.6.1"
|
||||
activity-compose = "1.7.2"
|
||||
compose-bom = "2023.08.00"
|
||||
compose-compiler = "1.5.3"
|
||||
hilt = "2.47"
|
||||
ksp = "1.9.10-1.0.13"
|
||||
google-services = "4.3.15"
|
||||
navigation-compose = "2.7.1"
|
||||
accompanist = "0.32.0"
|
||||
firebase-bom = "32.2.3"
|
||||
hilt-navigation-compose = "1.0.0"
|
||||
ktor = "2.3.3"
|
||||
coil-bom = "2.4.0"
|
||||
room = "2.5.2"
|
||||
webrtc = "1.0.32006"
|
||||
appcenter = "5.0.2"
|
||||
okhttp = "4.11.0"
|
||||
tabler-icons = "1.1.0"
|
||||
compose-constraintlayout = "1.0.1"
|
||||
minio = "8.5.4"
|
||||
androidx-test-ext-junit = "1.3.0"
|
||||
espresso-core = "3.7.0"
|
||||
lifecycle = "2.9.3"
|
||||
activity-compose = "1.10.1"
|
||||
compose-bom = "2025.08.01"
|
||||
hilt = "2.57.1"
|
||||
ksp = "2.2.10-2.0.2"
|
||||
google-services = "4.4.3"
|
||||
navigation = "2.9.3"
|
||||
accompanist = "0.37.3"
|
||||
firebase-bom = "34.2.0"
|
||||
hilt-navigation-compose = "1.2.0"
|
||||
ktor = "3.2.3"
|
||||
coil-bom = "3.3.0"
|
||||
room = "2.7.2"
|
||||
appcenter = "5.0.6"
|
||||
okhttp = "5.1.0"
|
||||
tabler-icons = "1.1.1"
|
||||
compose-constraintlayout = "1.1.1"
|
||||
minio = "8.5.17"
|
||||
kotlinx-serialization = "1.9.0"
|
||||
webrtc-android = "05eb774a24"
|
||||
datastore = "1.1.7"
|
||||
|
||||
[libraries]
|
||||
core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "core-ktx" }
|
||||
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "core-ktx" }
|
||||
junit = { group = "junit", name = "junit", version.ref = "junit" }
|
||||
androidx-test-ext-junit = { group = "androidx.test.ext", name = "junit", version.ref = "androidx-test-ext-junit" }
|
||||
espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espresso-core" }
|
||||
activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activity-compose" }
|
||||
# kotlinx-serialization
|
||||
kotlinx-serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "kotlinx-serialization" }
|
||||
# lifecycle
|
||||
lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycle-runtime-ktx" }
|
||||
lifecycle-runtime-compose = { group = "androidx.lifecycle", name = "lifecycle-runtime-compose", version.ref = "lifecycle-runtime-ktx" }
|
||||
lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycle" }
|
||||
lifecycle-runtime-compose = { group = "androidx.lifecycle", name = "lifecycle-runtime-compose", version.ref = "lifecycle" }
|
||||
# compose
|
||||
compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "compose-bom" }
|
||||
compose-animation = { group = "androidx.compose.animation", name = "animation" }
|
||||
@@ -53,13 +56,12 @@ compose-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4
|
||||
# constraintlayout
|
||||
compose-constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayout-compose", version.ref = "compose-constraintlayout" }
|
||||
# navigation-compose
|
||||
navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version.ref = "navigation-compose" }
|
||||
navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version.ref = "navigation" }
|
||||
hilt-navigation-compose = { group = "androidx.hilt", name = "hilt-navigation-compose", version.ref = "hilt-navigation-compose" }
|
||||
# accompanist
|
||||
accompanist-systemuicontroller = { group = "com.google.accompanist", name = "accompanist-systemuicontroller", version.ref = "accompanist" }
|
||||
accompanist-permissions = { group = "com.google.accompanist", name = "accompanist-permissions", version.ref = "accompanist" }
|
||||
# datastore
|
||||
datastore-preferences = { group = "androidx.datastore", name = "datastore-preferences", version = "1.0.0" }
|
||||
datastore-preferences = { group = "androidx.datastore", name = "datastore-preferences", version.ref = "datastore" }
|
||||
# hilt
|
||||
hilt-android = { group = "com.google.dagger", name = "hilt-android", version.ref = "hilt" }
|
||||
hilt-compiler = { group = "com.google.dagger", name = "hilt-compiler", version.ref = "hilt" }
|
||||
@@ -70,22 +72,25 @@ ktor-client-content-negotiation = { group = "io.ktor", name = "ktor-client-conte
|
||||
ktor-serialization-kotlinx-json = { group = "io.ktor", name = "ktor-serialization-kotlinx-json", version.ref = "ktor" }
|
||||
ktor-client-okhttp = { group = "io.ktor", name = "ktor-client-okhttp", version.ref = "ktor" }
|
||||
# coil
|
||||
coil-bom = { group = "io.coil-kt", name = "coil-bom", version.ref = "coil-bom" }
|
||||
coil-compose = { group = "io.coil-kt", name = "coil-compose" }
|
||||
coil-gif = { group = "io.coil-kt", name = "coil-gif" }
|
||||
coil-svg = { group = "io.coil-kt", name = "coil-svg" }
|
||||
coil-video = { group = "io.coil-kt", name = "coil-video" }
|
||||
coil-bom = { group = "io.coil-kt.coil3", name = "coil-bom", version.ref = "coil-bom" }
|
||||
coil-compose = { group = "io.coil-kt.coil3", name = "coil-compose" }
|
||||
coil-network-ktor3 = { group = "io.coil-kt.coil3", name = "coil-network-ktor3" }
|
||||
coil-network-cache-control = { group = "io.coil-kt.coil3", name = "coil-network-cache-control" }
|
||||
coil-gif = { group = "io.coil-kt.coil3", name = "coil-gif" }
|
||||
coil-svg = { group = "io.coil-kt.coil3", name = "coil-svg" }
|
||||
coil-video = { group = "io.coil-kt.coil3", name = "coil-video" }
|
||||
coil-test = { group = "io.coil-kt.coil3", name = "coil-test" }
|
||||
# room
|
||||
room-ktx = { group = "androidx.room", name = "room-ktx", version.ref = "room" }
|
||||
room-runtime = { group = "androidx.room", name = "room-runtime", version.ref = "room" }
|
||||
room-compiler = { group = "androidx.room", name = "room-compiler", version.ref = "room" }
|
||||
# webrtc
|
||||
google-webrtc = { group = "org.webrtc", name = "google-webrtc", version.ref = "webrtc" }
|
||||
webrtc-android = { module = "moe.tabidachi:webrtc-android", version.ref = "webrtc-android" }
|
||||
# firebase
|
||||
firebase-bom = { group = "com.google.firebase", name = "firebase-bom", version.ref = "firebase-bom" }
|
||||
firebase-analytics-ktx = { group = "com.google.firebase", name = "firebase-analytics-ktx" }
|
||||
firebase-messaging-ktx = { group = "com.google.firebase", name = "firebase-messaging-ktx" }
|
||||
firebase-inappmessaging-display-ktx = { group = "com.google.firebase", name = "firebase-inappmessaging-display-ktx" }
|
||||
firebase-analytics = { group = "com.google.firebase", name = "firebase-analytics" }
|
||||
firebase-messaging = { group = "com.google.firebase", name = "firebase-messaging" }
|
||||
firebase-inappmessaging-display = { group = "com.google.firebase", name = "firebase-inappmessaging-display" }
|
||||
# appcenter
|
||||
appcenter-analytics = { group = "com.microsoft.appcenter", name = "appcenter-analytics", version.ref = "appcenter" }
|
||||
appcenter-crashes = { group = "com.microsoft.appcenter", name = "appcenter-crashes", version.ref = "appcenter" }
|
||||
@@ -112,7 +117,6 @@ navigation-compose = [
|
||||
"hilt-navigation-compose",
|
||||
]
|
||||
accompanist = [
|
||||
"accompanist-systemuicontroller",
|
||||
"accompanist-permissions",
|
||||
]
|
||||
hilt = [
|
||||
@@ -128,6 +132,8 @@ ktor = [
|
||||
]
|
||||
coil = [
|
||||
"coil-compose",
|
||||
"coil-network-ktor3",
|
||||
"coil-network-cache-control",
|
||||
"coil-gif",
|
||||
"coil-svg",
|
||||
"coil-video",
|
||||
@@ -137,9 +143,9 @@ room = [
|
||||
"room-runtime",
|
||||
]
|
||||
firebase = [
|
||||
"firebase-analytics-ktx",
|
||||
"firebase-messaging-ktx",
|
||||
"firebase-inappmessaging-display-ktx",
|
||||
"firebase-analytics",
|
||||
"firebase-messaging",
|
||||
"firebase-inappmessaging-display",
|
||||
]
|
||||
appcenter = [
|
||||
"appcenter-analytics",
|
||||
@@ -148,9 +154,12 @@ appcenter = [
|
||||
]
|
||||
|
||||
[plugins]
|
||||
agp = { id = "com.android.application", version.ref = "agp" }
|
||||
kotlin = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
|
||||
android-application = { id = "com.android.application", version.ref = "agp" }
|
||||
android-library = { id = "com.android.library", version.ref = "agp" }
|
||||
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
|
||||
hilt = { id = "com.google.dagger.hilt.android", version.ref = "hilt" }
|
||||
serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
|
||||
kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
|
||||
ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
|
||||
google-services = { id = "com.google.gms.google-services", version.ref = "google-services" }
|
||||
kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
|
||||
kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
|
||||
|
||||
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Binary file not shown.
5
gradle/wrapper/gradle-wrapper.properties
vendored
5
gradle/wrapper/gradle-wrapper.properties
vendored
@@ -1,6 +1,7 @@
|
||||
#Sun Apr 30 16:57:46 CST 2023
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-9.0.0-bin.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
|
||||
296
gradlew
vendored
296
gradlew
vendored
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env sh
|
||||
#!/bin/sh
|
||||
|
||||
#
|
||||
# Copyright 2015 the original author or authors.
|
||||
# Copyright © 2015-2021 the original authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
@@ -15,69 +15,103 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
#
|
||||
|
||||
##############################################################################
|
||||
##
|
||||
## Gradle start up script for UN*X
|
||||
##
|
||||
#
|
||||
# Gradle start up script for POSIX generated by Gradle.
|
||||
#
|
||||
# Important for running:
|
||||
#
|
||||
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
|
||||
# noncompliant, but you have some other compliant shell such as ksh or
|
||||
# bash, then to run this script, type that shell name before the whole
|
||||
# command line, like:
|
||||
#
|
||||
# ksh Gradle
|
||||
#
|
||||
# Busybox and similar reduced shells will NOT work, because this script
|
||||
# requires all of these POSIX shell features:
|
||||
# * functions;
|
||||
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
|
||||
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
|
||||
# * compound commands having a testable exit status, especially «case»;
|
||||
# * various built-in commands including «command», «set», and «ulimit».
|
||||
#
|
||||
# Important for patching:
|
||||
#
|
||||
# (2) This script targets any POSIX shell, so it avoids extensions provided
|
||||
# by Bash, Ksh, etc; in particular arrays are avoided.
|
||||
#
|
||||
# The "traditional" practice of packing multiple parameters into a
|
||||
# space-separated string is a well documented source of bugs and security
|
||||
# problems, so this is (mostly) avoided, by progressively accumulating
|
||||
# options in "$@", and eventually passing that to Java.
|
||||
#
|
||||
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
|
||||
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
|
||||
# see the in-line comments for details.
|
||||
#
|
||||
# There are tweaks for specific operating systems such as AIX, CygWin,
|
||||
# Darwin, MinGW, and NonStop.
|
||||
#
|
||||
# (3) This script is generated from the Groovy template
|
||||
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||
# within the Gradle project.
|
||||
#
|
||||
# You can find Gradle at https://github.com/gradle/gradle/.
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
# Attempt to set APP_HOME
|
||||
|
||||
# Resolve links: $0 may be a link
|
||||
PRG="$0"
|
||||
# Need this for relative symlinks.
|
||||
while [ -h "$PRG" ] ; do
|
||||
ls=`ls -ld "$PRG"`
|
||||
link=`expr "$ls" : '.*-> \(.*\)$'`
|
||||
if expr "$link" : '/.*' > /dev/null; then
|
||||
PRG="$link"
|
||||
else
|
||||
PRG=`dirname "$PRG"`"/$link"
|
||||
fi
|
||||
app_path=$0
|
||||
|
||||
# Need this for daisy-chained symlinks.
|
||||
while
|
||||
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
|
||||
[ -h "$app_path" ]
|
||||
do
|
||||
ls=$( ls -ld "$app_path" )
|
||||
link=${ls#*' -> '}
|
||||
case $link in #(
|
||||
/*) app_path=$link ;; #(
|
||||
*) app_path=$APP_HOME$link ;;
|
||||
esac
|
||||
done
|
||||
SAVED="`pwd`"
|
||||
cd "`dirname \"$PRG\"`/" >/dev/null
|
||||
APP_HOME="`pwd -P`"
|
||||
cd "$SAVED" >/dev/null
|
||||
|
||||
APP_NAME="Gradle"
|
||||
APP_BASE_NAME=`basename "$0"`
|
||||
|
||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||
# This is normally unused
|
||||
# shellcheck disable=SC2034
|
||||
APP_BASE_NAME=${0##*/}
|
||||
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
|
||||
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
|
||||
|
||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||
MAX_FD="maximum"
|
||||
MAX_FD=maximum
|
||||
|
||||
warn () {
|
||||
echo "$*"
|
||||
}
|
||||
} >&2
|
||||
|
||||
die () {
|
||||
echo
|
||||
echo "$*"
|
||||
echo
|
||||
exit 1
|
||||
}
|
||||
} >&2
|
||||
|
||||
# OS specific support (must be 'true' or 'false').
|
||||
cygwin=false
|
||||
msys=false
|
||||
darwin=false
|
||||
nonstop=false
|
||||
case "`uname`" in
|
||||
CYGWIN* )
|
||||
cygwin=true
|
||||
;;
|
||||
Darwin* )
|
||||
darwin=true
|
||||
;;
|
||||
MINGW* )
|
||||
msys=true
|
||||
;;
|
||||
NONSTOP* )
|
||||
nonstop=true
|
||||
;;
|
||||
case "$( uname )" in #(
|
||||
CYGWIN* ) cygwin=true ;; #(
|
||||
Darwin* ) darwin=true ;; #(
|
||||
MSYS* | MINGW* ) msys=true ;; #(
|
||||
NONSTOP* ) nonstop=true ;;
|
||||
esac
|
||||
|
||||
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||
@@ -87,9 +121,9 @@ CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||
if [ -n "$JAVA_HOME" ] ; then
|
||||
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||
# IBM's JDK on AIX uses strange locations for the executables
|
||||
JAVACMD="$JAVA_HOME/jre/sh/java"
|
||||
JAVACMD=$JAVA_HOME/jre/sh/java
|
||||
else
|
||||
JAVACMD="$JAVA_HOME/bin/java"
|
||||
JAVACMD=$JAVA_HOME/bin/java
|
||||
fi
|
||||
if [ ! -x "$JAVACMD" ] ; then
|
||||
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
||||
@@ -98,88 +132,120 @@ Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
else
|
||||
JAVACMD="java"
|
||||
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
JAVACMD=java
|
||||
if ! command -v java >/dev/null 2>&1
|
||||
then
|
||||
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
fi
|
||||
|
||||
# Increase the maximum file descriptors if we can.
|
||||
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
|
||||
MAX_FD_LIMIT=`ulimit -H -n`
|
||||
if [ $? -eq 0 ] ; then
|
||||
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
|
||||
MAX_FD="$MAX_FD_LIMIT"
|
||||
fi
|
||||
ulimit -n $MAX_FD
|
||||
if [ $? -ne 0 ] ; then
|
||||
warn "Could not set maximum file descriptor limit: $MAX_FD"
|
||||
fi
|
||||
else
|
||||
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
|
||||
fi
|
||||
fi
|
||||
|
||||
# For Darwin, add options to specify how the application appears in the dock
|
||||
if $darwin; then
|
||||
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
|
||||
fi
|
||||
|
||||
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
|
||||
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
|
||||
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
|
||||
|
||||
JAVACMD=`cygpath --unix "$JAVACMD"`
|
||||
|
||||
# We build the pattern for arguments to be converted via cygpath
|
||||
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
|
||||
SEP=""
|
||||
for dir in $ROOTDIRSRAW ; do
|
||||
ROOTDIRS="$ROOTDIRS$SEP$dir"
|
||||
SEP="|"
|
||||
done
|
||||
OURCYGPATTERN="(^($ROOTDIRS))"
|
||||
# Add a user-defined pattern to the cygpath arguments
|
||||
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
|
||||
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
|
||||
fi
|
||||
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||
i=0
|
||||
for arg in "$@" ; do
|
||||
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
|
||||
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
|
||||
|
||||
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
|
||||
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
|
||||
else
|
||||
eval `echo args$i`="\"$arg\""
|
||||
fi
|
||||
i=`expr $i + 1`
|
||||
done
|
||||
case $i in
|
||||
0) set -- ;;
|
||||
1) set -- "$args0" ;;
|
||||
2) set -- "$args0" "$args1" ;;
|
||||
3) set -- "$args0" "$args1" "$args2" ;;
|
||||
4) set -- "$args0" "$args1" "$args2" "$args3" ;;
|
||||
5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
|
||||
6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
|
||||
7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
|
||||
8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
|
||||
9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
|
||||
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
||||
case $MAX_FD in #(
|
||||
max*)
|
||||
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
|
||||
# shellcheck disable=SC2039,SC3045
|
||||
MAX_FD=$( ulimit -H -n ) ||
|
||||
warn "Could not query maximum file descriptor limit"
|
||||
esac
|
||||
case $MAX_FD in #(
|
||||
'' | soft) :;; #(
|
||||
*)
|
||||
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
|
||||
# shellcheck disable=SC2039,SC3045
|
||||
ulimit -n "$MAX_FD" ||
|
||||
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
||||
esac
|
||||
fi
|
||||
|
||||
# Escape application args
|
||||
save () {
|
||||
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
|
||||
echo " "
|
||||
}
|
||||
APP_ARGS=`save "$@"`
|
||||
# Collect all arguments for the java command, stacking in reverse order:
|
||||
# * args from the command line
|
||||
# * the main class name
|
||||
# * -classpath
|
||||
# * -D...appname settings
|
||||
# * --module-path (only if needed)
|
||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
|
||||
|
||||
# Collect all arguments for the java command, following the shell quoting and substitution rules
|
||||
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
|
||||
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||
if "$cygwin" || "$msys" ; then
|
||||
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
|
||||
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
|
||||
|
||||
JAVACMD=$( cygpath --unix "$JAVACMD" )
|
||||
|
||||
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||
for arg do
|
||||
if
|
||||
case $arg in #(
|
||||
-*) false ;; # don't mess with options #(
|
||||
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
|
||||
[ -e "$t" ] ;; #(
|
||||
*) false ;;
|
||||
esac
|
||||
then
|
||||
arg=$( cygpath --path --ignore --mixed "$arg" )
|
||||
fi
|
||||
# Roll the args list around exactly as many times as the number of
|
||||
# args, so each arg winds up back in the position where it started, but
|
||||
# possibly modified.
|
||||
#
|
||||
# NB: a `for` loop captures its iteration list before it begins, so
|
||||
# changing the positional parameters here affects neither the number of
|
||||
# iterations, nor the values presented in `arg`.
|
||||
shift # remove old arg
|
||||
set -- "$@" "$arg" # push replacement arg
|
||||
done
|
||||
fi
|
||||
|
||||
|
||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||
|
||||
# Collect all arguments for the java command:
|
||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
|
||||
# and any embedded shellness will be escaped.
|
||||
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
|
||||
# treated as '${Hostname}' itself on the command line.
|
||||
|
||||
set -- \
|
||||
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
||||
-classpath "$CLASSPATH" \
|
||||
org.gradle.wrapper.GradleWrapperMain \
|
||||
"$@"
|
||||
|
||||
# Stop when "xargs" is not available.
|
||||
if ! command -v xargs >/dev/null 2>&1
|
||||
then
|
||||
die "xargs is not available"
|
||||
fi
|
||||
|
||||
# Use "xargs" to parse quoted args.
|
||||
#
|
||||
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
|
||||
#
|
||||
# In Bash we could simply go:
|
||||
#
|
||||
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
|
||||
# set -- "${ARGS[@]}" "$@"
|
||||
#
|
||||
# but POSIX shell has neither arrays nor command substitution, so instead we
|
||||
# post-process each arg (as a line of input to sed) to backslash-escape any
|
||||
# character that might be a shell metacharacter, then use eval to reverse
|
||||
# that process (while maintaining the separation between arguments), and wrap
|
||||
# the whole thing up as a single "set" statement.
|
||||
#
|
||||
# This will of course break if any of these variables contains a newline or
|
||||
# an unmatched quote.
|
||||
#
|
||||
|
||||
eval "set -- $(
|
||||
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
|
||||
xargs -n1 |
|
||||
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
|
||||
tr '\n' ' '
|
||||
)" '"$@"'
|
||||
|
||||
exec "$JAVACMD" "$@"
|
||||
|
||||
37
gradlew.bat
vendored
37
gradlew.bat
vendored
@@ -13,8 +13,10 @@
|
||||
@rem See the License for the specific language governing permissions and
|
||||
@rem limitations under the License.
|
||||
@rem
|
||||
@rem SPDX-License-Identifier: Apache-2.0
|
||||
@rem
|
||||
|
||||
@if "%DEBUG%" == "" @echo off
|
||||
@if "%DEBUG%"=="" @echo off
|
||||
@rem ##########################################################################
|
||||
@rem
|
||||
@rem Gradle startup script for Windows
|
||||
@@ -25,7 +27,8 @@
|
||||
if "%OS%"=="Windows_NT" setlocal
|
||||
|
||||
set DIRNAME=%~dp0
|
||||
if "%DIRNAME%" == "" set DIRNAME=.
|
||||
if "%DIRNAME%"=="" set DIRNAME=.
|
||||
@rem This is normally unused
|
||||
set APP_BASE_NAME=%~n0
|
||||
set APP_HOME=%DIRNAME%
|
||||
|
||||
@@ -40,13 +43,13 @@ if defined JAVA_HOME goto findJavaFromJavaHome
|
||||
|
||||
set JAVA_EXE=java.exe
|
||||
%JAVA_EXE% -version >NUL 2>&1
|
||||
if "%ERRORLEVEL%" == "0" goto execute
|
||||
if %ERRORLEVEL% equ 0 goto execute
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
echo. 1>&2
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
|
||||
echo. 1>&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||
echo location of your Java installation. 1>&2
|
||||
|
||||
goto fail
|
||||
|
||||
@@ -56,11 +59,11 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||
|
||||
if exist "%JAVA_EXE%" goto execute
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
echo. 1>&2
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
|
||||
echo. 1>&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||
echo location of your Java installation. 1>&2
|
||||
|
||||
goto fail
|
||||
|
||||
@@ -75,13 +78,15 @@ set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||
|
||||
:end
|
||||
@rem End local scope for the variables with windows NT shell
|
||||
if "%ERRORLEVEL%"=="0" goto mainEnd
|
||||
if %ERRORLEVEL% equ 0 goto mainEnd
|
||||
|
||||
:fail
|
||||
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||
rem the _cmd.exe /c_ return code!
|
||||
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
|
||||
exit /b 1
|
||||
set EXIT_CODE=%ERRORLEVEL%
|
||||
if %EXIT_CODE% equ 0 set EXIT_CODE=1
|
||||
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
|
||||
exit /b %EXIT_CODE%
|
||||
|
||||
:mainEnd
|
||||
if "%OS%"=="Windows_NT" endlocal
|
||||
|
||||
Reference in New Issue
Block a user