chore: 依赖库版本升级

This commit is contained in:
2025-08-31 01:36:08 +08:00
parent e40b5b4239
commit f780f2522f
64 changed files with 1051 additions and 1133 deletions

View File

@@ -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
}

View File

@@ -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

View File

@@ -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())
}
}

View File

@@ -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()
}
}

View File

@@ -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

View File

@@ -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)

View File

@@ -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)

View File

@@ -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(

View File

@@ -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)

View File

@@ -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)

View File

@@ -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
}
}

View File

@@ -129,7 +129,7 @@ class DownloadMessageItem(
downloading = false
}
) { sent, length ->
progress = sent.toFloat() / length.toFloat()
length?.let { progress = sent.toFloat() / it.toFloat() }
}
}

View File

@@ -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)
}
}

View File

@@ -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}")
}
}

View File

@@ -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)
}) {

View File

@@ -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
) {

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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)

View File

@@ -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,
)
}
}
}
}
}
}

View File

@@ -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 {

View File

@@ -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

View File

@@ -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)

View File

@@ -52,7 +52,7 @@ fun MessageTextField(
),
cursorBrush = SolidColor(MaterialTheme.colorScheme.primary),
decorationBox = {
TextFieldDefaults.TextFieldDecorationBox(
TextFieldDefaults.DecorationBox(
value = text,
innerTextField = it,
enabled = true,

View File

@@ -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
) {

View File

@@ -36,7 +36,7 @@ fun SearchTextField(
singleLine = true,
cursorBrush = SolidColor(MaterialTheme.colorScheme.primary),
decorationBox = {
TextFieldDefaults.TextFieldDecorationBox(
TextFieldDefaults.DecorationBox(
value = value,
innerTextField = it,
enabled = true,

View File

@@ -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,

View File

@@ -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

View File

@@ -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)

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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]) {

View File

@@ -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 = {}
)

View File

@@ -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
)
}
}

View File

@@ -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))
}
}

View 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()
)
}

View File

@@ -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)
}
}
}

View File

@@ -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(

View File

@@ -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)
)
)*/
}
}
}

View File

@@ -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
)
}
}
}
}

View File

@@ -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

View File

@@ -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

View File

@@ -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)
}
}

View File

@@ -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)

View File

@@ -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,

View File

@@ -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
}
}

View File

@@ -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

View File

@@ -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
}
}
}

View File

@@ -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

View File

@@ -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
}

View File

@@ -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" }

Binary file not shown.

View File

@@ -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
View File

@@ -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
View File

@@ -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