Compare commits
8
Commits
23780489f6
...
0384626d83
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0384626d83 | ||
|
|
ea783c9f27 | ||
|
|
4236c4342b | ||
|
|
c40b13a7ea | ||
|
|
de205b739f | ||
|
|
8ac9ccfaca | ||
|
|
7a2567f019 | ||
|
|
26417b8072 |
No files matched your search
@@ -1,7 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
* Created by sweetbread
|
* Created by sweetbread
|
||||||
* Copyright (c) 2025. All rights reserved.
|
* Copyright (c) 2025. All rights reserved.
|
||||||
* Last modified 03.03.2025, 16:46
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
@@ -87,4 +86,5 @@ dependencies {
|
|||||||
|
|
||||||
// Others
|
// Others
|
||||||
implementation(libs.splitties.base) // Syntax sugar
|
implementation(libs.splitties.base) // Syntax sugar
|
||||||
|
implementation(libs.jsoup) // HTML parser
|
||||||
}
|
}
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
* Created by sweetbread
|
* Created by sweetbread
|
||||||
* Copyright (c) 2025. All rights reserved.
|
* Copyright (c) 2025. All rights reserved.
|
||||||
* Last modified 03.03.2025, 20:22
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package ru.risdeveau.pixeldragon.ui.activity
|
package ru.risdeveau.pixeldragon.ui.activity
|
||||||
@@ -18,16 +17,23 @@ import androidx.compose.material3.Scaffold
|
|||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TopAppBar
|
import androidx.compose.material3.TopAppBar
|
||||||
import androidx.compose.material3.TopAppBarDefaults.topAppBarColors
|
import androidx.compose.material3.TopAppBarDefaults.topAppBarColors
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.navigation.NavType
|
import androidx.navigation.NavType
|
||||||
import androidx.navigation.compose.NavHost
|
import androidx.navigation.compose.NavHost
|
||||||
import androidx.navigation.compose.composable
|
import androidx.navigation.compose.composable
|
||||||
import androidx.navigation.compose.rememberNavController
|
import androidx.navigation.compose.rememberNavController
|
||||||
import androidx.navigation.navArgument
|
import androidx.navigation.navArgument
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import ru.risdeveau.pixeldragon.api.Me
|
||||||
|
import ru.risdeveau.pixeldragon.api.getMe
|
||||||
import ru.risdeveau.pixeldragon.ui.layout.Room
|
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
|
||||||
|
|
||||||
|
var ME: Me? = null
|
||||||
|
|
||||||
class MainActivity : ComponentActivity() {
|
class MainActivity : ComponentActivity() {
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
@@ -52,6 +58,12 @@ class MainActivity : ComponentActivity() {
|
|||||||
) { innerPadding ->
|
) { innerPadding ->
|
||||||
val navController = rememberNavController()
|
val navController = rememberNavController()
|
||||||
|
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
ME = getMe()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
NavHost(navController = navController, startDestination = "rooms") {
|
NavHost(navController = navController, startDestination = "rooms") {
|
||||||
composable("rooms") { RoomList(Modifier.padding(innerPadding), navController) }
|
composable("rooms") { RoomList(Modifier.padding(innerPadding), navController) }
|
||||||
composable(
|
composable(
|
||||||
|
|||||||
@@ -0,0 +1,77 @@
|
|||||||
|
/*
|
||||||
|
* Created by sweetbread
|
||||||
|
* Copyright (c) 2025. All rights reserved.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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 androidx.compose.material3.CircularProgressIndicator
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.layout.ContentScale
|
||||||
|
import coil3.compose.AsyncImagePainter
|
||||||
|
import coil3.compose.rememberAsyncImagePainter
|
||||||
|
import coil3.network.NetworkHeaders
|
||||||
|
import coil3.network.httpHeaders
|
||||||
|
import coil3.request.ImageRequest
|
||||||
|
import ru.risdeveau.pixeldragon.api.mxcToUrl
|
||||||
|
import ru.risdeveau.pixeldragon.token
|
||||||
|
import splitties.init.appCtx
|
||||||
|
|
||||||
|
|
||||||
|
enum class ImageLoadState {
|
||||||
|
Loading, Success, Error
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun MXCImage(
|
||||||
|
mxcUrl: String,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
contentScale: ContentScale = ContentScale.Fit,
|
||||||
|
contentDescription: String = ""
|
||||||
|
) {
|
||||||
|
val loadState = remember { mutableStateOf(ImageLoadState.Loading) }
|
||||||
|
val painter = rememberAsyncImagePainter(
|
||||||
|
model = ImageRequest.Builder(appCtx)
|
||||||
|
.data(mxcToUrl(mxcUrl))
|
||||||
|
.httpHeaders(
|
||||||
|
NetworkHeaders.Builder()
|
||||||
|
.set("Authorization", "Bearer $token")
|
||||||
|
.set("Cache-Control", "max-age=86400")
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
.build(),
|
||||||
|
onState = { state ->
|
||||||
|
loadState.value = when (state) {
|
||||||
|
is AsyncImagePainter.State.Loading -> ImageLoadState.Loading
|
||||||
|
is AsyncImagePainter.State.Success -> ImageLoadState.Success
|
||||||
|
else -> ImageLoadState.Error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = modifier.fillMaxSize()
|
||||||
|
) {
|
||||||
|
Image(
|
||||||
|
painter = painter,
|
||||||
|
contentDescription = contentDescription,
|
||||||
|
contentScale = contentScale,
|
||||||
|
modifier = Modifier.fillMaxSize()
|
||||||
|
)
|
||||||
|
|
||||||
|
when (loadState.value) {
|
||||||
|
ImageLoadState.Loading -> CircularProgressIndicator()
|
||||||
|
ImageLoadState.Error -> Icon(Icons.Outlined.Warning, "Error")
|
||||||
|
ImageLoadState.Success -> Unit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,40 +1,56 @@
|
|||||||
/*
|
/*
|
||||||
* Created by sweetbread
|
* Created by sweetbread
|
||||||
* Copyright (c) 2025. All rights reserved.
|
* Copyright (c) 2025. All rights reserved.
|
||||||
* Last modified 03.03.2025, 21:30
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package ru.risdeveau.pixeldragon.ui.layout
|
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.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.fillMaxHeight
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
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.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.DisposableEffect
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
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.text.font.FontWeight
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.viewinterop.AndroidView
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
import org.jsoup.Jsoup
|
||||||
|
import org.jsoup.safety.Safelist
|
||||||
import ru.risdeveau.pixeldragon.api.Event
|
import ru.risdeveau.pixeldragon.api.Event
|
||||||
import ru.risdeveau.pixeldragon.api.getAccountData
|
import ru.risdeveau.pixeldragon.api.getAccountData
|
||||||
import ru.risdeveau.pixeldragon.api.getEventsAround
|
import ru.risdeveau.pixeldragon.api.getEventsAround
|
||||||
import ru.risdeveau.pixeldragon.api.getMe
|
import ru.risdeveau.pixeldragon.ui.activity.ME
|
||||||
|
import ru.risdeveau.pixeldragon.ui.item.MXCImage
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun Room(modifier: Modifier = Modifier, rid: String) {
|
fun Room(modifier: Modifier = Modifier, rid: String) {
|
||||||
@@ -45,7 +61,7 @@ fun Room(modifier: Modifier = Modifier, rid: String) {
|
|||||||
LaunchedEffect(true) {
|
LaunchedEffect(true) {
|
||||||
coroutineScope.launch {
|
coroutineScope.launch {
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
val readMark = getAccountData(getMe()!!.userId, rid, "m.fully_read")
|
val readMark = getAccountData(ME!!.userId, rid, "m.fully_read")
|
||||||
val eventsAround = getEventsAround(rid, readMark!!.getString("event_id")) //FIXME: Null check
|
val eventsAround = getEventsAround(rid, readMark!!.getString("event_id")) //FIXME: Null check
|
||||||
eventsId = eventsAround.let {
|
eventsId = eventsAround.let {
|
||||||
it.before + listOf(it.base) + it.after
|
it.before + listOf(it.base) + it.after
|
||||||
@@ -67,24 +83,262 @@ fun Room(modifier: Modifier = Modifier, rid: String) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun EventItem(event: Event) {
|
fun EventItem(event: Event) {
|
||||||
Box (Modifier.fillMaxWidth()) {
|
Box (Modifier.fillMaxWidth()) {
|
||||||
when (event.type) {
|
when (event.type) {
|
||||||
"m.room.message" -> Column(
|
"m.room.message" -> Column(
|
||||||
Modifier
|
Modifier
|
||||||
.align(Alignment.CenterStart)
|
.fillMaxSize()
|
||||||
|
.then (
|
||||||
|
if (event.sender != ME!!.userId)
|
||||||
|
Modifier.padding(end = 16.dp)
|
||||||
|
else
|
||||||
|
Modifier.padding(start = 16.dp)
|
||||||
|
)
|
||||||
.padding(4.dp)
|
.padding(4.dp)
|
||||||
.background(
|
.background(
|
||||||
color = MaterialTheme.colorScheme.surfaceContainer,
|
if (event.sender != ME?.userId)
|
||||||
shape = RoundedCornerShape(16.dp)
|
MaterialTheme.colorScheme.surfaceContainer
|
||||||
|
else
|
||||||
|
MaterialTheme.colorScheme.primaryContainer
|
||||||
)
|
)
|
||||||
|
.clip(RoundedCornerShape(16.dp))
|
||||||
.padding(4.dp)
|
.padding(4.dp)
|
||||||
) {
|
) {
|
||||||
Text(event.sender, maxLines = 1, fontWeight = FontWeight.Bold)
|
Text(event.sender, maxLines = 1, fontWeight = FontWeight.Bold)
|
||||||
Text(event.content.getString("body"))
|
|
||||||
|
when (val msgtype = event.content.optString("msgtype", null)) {
|
||||||
|
"m.text" -> when (event.content.optString("format")) {
|
||||||
|
"org.matrix.custom.html" ->
|
||||||
|
HtmlRenderer(event.content.getString("formatted_body"))
|
||||||
|
|
||||||
|
else -> Text(event.content.getString("body"))
|
||||||
}
|
}
|
||||||
else -> Text(event.type, Modifier.padding(4.dp).background(MaterialTheme.colorScheme.errorContainer).padding(4.dp))
|
|
||||||
|
"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)
|
||||||
|
}
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
* Created by sweetbread
|
* Created by sweetbread
|
||||||
* Copyright (c) 2025. All rights reserved.
|
* Copyright (c) 2025. All rights reserved.
|
||||||
* Last modified 03.03.2025, 20:22
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package ru.risdeveau.pixeldragon.ui.layout
|
package ru.risdeveau.pixeldragon.ui.layout
|
||||||
@@ -19,7 +18,6 @@ import androidx.compose.foundation.lazy.items
|
|||||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material3.CircularProgressIndicator
|
|
||||||
import androidx.compose.material3.LinearProgressIndicator
|
import androidx.compose.material3.LinearProgressIndicator
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
@@ -32,22 +30,15 @@ import androidx.compose.runtime.rememberCoroutineScope
|
|||||||
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.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.layout.ContentScale
|
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.navigation.NavController
|
import androidx.navigation.NavController
|
||||||
import coil3.compose.SubcomposeAsyncImage
|
|
||||||
import coil3.network.NetworkHeaders
|
|
||||||
import coil3.network.httpHeaders
|
|
||||||
import coil3.request.ImageRequest
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import ru.risdeveau.pixeldragon.api.getRoom
|
import ru.risdeveau.pixeldragon.api.getRoom
|
||||||
import ru.risdeveau.pixeldragon.api.getRooms
|
import ru.risdeveau.pixeldragon.api.getRooms
|
||||||
import ru.risdeveau.pixeldragon.api.mxcToUrl
|
|
||||||
import ru.risdeveau.pixeldragon.db.Room
|
import ru.risdeveau.pixeldragon.db.Room
|
||||||
import ru.risdeveau.pixeldragon.token
|
import ru.risdeveau.pixeldragon.ui.item.MXCImage
|
||||||
import splitties.init.appCtx
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun RoomList(modifier: Modifier = Modifier, navController: NavController) {
|
fun RoomList(modifier: Modifier = Modifier, navController: NavController) {
|
||||||
@@ -102,16 +93,18 @@ fun RoomItem(modifier: Modifier = Modifier, rid: String, navController: NavContr
|
|||||||
.padding(8.dp)
|
.padding(8.dp)
|
||||||
.height((52 + 8 * 2).dp)
|
.height((52 + 8 * 2).dp)
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.background(MaterialTheme.colorScheme.background, RoundedCornerShape(16.dp))
|
.background(MaterialTheme.colorScheme.background)
|
||||||
.padding(8.dp)
|
.clip(RoundedCornerShape(12.dp))
|
||||||
.clickable {
|
.clickable {
|
||||||
if (room!!.type == "m.space")
|
if (room!!.type == "m.space")
|
||||||
navController.navigate("space/$rid")
|
navController.navigate("space/$rid")
|
||||||
else
|
else
|
||||||
navController.navigate("room/$rid")
|
navController.navigate("room/$rid")
|
||||||
}
|
}
|
||||||
|
.padding(8.dp)
|
||||||
) {
|
) {
|
||||||
SubcomposeAsyncImage(
|
MXCImage(
|
||||||
|
room!!.avatarUrl ?: "",
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(end = 4.dp)
|
.padding(end = 4.dp)
|
||||||
.height(52.dp)
|
.height(52.dp)
|
||||||
@@ -121,20 +114,6 @@ fun RoomItem(modifier: Modifier = Modifier, rid: String, navController: NavContr
|
|||||||
it.clip(RoundedCornerShape(12.dp))
|
it.clip(RoundedCornerShape(12.dp))
|
||||||
else
|
else
|
||||||
it.clip(CircleShape)
|
it.clip(CircleShape)
|
||||||
},
|
|
||||||
model = ImageRequest.Builder(appCtx)
|
|
||||||
.data(mxcToUrl(room!!.avatarUrl ?: ""))
|
|
||||||
.httpHeaders(
|
|
||||||
NetworkHeaders.Builder()
|
|
||||||
.set("Authorization", "Bearer $token")
|
|
||||||
.set("Cache-Control", "max-age=86400")
|
|
||||||
.build()
|
|
||||||
)
|
|
||||||
.build(),
|
|
||||||
contentDescription = room!!.roomId,
|
|
||||||
contentScale = ContentScale.Crop,
|
|
||||||
loading = {
|
|
||||||
CircularProgressIndicator()
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
Column {
|
Column {
|
||||||
@@ -152,7 +131,8 @@ fun RoomItem(modifier: Modifier = Modifier, rid: String, navController: NavContr
|
|||||||
.padding(8.dp)
|
.padding(8.dp)
|
||||||
.height(80.dp)
|
.height(80.dp)
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.background(MaterialTheme.colorScheme.background, RoundedCornerShape(16.dp))
|
.background(MaterialTheme.colorScheme.background)
|
||||||
|
.clip(RoundedCornerShape(16.dp))
|
||||||
.padding(8.dp)
|
.padding(8.dp)
|
||||||
) {
|
) {
|
||||||
LinearProgressIndicator(Modifier.fillMaxWidth())
|
LinearProgressIndicator(Modifier.fillMaxWidth())
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
[versions]
|
[versions]
|
||||||
agp = "8.7.3"
|
agp = "8.7.3"
|
||||||
coil = "3.1.0"
|
coil = "3.1.0"
|
||||||
|
jsoup = "1.20.1"
|
||||||
kotlin = "2.0.21"
|
kotlin = "2.0.21"
|
||||||
coreKtx = "1.15.0"
|
coreKtx = "1.15.0"
|
||||||
junit = "4.13.2"
|
junit = "4.13.2"
|
||||||
@@ -28,6 +29,7 @@ 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" }
|
||||||
|
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" }
|
||||||
androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
|
androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
|
||||||
|
|||||||
Reference in new issue
Block a user