Compare commits
2 Commits
a090ff614a
..
feat
| Author | SHA1 | Date | |
|---|---|---|---|
| a91bbaf129 | |||
| f8b10ebb34 |
+3
@@ -49,6 +49,9 @@
|
||||
<option name="composableFile" value="true" />
|
||||
<option name="previewFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="PreviewParameterProviderOnFirstParameter" enabled="true" level="ERROR" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="PreviewPickerAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
<option name="previewFile" value="true" />
|
||||
|
||||
@@ -66,6 +66,7 @@ dependencies {
|
||||
implementation(libs.trixnity.client)
|
||||
implementation(libs.trixnity.client.media.okio)
|
||||
implementation(libs.trixnity.client.repository.room)
|
||||
implementation(libs.trixnity.client.cryptodriver.vodozemac)
|
||||
// implementation(libs.trixnity.messenger)
|
||||
|
||||
// Ktor - web client
|
||||
|
||||
@@ -6,12 +6,12 @@
|
||||
package ru.risdeveau.pixeldragon
|
||||
|
||||
import android.util.Log
|
||||
import de.connect2x.trixnity.client.MatrixClient
|
||||
import io.ktor.client.HttpClient
|
||||
import io.ktor.client.plugins.cache.HttpCache
|
||||
import io.ktor.client.plugins.logging.LogLevel
|
||||
import io.ktor.client.plugins.logging.Logger
|
||||
import io.ktor.client.plugins.logging.Logging
|
||||
import net.folivo.trixnity.client.MatrixClient
|
||||
|
||||
val webClient = HttpClient {
|
||||
install(Logging) {
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
package ru.risdeveau.pixeldragon.api
|
||||
|
||||
import android.util.Log
|
||||
import io.ktor.client.request.get
|
||||
import io.ktor.client.statement.bodyAsText
|
||||
import org.json.JSONException
|
||||
@@ -14,10 +15,16 @@ import ru.risdeveau.pixeldragon.webClient
|
||||
|
||||
suspend fun getHomeserver(url: String): String? {
|
||||
val r = try { webClient.get("https://$url/.well-known/matrix/client") }
|
||||
catch (_: Exception) { return null }
|
||||
catch (e: Exception) {
|
||||
Log.w("getHomeserver", "Fail sending the request", e)
|
||||
return null
|
||||
}
|
||||
|
||||
val json = try { JSONObject(r.bodyAsText()) }
|
||||
catch (_: JSONException) { return null }
|
||||
catch (e: JSONException) {
|
||||
Log.w("getHomeserver", "Fail parsing the JSON", e)
|
||||
return null
|
||||
}
|
||||
|
||||
if (!json.has("m.homeserver")) return null
|
||||
|
||||
|
||||
@@ -6,10 +6,14 @@
|
||||
package ru.risdeveau.pixeldragon.api
|
||||
|
||||
import android.util.Log
|
||||
import de.connect2x.trixnity.client.CryptoDriverModule
|
||||
import io.ktor.http.Url
|
||||
import net.folivo.trixnity.client.MatrixClient
|
||||
import net.folivo.trixnity.client.login
|
||||
import net.folivo.trixnity.clientserverapi.model.authentication.IdentifierType
|
||||
import de.connect2x.trixnity.client.MatrixClient
|
||||
import de.connect2x.trixnity.client.create
|
||||
import de.connect2x.trixnity.client.cryptodriver.vodozemac.vodozemac
|
||||
import de.connect2x.trixnity.clientserverapi.client.MatrixClientAuthProviderData
|
||||
import de.connect2x.trixnity.clientserverapi.client.classicLogin
|
||||
import de.connect2x.trixnity.clientserverapi.model.authentication.IdentifierType
|
||||
import ru.risdeveau.pixeldragon.client
|
||||
import ru.risdeveau.pixeldragon.util.getMediaStore
|
||||
import ru.risdeveau.pixeldragon.util.getRoomStore
|
||||
@@ -24,13 +28,16 @@ suspend fun login(server: String, login: String, pass: String): Boolean {
|
||||
val pinfo = appCtx.packageManager.getPackageInfo(appCtx.packageName, 0)
|
||||
|
||||
try {
|
||||
client = MatrixClient.login(
|
||||
baseUrl = hs,
|
||||
identifier = IdentifierType.User(login),
|
||||
password = pass,
|
||||
initialDeviceDisplayName = "PixelDragon Android v${pinfo.versionName}",
|
||||
client = MatrixClient.create(
|
||||
repositoriesModule = getRoomStore(appCtx),
|
||||
mediaStore = getMediaStore()
|
||||
mediaStoreModule = getMediaStore(),
|
||||
cryptoDriverModule = CryptoDriverModule.vodozemac(),
|
||||
authProviderData = MatrixClientAuthProviderData.classicLogin(
|
||||
baseUrl = hs,
|
||||
identifier = IdentifierType.User(login),
|
||||
password = pass,
|
||||
initialDeviceDisplayName = "PixelDragon Android v${pinfo.versionName}",
|
||||
).getOrThrow()
|
||||
).getOrThrow()
|
||||
|
||||
return true
|
||||
|
||||
@@ -30,12 +30,14 @@ import androidx.navigation.compose.NavHost
|
||||
import androidx.navigation.compose.composable
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import androidx.navigation.navArgument
|
||||
import de.connect2x.trixnity.client.CryptoDriverModule
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import net.folivo.trixnity.client.MatrixClient
|
||||
import net.folivo.trixnity.client.fromStore
|
||||
import net.folivo.trixnity.clientserverapi.client.SyncState
|
||||
import de.connect2x.trixnity.client.MatrixClient
|
||||
import de.connect2x.trixnity.client.create
|
||||
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.RoomList
|
||||
@@ -111,9 +113,10 @@ class MainActivity : ComponentActivity() {
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
if (client == null) {
|
||||
client = MatrixClient.fromStore(
|
||||
getRoomStore(appCtx),
|
||||
getMediaStore()
|
||||
client = MatrixClient.create(
|
||||
repositoriesModule = getRoomStore(appCtx),
|
||||
mediaStoreModule = getMediaStore(),
|
||||
cryptoDriverModule = CryptoDriverModule.vodozemac()
|
||||
).getOrNull()
|
||||
|
||||
if (client == null) {
|
||||
|
||||
@@ -26,8 +26,8 @@ import androidx.compose.ui.platform.LocalContext
|
||||
import coil3.compose.rememberAsyncImagePainter
|
||||
import coil3.request.ImageRequest
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import net.folivo.trixnity.client.media
|
||||
import net.folivo.trixnity.clientserverapi.model.media.FileTransferProgress
|
||||
import de.connect2x.trixnity.client.media
|
||||
import de.connect2x.trixnity.clientserverapi.model.media.FileTransferProgress
|
||||
import ru.risdeveau.pixeldragon.client
|
||||
|
||||
enum class ImageLoadState {
|
||||
|
||||
@@ -1,342 +1,367 @@
|
||||
///*
|
||||
// * 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"
|
||||
/*
|
||||
* 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.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.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.produceState
|
||||
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 de.connect2x.trixnity.client.room
|
||||
import de.connect2x.trixnity.client.room.toFlowList
|
||||
import de.connect2x.trixnity.client.store.TimelineEvent
|
||||
import de.connect2x.trixnity.client.user
|
||||
import de.connect2x.trixnity.core.model.RoomId
|
||||
import de.connect2x.trixnity.core.model.events.ClientEvent
|
||||
import de.connect2x.trixnity.core.model.events.m.room.RoomMessageEventContent
|
||||
import io.ktor.util.reflect.instanceOf
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.filterNotNull
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.flatMapConcat
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.jsoup.Jsoup
|
||||
import org.jsoup.safety.Safelist
|
||||
import ru.risdeveau.pixeldragon.client
|
||||
import ru.risdeveau.pixeldragon.ui.item.MXCImage
|
||||
|
||||
@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)
|
||||
}
|
||||
val eventFlows by eventsFlow.collectAsState(initial = emptyList())
|
||||
|
||||
val listState = rememberLazyListState()
|
||||
|
||||
LazyColumn(modifier = modifier, state = listState, reverseLayout = true) {
|
||||
items(eventFlows, key = { it.hashCode() }) { eventFlow ->
|
||||
EventItem(eventFlow)
|
||||
}
|
||||
|
||||
if (eventFlows.isEmpty()) {
|
||||
item {
|
||||
Text("Empty room", modifier = Modifier.padding(16.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun EventItem(eventFlow: Flow<TimelineEvent>) {
|
||||
val event by eventFlow.collectAsState(null)
|
||||
event?.let { 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.content?.getOrNull().toString(),
|
||||
Modifier
|
||||
.fillMaxHeight()
|
||||
.padding(4.dp)
|
||||
.background(MaterialTheme.colorScheme.errorContainer)
|
||||
.padding(4.dp)
|
||||
)*/
|
||||
Text(event.content?.getOrNull().toString())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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("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)
|
||||
//}
|
||||
.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)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
fun Flow<Flow<Flow<TimelineEvent>>?>.flattenTimelineEvents(): Flow<TimelineEvent> = this
|
||||
.filterNotNull()
|
||||
.flatMapConcat { middleFlow ->
|
||||
middleFlow.flatMapConcat { innerFlow ->
|
||||
innerFlow
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,11 +31,11 @@ import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.navigation.NavController
|
||||
import kotlinx.coroutines.flow.map
|
||||
import net.folivo.trixnity.client.flattenValues
|
||||
import net.folivo.trixnity.client.room
|
||||
import net.folivo.trixnity.client.store.Room
|
||||
import net.folivo.trixnity.client.store.type
|
||||
import net.folivo.trixnity.core.model.events.m.room.CreateEventContent
|
||||
import de.connect2x.trixnity.client.flattenValues
|
||||
import de.connect2x.trixnity.client.room
|
||||
import de.connect2x.trixnity.client.store.Room
|
||||
import de.connect2x.trixnity.client.store.type
|
||||
import de.connect2x.trixnity.core.model.events.m.room.CreateEventContent
|
||||
import ru.risdeveau.pixeldragon.client
|
||||
import ru.risdeveau.pixeldragon.ui.item.MXCImage
|
||||
|
||||
|
||||
@@ -7,21 +7,18 @@ package ru.risdeveau.pixeldragon.util
|
||||
|
||||
import android.content.Context
|
||||
import androidx.room.Room
|
||||
import net.folivo.trixnity.client.media.okio.OkioMediaStore
|
||||
import net.folivo.trixnity.client.store.repository.room.TrixnityRoomDatabase
|
||||
import net.folivo.trixnity.client.store.repository.room.createRoomRepositoriesModule
|
||||
import de.connect2x.trixnity.client.MediaStoreModule
|
||||
import de.connect2x.trixnity.client.RepositoriesModule
|
||||
import de.connect2x.trixnity.client.media.okio.okio
|
||||
import de.connect2x.trixnity.client.store.repository.room.TrixnityRoomDatabase
|
||||
import de.connect2x.trixnity.client.store.repository.room.room
|
||||
import okio.Path.Companion.toPath
|
||||
import org.koin.dsl.module
|
||||
import splitties.init.appCtx
|
||||
|
||||
fun getMediaStore() = OkioMediaStore(appCtx.filesDir.resolve("media").absolutePath.toPath())
|
||||
fun getRoomStore(context: Context) = module {
|
||||
includes(
|
||||
createRoomRepositoriesModule(
|
||||
Room.databaseBuilder<TrixnityRoomDatabase>(
|
||||
context,
|
||||
"trixnity.db"
|
||||
)
|
||||
)
|
||||
fun getMediaStore() = MediaStoreModule.okio(appCtx.filesDir.resolve("media").absolutePath.toPath())
|
||||
fun getRoomStore(context: Context) = RepositoriesModule.room(
|
||||
databaseBuilder = Room.databaseBuilder<TrixnityRoomDatabase>(
|
||||
context,
|
||||
"trixnity.db"
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
@@ -15,7 +15,7 @@ composeBom = "2025.02.00"
|
||||
navigationCompose = "2.8.8"
|
||||
room = "2.6.1"
|
||||
splittiesFunPackAndroidBase = "3.0.0"
|
||||
trixnityClient = "4.22.7"
|
||||
trixnityClient = "5.2.0"
|
||||
|
||||
[libraries]
|
||||
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
|
||||
@@ -47,9 +47,10 @@ ktor-client-okhttp = { module = "io.ktor:ktor-client-okhttp", version.ref = "kto
|
||||
ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor" }
|
||||
ktor-client-logging = { module = "io.ktor:ktor-client-logging", version.ref = "ktor" }
|
||||
splitties-base = { module = "com.louiscad.splitties:splitties-fun-pack-android-base", version.ref = "splittiesFunPackAndroidBase" }
|
||||
trixnity-client = { module = "net.folivo:trixnity-client", version.ref = "trixnityClient" }
|
||||
trixnity-client-media-okio = { module = "net.folivo:trixnity-client-media-okio", version.ref = "trixnityClient" }
|
||||
trixnity-client-repository-room = { module = "net.folivo:trixnity-client-repository-room", version.ref = "trixnityClient" }
|
||||
trixnity-client = { module = "de.connect2x.trixnity:trixnity-client", version.ref = "trixnityClient" }
|
||||
trixnity-client-media-okio = { module = "de.connect2x.trixnity:trixnity-client-media-okio", version.ref = "trixnityClient" }
|
||||
trixnity-client-repository-room = { module = "de.connect2x.trixnity:trixnity-client-repository-room", version.ref = "trixnityClient" }
|
||||
trixnity-client-cryptodriver-vodozemac = { module = "de.connect2x.trixnity:trixnity-client-cryptodriver-vodozemac", version.ref = "trixnityClient" }
|
||||
|
||||
[plugins]
|
||||
android-application = { id = "com.android.application", version.ref = "agp" }
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user