Compare commits

3 Commits

Author SHA1 Message Date
Sweetbread d7d14389fc Display and send messages 2026-04-10 14:26:33 +03:00
Sweetbread 902af5e7b5 wip: show messages 2026-03-26 23:57:28 +03:00
Sweetbread 64de39f0ca update 2026-03-20 01:22:30 +03:00
7 changed files with 353 additions and 363 deletions
+10 -6
View File
@@ -1,3 +1,5 @@
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
/*
* Created by sweetbread
* Copyright (c) 2026. All rights reserved.
@@ -5,14 +7,13 @@
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
alias(libs.plugins.kotlin.compose)
// alias(libs.plugins.ksp)
}
android {
namespace = "ru.risdeveau.pixeldragon"
compileSdk = 35
compileSdk = 36
defaultConfig {
applicationId = "ru.risdeveau.pixeldragon"
@@ -37,14 +38,17 @@ android {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = "17"
}
buildFeatures {
compose = true
}
}
kotlin {
compilerOptions {
jvmTarget.set(JvmTarget.JVM_17)
}
}
dependencies {
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.lifecycle.runtime.ktx)
@@ -94,5 +98,5 @@ dependencies {
// Others
implementation(libs.splitties.base) // Syntax sugar
implementation(libs.jsoup) // HTML parser
implementation(libs.iconsax.compose) // Material icons
}
@@ -40,6 +40,7 @@ import de.connect2x.trixnity.client.cryptodriver.vodozemac.vodozemac
import de.connect2x.trixnity.clientserverapi.client.SyncState
import ru.risdeveau.pixeldragon.R
import ru.risdeveau.pixeldragon.client
import ru.risdeveau.pixeldragon.ui.layout.Room
import ru.risdeveau.pixeldragon.ui.layout.RoomList
import ru.risdeveau.pixeldragon.ui.theme.PixelDragonTheme
import ru.risdeveau.pixeldragon.util.getMediaStore
@@ -94,9 +95,9 @@ class MainActivity : ComponentActivity() {
"room/{rid}",
arguments = listOf(navArgument("rid") { type = NavType.StringType })
) { navBackStackEntry ->
// Room(Modifier
// .padding(innerPadding)
// .fillMaxSize(), navBackStackEntry.arguments!!.getString("rid")!!)
Room(Modifier
.padding(innerPadding)
.fillMaxSize(), navBackStackEntry.arguments!!.getString("rid")!!)
}
composable(
"space/{rid}",
@@ -8,8 +8,7 @@ package ru.risdeveau.pixeldragon.ui.item
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Warning
import io.github.rabehx.iconsax.Iconsax
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Icon
import androidx.compose.runtime.Composable
@@ -28,6 +27,7 @@ import coil3.request.ImageRequest
import kotlinx.coroutines.flow.MutableStateFlow
import de.connect2x.trixnity.client.media
import de.connect2x.trixnity.clientserverapi.model.media.FileTransferProgress
import io.github.rabehx.iconsax.outline.Warning2
import ru.risdeveau.pixeldragon.client
enum class ImageLoadState {
@@ -119,7 +119,7 @@ fun MXCImage(
}
imageLoadState == ImageLoadState.Error -> {
Icon(
Icons.Outlined.Warning,
Iconsax.Outline.Warning2,
contentDescription = "Error",
modifier = Modifier.align(Alignment.Center)
)
@@ -1,342 +1,322 @@
///*
// * Created by sweetbread
// * Copyright (c) 2025. All rights reserved.
// */
//
//package ru.risdeveau.pixeldragon.ui.layout
//
//import android.content.Context
//import android.content.Intent
//import android.net.Uri
//import android.webkit.WebResourceRequest
//import android.webkit.WebView
//import android.webkit.WebViewClient
//import androidx.compose.foundation.background
//import androidx.compose.foundation.layout.Box
//import androidx.compose.foundation.layout.Column
//import androidx.compose.foundation.layout.fillMaxHeight
//import androidx.compose.foundation.layout.fillMaxSize
//import androidx.compose.foundation.layout.fillMaxWidth
//import androidx.compose.foundation.layout.padding
//import androidx.compose.foundation.lazy.LazyColumn
//import androidx.compose.foundation.lazy.items
//import androidx.compose.foundation.lazy.rememberLazyListState
//import androidx.compose.foundation.shape.RoundedCornerShape
//import androidx.compose.material3.ExperimentalMaterial3Api
//import androidx.compose.material3.MaterialTheme
//import androidx.compose.material3.Text
//import androidx.compose.runtime.Composable
//import androidx.compose.runtime.DisposableEffect
//import androidx.compose.runtime.LaunchedEffect
//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.draw.clip
//import androidx.compose.ui.graphics.Color
//import androidx.compose.ui.graphics.toArgb
//import androidx.compose.ui.platform.LocalContext
//import androidx.compose.ui.text.font.FontWeight
//import androidx.compose.ui.unit.dp
//import androidx.compose.ui.viewinterop.AndroidView
//import kotlinx.coroutines.Dispatchers
//import kotlinx.coroutines.withContext
//import org.jsoup.Jsoup
//import org.jsoup.safety.Safelist
//import ru.risdeveau.pixeldragon.api.Event
//import ru.risdeveau.pixeldragon.api.getAccountData
//import ru.risdeveau.pixeldragon.api.getEventsAround
//import ru.risdeveau.pixeldragon.ui.activity.ME
//import ru.risdeveau.pixeldragon.ui.item.MXCImage
//
//@Composable
//fun Room(modifier: Modifier = Modifier, rid: String) {
// var eventsId by remember { mutableStateOf(listOf<Event>()) }
// val listState = rememberLazyListState()
//
// LaunchedEffect(Unit) {
// withContext(Dispatchers.IO) {
// val readMark = getAccountData(ME!!.userId, rid, "m.fully_read")
// val eventsAround = getEventsAround(rid, readMark!!.getString("event_id")) //FIXME: Null check
// eventsId = eventsAround.let {
// it.before + listOf(it.base) + it.after
// }
// }
// }
//
// LazyColumn(modifier = modifier, state = listState, reverseLayout = true) {
// items(eventsId.reversed()) { event ->
// EventItem(event)
// }
//
// item {
// if (eventsId.isEmpty()) {
// Text("Empty room")
// }
// }
// }
//}
//
//@OptIn(ExperimentalMaterial3Api::class)
//@Composable
//fun EventItem(event: Event) {
// Box (Modifier.fillMaxWidth()) {
// when (event.type) {
// "m.room.message" -> Column(
// Modifier
// .fillMaxSize()
// .then(
// if (event.sender != ME!!.userId)
// Modifier.padding(end = 16.dp)
// else
// Modifier.padding(start = 16.dp)
// )
// .padding(4.dp)
// .background(
// if (event.sender != ME?.userId)
// MaterialTheme.colorScheme.surfaceContainer
// else
// MaterialTheme.colorScheme.primaryContainer
// )
// .clip(RoundedCornerShape(16.dp))
// .padding(4.dp)
// ) {
// Text(event.sender, maxLines = 1, fontWeight = FontWeight.Bold)
//
// when (val msgtype = event.content.optString("msgtype", null)) {
// "m.text" -> when (event.content.optString("format")) {
// "org.matrix.custom.html" -> {
// if (event.content.getString("body") == event.content.getString("formatted_body"))
// Text(event.content.getString("body"))
// HtmlRenderer(event.content.getString("formatted_body"))
// }
//
// else -> Text(event.content.getString("body"))
// }
//
// "m.image" ->
// MXCImage(event.content.getString("url"), Modifier.fillMaxWidth(.9f))
//
// null -> Text(event.content.toString(2))
//
// else -> Text("Unknown type: $msgtype", color = MaterialTheme.colorScheme.error)
// }
//
// }
//
// else -> Text(event.type,
// Modifier
// .fillMaxHeight()
// .padding(4.dp)
// .background(MaterialTheme.colorScheme.errorContainer)
// .padding(4.dp)
// )
// }
// }
//}
//
//private fun String.sanitizeHTML(): String {
// val matrixSafelist = Safelist()
// .addTags(
// "h1", "h2", "h3", "h4", "h5", "h6",
// "b", "i", "u", "strong", "s", "del",
// "sup", "sub", "code",
// "table", "thead", "tbody",
// "tr", "th", "td", "ul", "ol", "li",
// "blockquote", "details", "summary",
// "em", "code", "div", "pre", "span", "img"
// )
//// .addAttributes("span",
//// "data-mx-bg-color", "data-mx-color",
//// "data-mx-spoiler", "data-mx-maths"
//// )
// .addAttributes("a",
// "target", "href"
// )
// .addAttributes("img",
// "width", "height", "alt", "title", "src"
// )
// .addAttributes("ol", "start")
// .addAttributes("code", "class")
// .addAttributes("div", "data-mx-maths")
//
// val doc = Jsoup.parse(this)
// doc.select("mx-reply").remove()
//
// val out = Jsoup.clean(doc.body().toString(), matrixSafelist)
// return out
//}
//
//
//@Composable
//fun HtmlRenderer(
// htmlContent: String,
// modifier: Modifier = Modifier
//) {
// val context = LocalContext.current
// val webView = remember { WebView(context).apply {
// settings.apply {
// javaScriptEnabled = false
// loadWithOverviewMode = true
// useWideViewPort = true
// }
// isVerticalScrollBarEnabled = false
// setBackgroundColor(Color.Transparent.toArgb())
// } }
//
// val css = """
// body {
// font-family: -apple-system, sans-serif;
// font-size: 16px;
// line-height: 1.6;
// color: ${colorToCss(MaterialTheme.colorScheme.onBackground)};
// margin: 0;
// padding: 0;
// }
// h1, h2, h3, h4, h5, h6 {
// font-weight: bold;
// padding: 0;
// }
// h1 { font-size: 24px; }
// h2 { font-size: 22px; }
// h3 { font-size: 20px; }
// h4 { font-size: 18px; }
// h5 { font-size: 16px; }
// h6 { font-size: 14px; }
// a {
// color: ${colorToCss(MaterialTheme.colorScheme.primary)};
// text-decoration: none;
// }
// img {
// max-width: 100%;
// height: auto;
// display: block;
// margin: 12px 0;
// }
// table {
// width: 100%;
// border-collapse: collapse;
// margin: 16px 0;
// }
// th, td {
// border: 1px solid ${colorToCss(MaterialTheme.colorScheme.outline)};
// padding: 12px;
// text-align: left;
// }
// th {
// background-color: ${colorToCss(MaterialTheme.colorScheme.surfaceVariant)};
// font-weight: bold;
// }
// blockquote {
// border-left: 4px solid ${colorToCss(MaterialTheme.colorScheme.primary)};
// padding-left: 16px;
// margin-left: 0;
// color: ${colorToCss(MaterialTheme.colorScheme.onSurfaceVariant)};
// }
// pre {
// background-color: ${colorToCss(MaterialTheme.colorScheme.surfaceVariant)};
// padding: 16px;
// overflow: auto;
// border-radius: 4px;
// }
// code {
// font-family: monospace;
// background-color: ${colorToCss(MaterialTheme.colorScheme.surfaceVariant)};
// padding: 2px 4px;
// border-radius: 4px;
// }
// hr {
// border: 0;
// height: 1px;
// background-color: ${colorToCss(MaterialTheme.colorScheme.outline)};
// margin: 24px 0;
// }
// ul, ol {
// padding-left: 24px;
// margin: 12px 0;
// }
// li {
// margin-bottom: 8px;
// }
// details {
// margin: 12px 0;
// }
// summary {
// font-weight: bold;
// cursor: pointer;
// }
// @media (prefers-color-scheme: dark) {
// :root {
// --border-color: ${colorToCss(MaterialTheme.colorScheme.outline)};
// }
// }
// """.trimIndent()
//
// LaunchedEffect(htmlContent) {
// webView.loadDataWithBaseURL(
// null,
// wrapHtml(htmlContent, css),
// "text/html",
// "UTF-8",
// null
// )
// }
//
// DisposableEffect(webView) {
// onDispose {
// webView.destroy()
// }
// }
//
// AndroidView(
// factory = { webView },
// modifier = modifier,
// update = { view ->
// view.webViewClient = SafeWebViewClient(context)
// }
// )
//}
//
//private class SafeWebViewClient(
// private val context: Context
//) : WebViewClient() {
// override fun shouldOverrideUrlLoading(
// view: WebView,
// request: WebResourceRequest
// ): Boolean {
// val url = request.url.toString()
// try {
// // Открываем ссылки во внешнем браузере
// context.startActivity(
// Intent(Intent.ACTION_VIEW, Uri.parse(url))
// )
// return true
// } catch (e: Exception) {
// // Обработка ошибок открытия ссылки
// return false
// }
// }
//}
//
//private fun wrapHtml(content: String, css: String): String {
// return """
// <!DOCTYPE html>
// <html>
// <head>
// <meta name="viewport" content="width=device-width, initial-scale=1">
// <style>
// $css
// </style>
// </head>
// <body>
// ${content.sanitizeHTML()}
// </body>
// </html>
// """.trimIndent()
//}
//
//private fun colorToCss(color: Color): String {
// val argb = color.toArgb()
// return String.format("#%06X", 0xFFFFFF and argb)
//}
/*
* Created by sweetbread
* Copyright (c) 2025. All rights reserved.
*/
package ru.risdeveau.pixeldragon.ui.layout
import android.annotation.SuppressLint
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.view.MotionEvent
import android.view.ViewGroup
import android.webkit.WebResourceRequest
import android.webkit.WebSettings
import android.webkit.WebView
import android.webkit.WebViewClient
import androidx.compose.animation.animateContentSize
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView
import de.connect2x.trixnity.client.room
import de.connect2x.trixnity.client.room.message.MessageBuilder
import de.connect2x.trixnity.client.room.message.text
import de.connect2x.trixnity.client.room.toFlowList
import de.connect2x.trixnity.client.store.TimelineEvent
import de.connect2x.trixnity.client.store.eventId
import de.connect2x.trixnity.client.store.unsigned
import de.connect2x.trixnity.core.model.RoomId
import de.connect2x.trixnity.core.model.events.ClientEvent
import de.connect2x.trixnity.core.model.events.m.room.AvatarEventContent
import de.connect2x.trixnity.core.model.events.m.room.CanonicalAliasEventContent
import de.connect2x.trixnity.core.model.events.m.room.HistoryVisibilityEventContent
import de.connect2x.trixnity.core.model.events.m.room.MemberEventContent
import de.connect2x.trixnity.core.model.events.m.room.NameEventContent
import de.connect2x.trixnity.core.model.events.m.room.PowerLevelsEventContent
import de.connect2x.trixnity.core.model.events.m.room.RoomMessageEventContent
import de.connect2x.trixnity.core.model.events.m.room.TopicEventContent
import io.github.rabehx.iconsax.Iconsax
import io.github.rabehx.iconsax.filled.ArrowRight
import io.github.rabehx.iconsax.filled.Send
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.launch
import org.jsoup.Jsoup
import org.jsoup.safety.Safelist
import ru.risdeveau.pixeldragon.client
@OptIn(ExperimentalCoroutinesApi::class)
@Composable
fun Room(modifier: Modifier = Modifier, rid: String) {
val roomId = remember(rid) { RoomId(rid) }
val limit = remember { MutableStateFlow(50) }
val eventsFlow = remember(roomId) {
client!!.room
.getLastTimelineEvents(roomId)
.toFlowList(limit)
.flatMapLatest { flows ->
if (flows.isEmpty()) flowOf(emptyList())
else combine(flows) { it.toList() }
}
}
val events by eventsFlow.collectAsState(initial = emptyList())
val listState = rememberLazyListState()
Column(modifier) {
var message by remember { mutableStateOf("") }
LazyColumn(Modifier.weight(1f), state = listState, reverseLayout = true) {
items(events, key = { it.eventId.full }) { event ->
EventItem(event)
}
if (events.isEmpty()) {
item {
Text("Empty room", modifier = Modifier.padding(16.dp))
}
}
}
Row (Modifier.fillMaxWidth()) {
OutlinedTextField(
modifier = Modifier
.padding(4.dp)
.weight(1f),
value = message,
onValueChange = { message = it.trim() },
)
IconButton(
enabled = message.isNotBlank(),
content = { Icon(Iconsax.Filled.ArrowRight, contentDescription = "Send") },
onClick = {
CoroutineScope(Dispatchers.IO).launch {
client!!.room.sendMessage(RoomId(rid)) {
text(message)
}
}
message = ""
}
)
}
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun EventItem(event: TimelineEvent) {
val content = event.content?.getOrNull()
Box(Modifier
.fillMaxWidth()
.heightIn(min = 24.dp)) {
when {
content == null -> Text("Not decrypted",
Modifier
.fillMaxHeight()
.padding(4.dp)
.background(MaterialTheme.colorScheme.errorContainer)
.padding(4.dp)
)
content is RoomMessageEventContent -> {
val formatted = content.formattedBody
if (formatted != null) {
HtmlRenderer(
htmlContent = formatted,
plainText = content.body
)
} else {
Text(
text = content.body,
modifier = Modifier.padding(4.dp)
)
}
}
event.event is ClientEvent.RoomEvent.StateEvent -> Text(
when (content) {
is AvatarEventContent -> "Avatar changed"
is PowerLevelsEventContent -> "Permissions changed"
is MemberEventContent -> "Membership changed"
is CanonicalAliasEventContent -> "Canonical alias changed"
is TopicEventContent -> "Topic changed"
is NameEventContent -> "Name changed"
is HistoryVisibilityEventContent -> "History visibility changed"
else -> content.toString()
},
Modifier.fillMaxWidth(),
textAlign = TextAlign.Center
)
else -> Text(content.toString(),
Modifier
.fillMaxHeight()
.padding(4.dp)
.background(MaterialTheme.colorScheme.errorContainer)
.padding(4.dp)
)
}
}
}
private fun String.sanitizeHTML(): String {
val matrixSafelist = Safelist()
.addTags(
"h1", "h2", "h3", "h4", "h5", "h6",
"b", "i", "u", "strong", "s", "del",
"sup", "sub", "code",
"table", "thead", "tbody",
"tr", "th", "td", "ul", "ol", "li",
"blockquote", "details", "summary",
"em", "code", "div", "pre", "span", "img"
)
.addAttributes("a", "target", "href")
.addAttributes("img", "width", "height", "alt", "title", "src")
.addAttributes("ol", "start")
.addAttributes("code", "class")
.addAttributes("div", "data-mx-maths")
val doc = Jsoup.parse(this)
doc.select("mx-reply").remove()
return Jsoup.clean(doc.body().toString(), matrixSafelist)
}
@SuppressLint("ClickableViewAccessibility")
@Composable
fun HtmlRenderer(
htmlContent: String,
modifier: Modifier = Modifier,
plainText: String = ""
) {
val context = LocalContext.current
var isLoaded by remember(htmlContent) { mutableStateOf(false) }
val webView = remember { WebView(context).apply {
layoutParams = ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT
)
settings.apply {
javaScriptEnabled = false
loadWithOverviewMode = true
useWideViewPort = true
layoutAlgorithm = WebSettings.LayoutAlgorithm.TEXT_AUTOSIZING
}
isVerticalScrollBarEnabled = false
setBackgroundColor(Color.Transparent.toArgb())
setOnTouchListener { _, e -> e.action == MotionEvent.ACTION_MOVE }
} }
val css = """
body {
font-family: -apple-system, sans-serif;
font-size: 16px;
line-height: 1.5;
color: ${colorToCss(MaterialTheme.colorScheme.onBackground)};
margin: 0; padding: 2px 0;
word-wrap: break-word;
}
a { color: ${colorToCss(MaterialTheme.colorScheme.primary)}; text-decoration: none; }
img { max-width: 100%; height: auto; display: block; margin: 8px 0; }
table { width: 100%; border-collapse: collapse; margin: 8px 0; }
th, td { border: 1px solid ${colorToCss(MaterialTheme.colorScheme.outline)}; padding: 8px; text-align: left; }
blockquote { border-left: 4px solid ${colorToCss(MaterialTheme.colorScheme.primary)}; padding-left: 12px; margin: 8px 0; }
pre, code { background-color: ${colorToCss(MaterialTheme.colorScheme.surfaceVariant)}; padding: 2px 4px; border-radius: 4px; }
""".trimIndent()
LaunchedEffect(htmlContent) {
isLoaded = false
webView.webViewClient = object : WebViewClient() {
override fun onPageFinished(view: WebView?, url: String?) {
isLoaded = true
view?.requestLayout()
}
override fun shouldOverrideUrlLoading(view: WebView, request: WebResourceRequest): Boolean {
return try {
context.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(request.url.toString())))
true
} catch (e: Exception) { false }
}
}
webView.loadDataWithBaseURL(null, wrapHtml(htmlContent, css), "text/html", "UTF-8", null)
}
DisposableEffect(webView) {
onDispose { /* webView.destroy() */ }
}
Box(
modifier = modifier
.fillMaxWidth()
.heightIn(min = 24.dp)
.animateContentSize()
) {
if (!isLoaded) {
Text(
text = plainText.ifBlank { " " },
modifier = Modifier
.padding(vertical = 2.dp)
.alpha(0.6f),
style = MaterialTheme.typography.bodyMedium
)
}
AndroidView(
factory = { webView },
modifier = Modifier
.fillMaxWidth()
.alpha(if (isLoaded) 1f else 0f)
)
}
}
private fun wrapHtml(content: String, css: String): String {
return """
<!DOCTYPE html><html><head>
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>$css</style></head><body>${content.sanitizeHTML()}</body></html>
""".trimIndent()
}
private fun colorToCss(color: Color): String {
return String.format("#%06X", 0xFFFFFF and color.toArgb())
}
+10 -8
View File
@@ -1,18 +1,19 @@
[versions]
agp = "8.13.0"
coil = "3.1.0"
jsoup = "1.20.1"
agp = "9.1.0"
coil = "3.4.0"
iconsaxCompose = "0.0.5"
jsoup = "1.22.1"
kotlin = "2.2.21"
coreKtx = "1.15.0"
junit = "4.13.2"
junitVersion = "1.2.1"
espressoCore = "3.6.1"
junitVersion = "1.3.0"
espressoCore = "3.7.0"
kotlinxSerializationJson = "1.7.3"
ktor = "3.1.0"
ktor = "3.4.1"
lifecycleRuntimeKtx = "2.8.7"
activityCompose = "1.10.0"
composeBom = "2025.02.00"
navigationCompose = "2.8.8"
composeBom = "2026.03.00"
navigationCompose = "2.9.7"
room = "2.6.1"
splittiesFunPackAndroidBase = "3.0.0"
trixnityClient = "5.2.0"
@@ -29,6 +30,7 @@ androidx-room-ktx = { module = "androidx.room:room-ktx", version.ref = "room" }
androidx-room-runtime = { module = "androidx.room:room-runtime", version.ref = "room" }
coil-compose = { module = "io.coil-kt.coil3:coil-compose", version.ref = "coil" }
coil-network-okhttp = { module = "io.coil-kt.coil3:coil-network-okhttp", version.ref = "coil" }
iconsax-compose = { module = "io.github.rabehx:iconsax-compose", version.ref = "iconsaxCompose" }
jsoup = { module = "org.jsoup:jsoup", version.ref = "jsoup" }
junit = { group = "junit", name = "junit", version.ref = "junit" }
androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
+1 -1
View File
@@ -6,6 +6,6 @@
#Thu Feb 20 10:45:47 GMT 2025
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.1-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
+3
View File
@@ -16,6 +16,9 @@ pluginManagement {
gradlePluginPortal()
}
}
plugins {
id("org.gradle.toolchains.foojay-resolver-convention") version "1.0.0"
}
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {