Compare commits
1 Commits
d7d14389fc
..
feat
| Author | SHA1 | Date | |
|---|---|---|---|
| a91bbaf129 |
+6
-10
@@ -1,5 +1,3 @@
|
|||||||
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Created by sweetbread
|
* Created by sweetbread
|
||||||
* Copyright (c) 2026. All rights reserved.
|
* Copyright (c) 2026. All rights reserved.
|
||||||
@@ -7,13 +5,14 @@ import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
|||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
alias(libs.plugins.android.application)
|
alias(libs.plugins.android.application)
|
||||||
|
alias(libs.plugins.kotlin.android)
|
||||||
alias(libs.plugins.kotlin.compose)
|
alias(libs.plugins.kotlin.compose)
|
||||||
// alias(libs.plugins.ksp)
|
// alias(libs.plugins.ksp)
|
||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
namespace = "ru.risdeveau.pixeldragon"
|
namespace = "ru.risdeveau.pixeldragon"
|
||||||
compileSdk = 36
|
compileSdk = 35
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId = "ru.risdeveau.pixeldragon"
|
applicationId = "ru.risdeveau.pixeldragon"
|
||||||
@@ -38,17 +37,14 @@ android {
|
|||||||
sourceCompatibility = JavaVersion.VERSION_17
|
sourceCompatibility = JavaVersion.VERSION_17
|
||||||
targetCompatibility = JavaVersion.VERSION_17
|
targetCompatibility = JavaVersion.VERSION_17
|
||||||
}
|
}
|
||||||
|
kotlinOptions {
|
||||||
|
jvmTarget = "17"
|
||||||
|
}
|
||||||
buildFeatures {
|
buildFeatures {
|
||||||
compose = true
|
compose = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
kotlin {
|
|
||||||
compilerOptions {
|
|
||||||
jvmTarget.set(JvmTarget.JVM_17)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation(libs.androidx.core.ktx)
|
implementation(libs.androidx.core.ktx)
|
||||||
implementation(libs.androidx.lifecycle.runtime.ktx)
|
implementation(libs.androidx.lifecycle.runtime.ktx)
|
||||||
@@ -98,5 +94,5 @@ dependencies {
|
|||||||
// Others
|
// Others
|
||||||
implementation(libs.splitties.base) // Syntax sugar
|
implementation(libs.splitties.base) // Syntax sugar
|
||||||
implementation(libs.jsoup) // HTML parser
|
implementation(libs.jsoup) // HTML parser
|
||||||
implementation(libs.iconsax.compose) // Material icons
|
|
||||||
}
|
}
|
||||||
@@ -40,7 +40,6 @@ import de.connect2x.trixnity.client.cryptodriver.vodozemac.vodozemac
|
|||||||
import de.connect2x.trixnity.clientserverapi.client.SyncState
|
import de.connect2x.trixnity.clientserverapi.client.SyncState
|
||||||
import ru.risdeveau.pixeldragon.R
|
import ru.risdeveau.pixeldragon.R
|
||||||
import ru.risdeveau.pixeldragon.client
|
import ru.risdeveau.pixeldragon.client
|
||||||
import ru.risdeveau.pixeldragon.ui.layout.Room
|
|
||||||
import ru.risdeveau.pixeldragon.ui.layout.RoomList
|
import ru.risdeveau.pixeldragon.ui.layout.RoomList
|
||||||
import ru.risdeveau.pixeldragon.ui.theme.PixelDragonTheme
|
import ru.risdeveau.pixeldragon.ui.theme.PixelDragonTheme
|
||||||
import ru.risdeveau.pixeldragon.util.getMediaStore
|
import ru.risdeveau.pixeldragon.util.getMediaStore
|
||||||
@@ -95,9 +94,9 @@ class MainActivity : ComponentActivity() {
|
|||||||
"room/{rid}",
|
"room/{rid}",
|
||||||
arguments = listOf(navArgument("rid") { type = NavType.StringType })
|
arguments = listOf(navArgument("rid") { type = NavType.StringType })
|
||||||
) { navBackStackEntry ->
|
) { navBackStackEntry ->
|
||||||
Room(Modifier
|
// Room(Modifier
|
||||||
.padding(innerPadding)
|
// .padding(innerPadding)
|
||||||
.fillMaxSize(), navBackStackEntry.arguments!!.getString("rid")!!)
|
// .fillMaxSize(), navBackStackEntry.arguments!!.getString("rid")!!)
|
||||||
}
|
}
|
||||||
composable(
|
composable(
|
||||||
"space/{rid}",
|
"space/{rid}",
|
||||||
|
|||||||
@@ -8,7 +8,8 @@ package ru.risdeveau.pixeldragon.ui.item
|
|||||||
import androidx.compose.foundation.Image
|
import androidx.compose.foundation.Image
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import io.github.rabehx.iconsax.Iconsax
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.outlined.Warning
|
||||||
import androidx.compose.material3.CircularProgressIndicator
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
@@ -27,7 +28,6 @@ import coil3.request.ImageRequest
|
|||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import de.connect2x.trixnity.client.media
|
import de.connect2x.trixnity.client.media
|
||||||
import de.connect2x.trixnity.clientserverapi.model.media.FileTransferProgress
|
import de.connect2x.trixnity.clientserverapi.model.media.FileTransferProgress
|
||||||
import io.github.rabehx.iconsax.outline.Warning2
|
|
||||||
import ru.risdeveau.pixeldragon.client
|
import ru.risdeveau.pixeldragon.client
|
||||||
|
|
||||||
enum class ImageLoadState {
|
enum class ImageLoadState {
|
||||||
@@ -119,7 +119,7 @@ fun MXCImage(
|
|||||||
}
|
}
|
||||||
imageLoadState == ImageLoadState.Error -> {
|
imageLoadState == ImageLoadState.Error -> {
|
||||||
Icon(
|
Icon(
|
||||||
Iconsax.Outline.Warning2,
|
Icons.Outlined.Warning,
|
||||||
contentDescription = "Error",
|
contentDescription = "Error",
|
||||||
modifier = Modifier.align(Alignment.Center)
|
modifier = Modifier.align(Alignment.Center)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -9,190 +9,145 @@ import android.annotation.SuppressLint
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.view.MotionEvent
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import android.webkit.WebResourceRequest
|
import android.webkit.WebResourceRequest
|
||||||
import android.webkit.WebSettings
|
|
||||||
import android.webkit.WebView
|
import android.webkit.WebView
|
||||||
import android.webkit.WebViewClient
|
import android.webkit.WebViewClient
|
||||||
import androidx.compose.animation.animateContentSize
|
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
|
||||||
import androidx.compose.foundation.layout.fillMaxHeight
|
import androidx.compose.foundation.layout.fillMaxHeight
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.heightIn
|
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.Icon
|
|
||||||
import androidx.compose.material3.IconButton
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.OutlinedTextField
|
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextField
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.DisposableEffect
|
import androidx.compose.runtime.DisposableEffect
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.produceState
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.alpha
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.toArgb
|
import androidx.compose.ui.graphics.toArgb
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.viewinterop.AndroidView
|
import androidx.compose.ui.viewinterop.AndroidView
|
||||||
import de.connect2x.trixnity.client.room
|
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.room.toFlowList
|
||||||
import de.connect2x.trixnity.client.store.TimelineEvent
|
import de.connect2x.trixnity.client.store.TimelineEvent
|
||||||
import de.connect2x.trixnity.client.store.eventId
|
import de.connect2x.trixnity.client.user
|
||||||
import de.connect2x.trixnity.client.store.unsigned
|
|
||||||
import de.connect2x.trixnity.core.model.RoomId
|
import de.connect2x.trixnity.core.model.RoomId
|
||||||
import de.connect2x.trixnity.core.model.events.ClientEvent
|
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.RoomMessageEventContent
|
||||||
import de.connect2x.trixnity.core.model.events.m.room.TopicEventContent
|
import io.ktor.util.reflect.instanceOf
|
||||||
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.Dispatchers
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
import kotlinx.coroutines.flow.flatMapLatest
|
import kotlinx.coroutines.flow.filterNotNull
|
||||||
import kotlinx.coroutines.flow.flowOf
|
import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.flow.flatMapConcat
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
import org.jsoup.Jsoup
|
import org.jsoup.Jsoup
|
||||||
import org.jsoup.safety.Safelist
|
import org.jsoup.safety.Safelist
|
||||||
import ru.risdeveau.pixeldragon.client
|
import ru.risdeveau.pixeldragon.client
|
||||||
|
import ru.risdeveau.pixeldragon.ui.item.MXCImage
|
||||||
|
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
|
||||||
@Composable
|
@Composable
|
||||||
fun Room(modifier: Modifier = Modifier, rid: String) {
|
fun Room(modifier: Modifier = Modifier, rid: String) {
|
||||||
val roomId = remember(rid) { RoomId(rid) }
|
val roomId = remember(rid) { RoomId(rid) }
|
||||||
val limit = remember { MutableStateFlow(50) }
|
val limit = remember { MutableStateFlow(50) }
|
||||||
|
|
||||||
val eventsFlow = remember(roomId) {
|
val eventsFlow = remember(roomId) {
|
||||||
client!!.room
|
client!!.room
|
||||||
.getLastTimelineEvents(roomId)
|
.getLastTimelineEvents(roomId)
|
||||||
.toFlowList(limit)
|
.toFlowList(limit)
|
||||||
.flatMapLatest { flows ->
|
|
||||||
if (flows.isEmpty()) flowOf(emptyList())
|
|
||||||
else combine(flows) { it.toList() }
|
|
||||||
}
|
}
|
||||||
}
|
val eventFlows by eventsFlow.collectAsState(initial = emptyList())
|
||||||
val events by eventsFlow.collectAsState(initial = emptyList())
|
|
||||||
|
|
||||||
val listState = rememberLazyListState()
|
val listState = rememberLazyListState()
|
||||||
|
|
||||||
Column(modifier) {
|
LazyColumn(modifier = modifier, state = listState, reverseLayout = true) {
|
||||||
var message by remember { mutableStateOf("") }
|
items(eventFlows, key = { it.hashCode() }) { eventFlow ->
|
||||||
|
EventItem(eventFlow)
|
||||||
LazyColumn(Modifier.weight(1f), state = listState, reverseLayout = true) {
|
|
||||||
items(events, key = { it.eventId.full }) { event ->
|
|
||||||
EventItem(event)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (events.isEmpty()) {
|
if (eventFlows.isEmpty()) {
|
||||||
item {
|
item {
|
||||||
Text("Empty room", modifier = Modifier.padding(16.dp))
|
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)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun EventItem(event: TimelineEvent) {
|
fun EventItem(eventFlow: Flow<TimelineEvent>) {
|
||||||
val content = event.content?.getOrNull()
|
val event by eventFlow.collectAsState(null)
|
||||||
Box(Modifier
|
event?.let { event ->
|
||||||
.fillMaxWidth()
|
Box(Modifier.fillMaxWidth()) {
|
||||||
.heightIn(min = 24.dp)) {
|
/*when (event.type) {
|
||||||
when {
|
"m.room.message" -> Column(
|
||||||
content == null -> Text("Not decrypted",
|
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.content?.getOrNull().toString(),
|
||||||
Modifier
|
Modifier
|
||||||
.fillMaxHeight()
|
.fillMaxHeight()
|
||||||
.padding(4.dp)
|
.padding(4.dp)
|
||||||
.background(MaterialTheme.colorScheme.errorContainer)
|
.background(MaterialTheme.colorScheme.errorContainer)
|
||||||
.padding(4.dp)
|
.padding(4.dp)
|
||||||
)
|
)*/
|
||||||
|
Text(event.content?.getOrNull().toString())
|
||||||
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)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -208,115 +163,205 @@ private fun String.sanitizeHTML(): String {
|
|||||||
"blockquote", "details", "summary",
|
"blockquote", "details", "summary",
|
||||||
"em", "code", "div", "pre", "span", "img"
|
"em", "code", "div", "pre", "span", "img"
|
||||||
)
|
)
|
||||||
.addAttributes("a", "target", "href")
|
// .addAttributes("span",
|
||||||
.addAttributes("img", "width", "height", "alt", "title", "src")
|
// "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("ol", "start")
|
||||||
.addAttributes("code", "class")
|
.addAttributes("code", "class")
|
||||||
.addAttributes("div", "data-mx-maths")
|
.addAttributes("div", "data-mx-maths")
|
||||||
|
|
||||||
val doc = Jsoup.parse(this)
|
val doc = Jsoup.parse(this)
|
||||||
doc.select("mx-reply").remove()
|
doc.select("mx-reply").remove()
|
||||||
return Jsoup.clean(doc.body().toString(), matrixSafelist)
|
|
||||||
|
val out = Jsoup.clean(doc.body().toString(), matrixSafelist)
|
||||||
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@SuppressLint("ClickableViewAccessibility")
|
|
||||||
@Composable
|
@Composable
|
||||||
fun HtmlRenderer(
|
fun HtmlRenderer(
|
||||||
htmlContent: String,
|
htmlContent: String,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier
|
||||||
plainText: String = ""
|
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
var isLoaded by remember(htmlContent) { mutableStateOf(false) }
|
|
||||||
|
|
||||||
val webView = remember { WebView(context).apply {
|
val webView = remember { WebView(context).apply {
|
||||||
layoutParams = ViewGroup.LayoutParams(
|
|
||||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
|
||||||
ViewGroup.LayoutParams.WRAP_CONTENT
|
|
||||||
)
|
|
||||||
settings.apply {
|
settings.apply {
|
||||||
javaScriptEnabled = false
|
javaScriptEnabled = false
|
||||||
loadWithOverviewMode = true
|
loadWithOverviewMode = true
|
||||||
useWideViewPort = true
|
useWideViewPort = true
|
||||||
layoutAlgorithm = WebSettings.LayoutAlgorithm.TEXT_AUTOSIZING
|
|
||||||
}
|
}
|
||||||
isVerticalScrollBarEnabled = false
|
isVerticalScrollBarEnabled = false
|
||||||
setBackgroundColor(Color.Transparent.toArgb())
|
setBackgroundColor(Color.Transparent.toArgb())
|
||||||
setOnTouchListener { _, e -> e.action == MotionEvent.ACTION_MOVE }
|
|
||||||
} }
|
} }
|
||||||
|
|
||||||
val css = """
|
val css = """
|
||||||
body {
|
body {
|
||||||
font-family: -apple-system, sans-serif;
|
font-family: -apple-system, sans-serif;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
line-height: 1.5;
|
line-height: 1.6;
|
||||||
color: ${colorToCss(MaterialTheme.colorScheme.onBackground)};
|
color: ${colorToCss(MaterialTheme.colorScheme.onBackground)};
|
||||||
margin: 0; padding: 2px 0;
|
margin: 0;
|
||||||
word-wrap: break-word;
|
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)};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
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()
|
""".trimIndent()
|
||||||
|
|
||||||
LaunchedEffect(htmlContent) {
|
LaunchedEffect(htmlContent) {
|
||||||
isLoaded = false
|
webView.loadDataWithBaseURL(
|
||||||
webView.webViewClient = object : WebViewClient() {
|
null,
|
||||||
override fun onPageFinished(view: WebView?, url: String?) {
|
wrapHtml(htmlContent, css),
|
||||||
isLoaded = true
|
"text/html",
|
||||||
view?.requestLayout()
|
"UTF-8",
|
||||||
}
|
null
|
||||||
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) {
|
DisposableEffect(webView) {
|
||||||
onDispose { /* webView.destroy() */ }
|
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(
|
AndroidView(
|
||||||
factory = { webView },
|
factory = { webView },
|
||||||
modifier = Modifier
|
modifier = modifier,
|
||||||
.fillMaxWidth()
|
update = { view ->
|
||||||
.alpha(if (isLoaded) 1f else 0f)
|
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 {
|
private fun wrapHtml(content: String, css: String): String {
|
||||||
return """
|
return """
|
||||||
<!DOCTYPE html><html><head>
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<style>$css</style></head><body>${content.sanitizeHTML()}</body></html>
|
<style>
|
||||||
|
$css
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
${content.sanitizeHTML()}
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
""".trimIndent()
|
""".trimIndent()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun colorToCss(color: Color): String {
|
private fun colorToCss(color: Color): String {
|
||||||
return String.format("#%06X", 0xFFFFFF and color.toArgb())
|
val argb = color.toArgb()
|
||||||
|
return String.format("#%06X", 0xFFFFFF and argb)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
|
fun Flow<Flow<Flow<TimelineEvent>>?>.flattenTimelineEvents(): Flow<TimelineEvent> = this
|
||||||
|
.filterNotNull()
|
||||||
|
.flatMapConcat { middleFlow ->
|
||||||
|
middleFlow.flatMapConcat { innerFlow ->
|
||||||
|
innerFlow
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,19 +1,18 @@
|
|||||||
[versions]
|
[versions]
|
||||||
agp = "9.1.0"
|
agp = "8.13.0"
|
||||||
coil = "3.4.0"
|
coil = "3.1.0"
|
||||||
iconsaxCompose = "0.0.5"
|
jsoup = "1.20.1"
|
||||||
jsoup = "1.22.1"
|
|
||||||
kotlin = "2.2.21"
|
kotlin = "2.2.21"
|
||||||
coreKtx = "1.15.0"
|
coreKtx = "1.15.0"
|
||||||
junit = "4.13.2"
|
junit = "4.13.2"
|
||||||
junitVersion = "1.3.0"
|
junitVersion = "1.2.1"
|
||||||
espressoCore = "3.7.0"
|
espressoCore = "3.6.1"
|
||||||
kotlinxSerializationJson = "1.7.3"
|
kotlinxSerializationJson = "1.7.3"
|
||||||
ktor = "3.4.1"
|
ktor = "3.1.0"
|
||||||
lifecycleRuntimeKtx = "2.8.7"
|
lifecycleRuntimeKtx = "2.8.7"
|
||||||
activityCompose = "1.10.0"
|
activityCompose = "1.10.0"
|
||||||
composeBom = "2026.03.00"
|
composeBom = "2025.02.00"
|
||||||
navigationCompose = "2.9.7"
|
navigationCompose = "2.8.8"
|
||||||
room = "2.6.1"
|
room = "2.6.1"
|
||||||
splittiesFunPackAndroidBase = "3.0.0"
|
splittiesFunPackAndroidBase = "3.0.0"
|
||||||
trixnityClient = "5.2.0"
|
trixnityClient = "5.2.0"
|
||||||
@@ -30,7 +29,6 @@ androidx-room-ktx = { module = "androidx.room:room-ktx", version.ref = "room" }
|
|||||||
androidx-room-runtime = { module = "androidx.room:room-runtime", 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-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" }
|
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" }
|
jsoup = { module = "org.jsoup:jsoup", version.ref = "jsoup" }
|
||||||
junit = { group = "junit", name = "junit", version.ref = "junit" }
|
junit = { group = "junit", name = "junit", version.ref = "junit" }
|
||||||
androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
|
androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
|
||||||
|
|||||||
+1
-1
@@ -6,6 +6,6 @@
|
|||||||
#Thu Feb 20 10:45:47 GMT 2025
|
#Thu Feb 20 10:45:47 GMT 2025
|
||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.1-bin.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
|
|||||||
Reference in New Issue
Block a user