Compare commits
53 Commits
master
..
e10e47d6c0
| Author | SHA1 | Date | |
|---|---|---|---|
| e10e47d6c0 | |||
| 8f6adf0746 | |||
| eae2d4f388 | |||
| b312cc50f1 | |||
| 8b8926f931 | |||
| a35f5196b1 | |||
| cab1ff93a4 | |||
| 769319f308 | |||
| d28baa70d4 | |||
| f29672d1bd | |||
| dd30db2130 | |||
| 0711ce41d3 | |||
| b1357bd4e7 | |||
| 0e107eca0b | |||
| f90a8e3472 | |||
| aa18e5b0a6 | |||
| a21860c21d | |||
| d7d14389fc | |||
| 902af5e7b5 | |||
| 64de39f0ca | |||
| f8b10ebb34 | |||
| a090ff614a | |||
| dd3b31d0b2 | |||
| c7b5f20c06 | |||
| 8d6a76ccb5 | |||
| 1cfad2ca4e | |||
| b6e8c73758 | |||
| 28337b1306 | |||
| 05d3d739e2 | |||
| 5c6cd29a05 | |||
| 70d9db6cbf | |||
| 7026acc229 | |||
| fa53e0ae86 | |||
| ebbc9a4b1f | |||
| 05fbfbac07 | |||
| 5897d31a51 | |||
| a0bfba23cf | |||
| 00f273c866 | |||
| 0384626d83 | |||
| ea783c9f27 | |||
| 4236c4342b | |||
| c40b13a7ea | |||
| de205b739f | |||
| 8ac9ccfaca | |||
| 7a2567f019 | |||
| 26417b8072 | |||
| 23780489f6 | |||
| bd87ca2729 | |||
| 7ba5876a71 | |||
| cab56d6329 | |||
| e70049f1f5 | |||
| c0944ec0a8 | |||
| 5fbffd8700 |
+6
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="AndroidProjectSystem">
|
||||
<option name="providerId" value="com.android.tools.idea.GradleProjectSystem" />
|
||||
</component>
|
||||
</project>
|
||||
Generated
+1
-5
@@ -1,7 +1,3 @@
|
||||
<component name="CopyrightManager">
|
||||
<settings default="My">
|
||||
<module2copyright>
|
||||
<element module="All" copyright="My" />
|
||||
</module2copyright>
|
||||
</settings>
|
||||
<settings default="My" />
|
||||
</component>
|
||||
Generated
+8
@@ -4,6 +4,14 @@
|
||||
<selectionStates>
|
||||
<SelectionState runConfigName="app">
|
||||
<option name="selectionMode" value="DROPDOWN" />
|
||||
<DropdownSelection timestamp="2025-02-22T11:46:39.159466074Z">
|
||||
<Target type="DEFAULT_BOOT">
|
||||
<handle>
|
||||
<DeviceId pluginId="PhysicalDevice" identifier="serial=22163a3c" />
|
||||
</handle>
|
||||
</Target>
|
||||
</DropdownSelection>
|
||||
<DialogSelection />
|
||||
</SelectionState>
|
||||
</selectionStates>
|
||||
</component>
|
||||
|
||||
+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" />
|
||||
|
||||
Generated
+1
-1
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="KotlinJpsPluginSettings">
|
||||
<option name="version" value="2.0.0" />
|
||||
<option name="version" value="2.0.21" />
|
||||
</component>
|
||||
</project>
|
||||
Generated
+4
@@ -5,8 +5,12 @@
|
||||
<set>
|
||||
<option value="com.intellij.execution.junit.AbstractAllInDirectoryConfigurationProducer" />
|
||||
<option value="com.intellij.execution.junit.AllInPackageConfigurationProducer" />
|
||||
<option value="com.intellij.execution.junit.PatternConfigurationProducer" />
|
||||
<option value="com.intellij.execution.junit.TestInClassConfigurationProducer" />
|
||||
<option value="com.intellij.execution.junit.UniqueIdConfigurationProducer" />
|
||||
<option value="com.intellij.execution.junit.testDiscovery.JUnitTestDiscoveryConfigurationProducer" />
|
||||
<option value="org.jetbrains.kotlin.idea.junit.KotlinJUnitRunConfigurationProducer" />
|
||||
<option value="org.jetbrains.kotlin.idea.junit.KotlinPatternConfigurationProducer" />
|
||||
</set>
|
||||
</option>
|
||||
</component>
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="StudioBotProjectSettings">
|
||||
<option name="shareContext" value="OptedIn" />
|
||||
</component>
|
||||
</project>
|
||||
Generated
+1
-1
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||
<mapping directory="" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
+45
-12
@@ -1,22 +1,28 @@
|
||||
/*
|
||||
* Created by sweetbread on 21.02.2025, 12:01
|
||||
* Copyright (c) 2025. All rights reserved.
|
||||
* Last modified 21.02.2025, 12:01
|
||||
* Created by sweetbread
|
||||
* Copyright (c) 2026. All rights reserved.
|
||||
*/
|
||||
|
||||
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
||||
|
||||
/*
|
||||
* Created by sweetbread
|
||||
* Copyright (c) 2026. All rights reserved.
|
||||
*/
|
||||
|
||||
plugins {
|
||||
alias(libs.plugins.android.application)
|
||||
alias(libs.plugins.kotlin.android)
|
||||
alias(libs.plugins.kotlin.compose)
|
||||
// alias(libs.plugins.ksp)
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "ru.risdeveau.pixeldragon"
|
||||
compileSdk = 35
|
||||
compileSdk = 36
|
||||
|
||||
defaultConfig {
|
||||
applicationId = "ru.risdeveau.pixeldragon"
|
||||
minSdk = 24
|
||||
minSdk = 28
|
||||
targetSdk = 35
|
||||
versionCode = 1
|
||||
versionName = "1.0"
|
||||
@@ -34,17 +40,20 @@ android {
|
||||
}
|
||||
}
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||
targetCompatibility = JavaVersion.VERSION_1_8
|
||||
}
|
||||
kotlinOptions {
|
||||
jvmTarget = "1.8"
|
||||
sourceCompatibility = JavaVersion.VERSION_17
|
||||
targetCompatibility = JavaVersion.VERSION_17
|
||||
}
|
||||
buildFeatures {
|
||||
compose = true
|
||||
}
|
||||
}
|
||||
|
||||
kotlin {
|
||||
compilerOptions {
|
||||
jvmTarget.set(JvmTarget.JVM_17)
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(libs.androidx.core.ktx)
|
||||
implementation(libs.androidx.lifecycle.runtime.ktx)
|
||||
@@ -54,6 +63,7 @@ dependencies {
|
||||
implementation(libs.androidx.ui.graphics)
|
||||
implementation(libs.androidx.ui.tooling.preview)
|
||||
implementation(libs.androidx.material3)
|
||||
implementation(libs.androidx.compose.ui.unit)
|
||||
testImplementation(libs.junit)
|
||||
androidTestImplementation(libs.androidx.junit)
|
||||
androidTestImplementation(libs.androidx.espresso.core)
|
||||
@@ -62,14 +72,37 @@ dependencies {
|
||||
debugImplementation(libs.androidx.ui.tooling)
|
||||
debugImplementation(libs.androidx.ui.test.manifest)
|
||||
|
||||
// Trixnity - Matrix wrapper
|
||||
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
|
||||
implementation(libs.ktor.client.core)
|
||||
implementation(libs.ktor.client.cio)
|
||||
implementation(libs.ktor.client.okhttp)
|
||||
implementation(libs.ktor.client.logging)
|
||||
|
||||
// Coil - image loader
|
||||
implementation(libs.coil.compose)
|
||||
implementation(libs.coil.network.okhttp)
|
||||
|
||||
// Room - database
|
||||
implementation(libs.androidx.room.runtime)
|
||||
implementation(libs.androidx.lifecycle.viewmodel.ktx)
|
||||
implementation(libs.androidx.room.ktx)
|
||||
// ksp(libs.androidx.room.compiler)
|
||||
|
||||
// Navigation Compose
|
||||
implementation(libs.androidx.navigation.compose)
|
||||
implementation(libs.androidx.navigation.fragment)
|
||||
implementation(libs.androidx.navigation.ui)
|
||||
implementation(libs.androidx.navigation.dynamic.features.fragment)
|
||||
androidTestImplementation(libs.androidx.navigation.testing)
|
||||
|
||||
// Others
|
||||
implementation(libs.splitties.base) // Syntax sugar
|
||||
implementation(libs.jsoup) // HTML parser
|
||||
implementation(libs.iconsax.compose) // Material icons
|
||||
}
|
||||
@@ -1,8 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
~ Created by sweetbread on 21.02.2025, 12:08
|
||||
~ Created by sweetbread
|
||||
~ Copyright (c) 2025. All rights reserved.
|
||||
~ Last modified 21.02.2025, 12:07
|
||||
-->
|
||||
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
@@ -20,15 +19,9 @@
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.PixelDragon"
|
||||
tools:targetApi="31">
|
||||
<activity
|
||||
android:name=".ui.activity.Login"
|
||||
android:exported="false"
|
||||
android:label="@string/title_activity_login"
|
||||
android:theme="@style/Theme.PixelDragon" />
|
||||
<activity
|
||||
android:name=".ui.activity.MainActivity"
|
||||
android:exported="true"
|
||||
android:label="@string/app_name"
|
||||
android:theme="@style/Theme.PixelDragon">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
@@ -36,6 +29,11 @@
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name=".ui.activity.Login"
|
||||
android:exported="false"
|
||||
android:theme="@style/Theme.PixelDragon" />
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
@@ -1,43 +1,29 @@
|
||||
/*
|
||||
* Created by sweetbread on 21.02.2025, 12:01
|
||||
* Copyright (c) 2025. All rights reserved.
|
||||
* Last modified 21.02.2025, 12:01
|
||||
* Created by sweetbread
|
||||
* Copyright (c) 2026. All rights reserved.
|
||||
*/
|
||||
|
||||
package ru.risdeveau.pixeldragon
|
||||
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import de.connect2x.trixnity.client.MatrixClient
|
||||
import io.ktor.client.HttpClient
|
||||
import io.ktor.client.engine.cio.CIO
|
||||
import io.ktor.client.engine.cio.endpoint
|
||||
import io.ktor.client.plugins.cache.HttpCache
|
||||
import ru.risdeveau.pixeldragon.api.getMe
|
||||
import splitties.init.appCtx
|
||||
import io.ktor.client.plugins.logging.LogLevel
|
||||
import io.ktor.client.plugins.logging.Logger
|
||||
import io.ktor.client.plugins.logging.Logging
|
||||
|
||||
val client = HttpClient(CIO) {
|
||||
engine {
|
||||
endpoint {
|
||||
maxConnectionsPerRoute = 100
|
||||
pipelineMaxSize = 20
|
||||
keepAliveTime = 30000
|
||||
connectTimeout = 15000
|
||||
connectAttempts = 5
|
||||
val webClient = HttpClient {
|
||||
install(Logging) {
|
||||
logger = object : Logger {
|
||||
override fun log(message: String) {
|
||||
Log.i("Ktor", message)
|
||||
}
|
||||
}
|
||||
level = LogLevel.ALL
|
||||
}
|
||||
|
||||
install(HttpCache)
|
||||
}
|
||||
|
||||
val accountData = appCtx.getSharedPreferences("settings", Context.MODE_PRIVATE)
|
||||
lateinit var urlBase: String
|
||||
lateinit var token: String
|
||||
|
||||
suspend fun initCheck(): Boolean {
|
||||
if (!accountData.contains("token")) return false
|
||||
if (!accountData.contains("homeserver")) return false
|
||||
|
||||
token = accountData.getString("token", "").toString()
|
||||
urlBase = "https://${accountData.getString("homeserver", "")}/_matrix/client/v3"
|
||||
|
||||
return getMe() != null
|
||||
}
|
||||
var client: MatrixClient? = null
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
* Created by sweetbread
|
||||
* Copyright (c) 2026. All rights reserved.
|
||||
*/
|
||||
|
||||
package ru.risdeveau.pixeldragon.api
|
||||
|
||||
import org.json.JSONObject
|
||||
import java.time.Instant
|
||||
|
||||
class Event (
|
||||
val id: String,
|
||||
val rid: String,
|
||||
val type: String,
|
||||
val content: JSONObject,
|
||||
val time: Instant,
|
||||
val sender: String
|
||||
) {
|
||||
// constructor(json: JSONObject) : this(
|
||||
// json.getString("event_id"),
|
||||
// json.getString("room_id"),
|
||||
// json.getString("sender"),
|
||||
// json.getString("type"),
|
||||
// json.getJSONObject("content")
|
||||
// )
|
||||
}
|
||||
|
||||
//data class EventsAround (
|
||||
// val base: Event,
|
||||
// val before: List<Event>,
|
||||
// val after: List<Event>
|
||||
//)
|
||||
//
|
||||
//suspend fun getEventsAround(room: String, event: String): EventsAround {
|
||||
// val r = client.get("$baseUrl/rooms/$room/context/$event") {
|
||||
// bearerAuth(token)
|
||||
// parameter("limit", "50")
|
||||
// }
|
||||
// val json = JSONObject(r.bodyAsText())
|
||||
//
|
||||
// return EventsAround(
|
||||
// Event(json.getJSONObject("event")),
|
||||
// if (json.has("events_before")) json.getJSONArray("events_before").let {
|
||||
// List<Event>(it.length()) { i -> Event(it.getJSONObject(i))}.reversed()
|
||||
// } else listOf(),
|
||||
// if (json.has("events_after")) json.getJSONArray("events_after").let {
|
||||
// List<Event>(it.length()) { i -> Event(it.getJSONObject(i))}
|
||||
// } else listOf()
|
||||
// )
|
||||
//}
|
||||
@@ -0,0 +1,4 @@
|
||||
/*
|
||||
* Created by sweetbread
|
||||
* Copyright (c) 2026. All rights reserved.
|
||||
*/
|
||||
@@ -1,23 +1,44 @@
|
||||
/*
|
||||
* Created by sweetbread on 21.02.2025, 12:01
|
||||
* Copyright (c) 2025. All rights reserved.
|
||||
* Last modified 21.02.2025, 12:01
|
||||
* Created by sweetbread
|
||||
* Copyright (c) 2026. All rights reserved.
|
||||
*/
|
||||
|
||||
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
|
||||
import org.json.JSONObject
|
||||
import ru.risdeveau.pixeldragon.client
|
||||
import ru.risdeveau.pixeldragon.webClient
|
||||
//import ru.risdeveau.pixeldragon.homeserver
|
||||
|
||||
suspend fun isMatrixServer(url: String): Boolean {
|
||||
val r = try { client.get("https://$url/.well-known/matrix/client") }
|
||||
catch (_: Exception) { return false }
|
||||
suspend fun getHomeserver(url: String): String? {
|
||||
val r = try { webClient.get("https://$url/.well-known/matrix/client") }
|
||||
catch (e: Exception) {
|
||||
Log.w("getHomeserver", "Fail sending the request", e)
|
||||
return null
|
||||
}
|
||||
|
||||
try { JSONObject(r.bodyAsText()) }
|
||||
catch (_: JSONException) { return false }
|
||||
val json = try { JSONObject(r.bodyAsText()) }
|
||||
catch (e: JSONException) {
|
||||
Log.w("getHomeserver", "Fail parsing the JSON", e)
|
||||
return null
|
||||
}
|
||||
|
||||
return true
|
||||
if (!json.has("m.homeserver")) return null
|
||||
|
||||
var homeserver = json.getJSONObject("m.homeserver").getString("base_url")
|
||||
if (homeserver.endsWith("/")) homeserver = homeserver.dropLast(1)
|
||||
|
||||
return homeserver
|
||||
}
|
||||
|
||||
//fun mxcToUrl(mxc: String): String? {
|
||||
// val pattern = Regex("mxc://([-a-zA-Z0-9@:%._+~#=]{1,256}\\.[a-zA-Z0-9()]{1,6})/([^#?]+)")
|
||||
// val match = pattern.find(mxc)
|
||||
//
|
||||
// if ((match?.groupValues[1] == null) or (match?.groupValues[2] == null)) return null
|
||||
//
|
||||
// return "$homeserver/_matrix/client/v1/media/download/${match?.groupValues[1]}/${match?.groupValues[2]}"
|
||||
//}
|
||||
@@ -1,82 +1,49 @@
|
||||
/*
|
||||
* Created by sweetbread on 21.02.2025, 12:09
|
||||
* Copyright (c) 2025. All rights reserved.
|
||||
* Last modified 21.02.2025, 12:01
|
||||
* Created by sweetbread
|
||||
* Copyright (c) 2026. All rights reserved.
|
||||
*/
|
||||
|
||||
package ru.risdeveau.pixeldragon.api
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.util.Log
|
||||
import io.ktor.client.request.bearerAuth
|
||||
import io.ktor.client.request.get
|
||||
import io.ktor.client.request.post
|
||||
import io.ktor.client.request.setBody
|
||||
import io.ktor.client.statement.bodyAsText
|
||||
import io.ktor.http.ContentType
|
||||
import io.ktor.http.HttpStatusCode
|
||||
import io.ktor.http.contentType
|
||||
import org.json.JSONObject
|
||||
import ru.risdeveau.pixeldragon.accountData
|
||||
import de.connect2x.trixnity.client.CryptoDriverModule
|
||||
import io.ktor.http.Url
|
||||
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.initCheck
|
||||
import ru.risdeveau.pixeldragon.token
|
||||
import ru.risdeveau.pixeldragon.urlBase
|
||||
import ru.risdeveau.pixeldragon.util.getMediaStore
|
||||
import ru.risdeveau.pixeldragon.util.getRoomStore
|
||||
import splitties.experimental.ExperimentalSplittiesApi
|
||||
import splitties.init.appCtx
|
||||
|
||||
data class Me (val userId: String, val deviceId: String)
|
||||
|
||||
/**
|
||||
* This func is to validate the token
|
||||
*/
|
||||
suspend fun getMe(): Me? {
|
||||
val r = client.get("$urlBase/account/whoami") { bearerAuth(token) }
|
||||
if (r.status != HttpStatusCode.OK) {
|
||||
Log.e("getMe", r.bodyAsText())
|
||||
return null
|
||||
}
|
||||
@OptIn(ExperimentalSplittiesApi::class)
|
||||
suspend fun login(server: String, login: String, pass: String): Boolean {
|
||||
val hs = Url(getHomeserver(server)!!)
|
||||
|
||||
val json = JSONObject(r.bodyAsText())
|
||||
return Me(json.getString("user_id"), json.getString("device_id"))
|
||||
}
|
||||
val pinfo = appCtx.packageManager.getPackageInfo(appCtx.packageName, 0)
|
||||
|
||||
@SuppressLint("ApplySharedPref")
|
||||
suspend fun login(homeserver: String, login: String, pass: String): Boolean {
|
||||
val pinfo = appCtx.packageManager.getPackageInfo(appCtx.packageName, 0);
|
||||
try {
|
||||
client = MatrixClient.create(
|
||||
repositoriesModule = getRoomStore(appCtx),
|
||||
mediaStoreModule = getMediaStore(),
|
||||
cryptoDriverModule = CryptoDriverModule.vodozemac(),
|
||||
authProviderData = MatrixClientAuthProviderData.classicLogin(
|
||||
baseUrl = hs,
|
||||
identifier = IdentifierType.User(login),
|
||||
password = pass,
|
||||
initialDeviceDisplayName = "PixelDragon Android v${pinfo.versionName}",
|
||||
).getOrThrow()
|
||||
).getOrThrow()
|
||||
|
||||
val pattern = """
|
||||
{
|
||||
"type": "m.login.password",
|
||||
"identifier": {
|
||||
"type": "m.id.user"
|
||||
},
|
||||
"initial_device_display_name": "PixelDragon Android v${pinfo.versionName}"
|
||||
}
|
||||
""".trimIndent()
|
||||
val json = JSONObject(pattern)
|
||||
json.getJSONObject("identifier").put("user", login)
|
||||
json.put("password", pass)
|
||||
|
||||
val r = try {
|
||||
client.post("https://$homeserver/_matrix/client/v3/login") {
|
||||
setBody(json.toString())
|
||||
contentType(ContentType.Application.Json)
|
||||
}
|
||||
return true
|
||||
} catch (e: Exception) {
|
||||
Log.e("login", e.toString())
|
||||
Log.i("Login", "Failed to login", e)
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
if (r.status != HttpStatusCode.OK) {
|
||||
Log.e("login", r.bodyAsText())
|
||||
return false // TODO: Inform a user of error code
|
||||
}
|
||||
|
||||
val res = JSONObject(r.bodyAsText())
|
||||
val editor = accountData.edit()
|
||||
editor.putString("token", res.getString("access_token"))
|
||||
editor.putString("homeserver", res.getString("home_server"))
|
||||
editor.commit()
|
||||
|
||||
return initCheck()
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
/*
|
||||
* Created by sweetbread
|
||||
* Copyright (c) 2026. All rights reserved.
|
||||
*/
|
||||
@@ -0,0 +1,4 @@
|
||||
/*
|
||||
* Created by sweetbread
|
||||
* Copyright (c) 2026. All rights reserved.
|
||||
*/
|
||||
@@ -0,0 +1,4 @@
|
||||
/*
|
||||
* Created by sweetbread
|
||||
* Copyright (c) 2026. All rights reserved.
|
||||
*/
|
||||
@@ -0,0 +1,4 @@
|
||||
/*
|
||||
* Created by sweetbread
|
||||
* Copyright (c) 2026. All rights reserved.
|
||||
*/
|
||||
@@ -0,0 +1,4 @@
|
||||
/*
|
||||
* Created by sweetbread
|
||||
* Copyright (c) 2026. All rights reserved.
|
||||
*/
|
||||
@@ -0,0 +1,4 @@
|
||||
/*
|
||||
* Created by sweetbread
|
||||
* Copyright (c) 2026. All rights reserved.
|
||||
*/
|
||||
@@ -1,7 +1,6 @@
|
||||
/*
|
||||
* Created by sweetbread on 21.02.2025, 12:08
|
||||
* Created by sweetbread
|
||||
* Copyright (c) 2025. All rights reserved.
|
||||
* Last modified 21.02.2025, 12:08
|
||||
*/
|
||||
|
||||
package ru.risdeveau.pixeldragon.ui.activity
|
||||
@@ -35,15 +34,18 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
||||
import androidx.compose.ui.unit.dp
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.launch
|
||||
import ru.risdeveau.pixeldragon.api.isMatrixServer
|
||||
import ru.risdeveau.pixeldragon.api.getHomeserver
|
||||
import ru.risdeveau.pixeldragon.api.login
|
||||
import ru.risdeveau.pixeldragon.ui.theme.PixelDragonTheme
|
||||
import splitties.activities.start
|
||||
|
||||
class Login : ComponentActivity() {
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
enableEdgeToEdge()
|
||||
setContent {
|
||||
PixelDragonTheme {
|
||||
@@ -56,10 +58,16 @@ class Login : ComponentActivity() {
|
||||
SnackbarHost(hostState = snackbarHostState)
|
||||
}
|
||||
) { innerPadding ->
|
||||
Box(Modifier.fillMaxSize().padding(innerPadding)) {
|
||||
Box(
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.padding(innerPadding)) {
|
||||
LoginField(
|
||||
Modifier.align(Alignment.Center),
|
||||
{ start<MainActivity>() },
|
||||
{
|
||||
start<MainActivity>()
|
||||
finish()
|
||||
},
|
||||
{
|
||||
scope.launch {
|
||||
snackbarHostState
|
||||
@@ -89,7 +97,7 @@ fun LoginField(modifier: Modifier = Modifier, ok: () -> Unit, err: () -> Unit) {
|
||||
var login by remember { mutableStateOf("") }
|
||||
var pass by remember { mutableStateOf("") }
|
||||
|
||||
var hmsValid by remember { mutableStateOf(false) }
|
||||
var hmsValid: Boolean by remember { mutableStateOf(false) }
|
||||
|
||||
OutlinedTextField(
|
||||
modifier = Modifier.padding(4.dp),
|
||||
@@ -119,18 +127,17 @@ fun LoginField(modifier: Modifier = Modifier, ok: () -> Unit, err: () -> Unit) {
|
||||
Button(
|
||||
enabled = hmsValid && !login.isEmpty() && !pass.isEmpty(),
|
||||
onClick = {
|
||||
var loginSuccess = false
|
||||
scope.launch { loginSuccess = login(homeserver, login, pass) }
|
||||
|
||||
if (loginSuccess) ok()
|
||||
scope.launch {
|
||||
if (login(homeserver, login, pass)) ok()
|
||||
else err()
|
||||
}
|
||||
}
|
||||
) {
|
||||
Text("Login")
|
||||
}
|
||||
|
||||
LaunchedEffect(homeserver) {
|
||||
scope.launch { hmsValid = isMatrixServer(homeserver) }
|
||||
scope.launch { hmsValid = (getHomeserver(homeserver) != null) }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,81 +1,319 @@
|
||||
/*
|
||||
* Created by sweetbread on 21.02.2025, 12:08
|
||||
* Copyright (c) 2025. All rights reserved.
|
||||
* Last modified 21.02.2025, 12:07
|
||||
* Created by sweetbread
|
||||
* Copyright (c) 2026. All rights reserved.
|
||||
*/
|
||||
|
||||
package ru.risdeveau.pixeldragon.ui.activity
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.CenterAlignedTopAppBar
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.material3.TopAppBarDefaults.topAppBarColors
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.navigation.NavHostController
|
||||
import androidx.navigation.NavType
|
||||
import androidx.navigation.compose.NavHost
|
||||
import androidx.navigation.compose.composable
|
||||
import androidx.navigation.compose.currentBackStackEntryAsState
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import androidx.navigation.navArgument
|
||||
import de.connect2x.trixnity.client.CryptoDriverModule
|
||||
import de.connect2x.trixnity.client.MatrixClient
|
||||
import de.connect2x.trixnity.client.create
|
||||
import de.connect2x.trixnity.client.cryptodriver.vodozemac.vodozemac
|
||||
import de.connect2x.trixnity.client.flattenValues
|
||||
import de.connect2x.trixnity.client.room
|
||||
import de.connect2x.trixnity.client.store.type
|
||||
import de.connect2x.trixnity.clientserverapi.client.SyncState
|
||||
import de.connect2x.trixnity.clientserverapi.model.user.avatarUrl
|
||||
import de.connect2x.trixnity.clientserverapi.model.user.displayName
|
||||
import de.connect2x.trixnity.core.model.events.m.room.CreateEventContent
|
||||
import io.github.rabehx.iconsax.Iconsax
|
||||
import io.github.rabehx.iconsax.automirrored.outline.ArrowLeft2
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.launch
|
||||
import ru.risdeveau.pixeldragon.initCheck
|
||||
import ru.risdeveau.pixeldragon.R
|
||||
import ru.risdeveau.pixeldragon.client
|
||||
import ru.risdeveau.pixeldragon.ui.item.Avatar
|
||||
import ru.risdeveau.pixeldragon.ui.layout.Room
|
||||
import ru.risdeveau.pixeldragon.ui.layout.RoomList
|
||||
import ru.risdeveau.pixeldragon.ui.theme.PixelDragonTheme
|
||||
import ru.risdeveau.pixeldragon.util.getMediaStore
|
||||
import ru.risdeveau.pixeldragon.util.getRoomStore
|
||||
import splitties.activities.start
|
||||
import splitties.init.appCtx
|
||||
import de.connect2x.trixnity.client.store.Room as TrixnityRoom
|
||||
|
||||
|
||||
class MainActivity : ComponentActivity() {
|
||||
@OptIn(ExperimentalMaterial3Api::class, DelicateCoroutinesApi::class)
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
GlobalScope.launch {
|
||||
if (!initCheck()) start<Login>()
|
||||
}
|
||||
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
enableEdgeToEdge()
|
||||
setContent {
|
||||
PixelDragonTheme {
|
||||
var isClientReady by remember { mutableStateOf(false) }
|
||||
|
||||
if (!isClientReady || client == null) {
|
||||
Box(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
CircularProgressIndicator()
|
||||
}
|
||||
} else {
|
||||
val navController = rememberNavController()
|
||||
val syncState by client!!.api.sync.currentSyncState
|
||||
.collectAsState(initial = SyncState.STOPPED)
|
||||
|
||||
Scaffold(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
PixelDragonTopBar(
|
||||
navController = navController,
|
||||
syncState = syncState,
|
||||
)
|
||||
},
|
||||
) { innerPadding ->
|
||||
NavHost(navController = navController, startDestination = "rooms") {
|
||||
composable("rooms") {
|
||||
RoomList(Modifier.padding(innerPadding), navController)
|
||||
}
|
||||
composable(
|
||||
"room/{rid}",
|
||||
arguments = listOf(navArgument("rid") { type = NavType.StringType })
|
||||
) { navBackStackEntry ->
|
||||
Room(Modifier
|
||||
.padding(innerPadding)
|
||||
.fillMaxSize(), navBackStackEntry.arguments!!.getString("rid")!!)
|
||||
}
|
||||
composable(
|
||||
"space/{rid}",
|
||||
arguments = listOf(navArgument("rid") { type = NavType.StringType })
|
||||
) {
|
||||
Text(
|
||||
modifier = Modifier.padding(innerPadding),
|
||||
text = "Not implemented"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
if (client == null) {
|
||||
client = MatrixClient.create(
|
||||
repositoriesModule = getRoomStore(appCtx),
|
||||
mediaStoreModule = getMediaStore(),
|
||||
cryptoDriverModule = CryptoDriverModule.vodozemac()
|
||||
).getOrNull()
|
||||
|
||||
if (client == null) {
|
||||
start<Login>()
|
||||
finish()
|
||||
return@LaunchedEffect
|
||||
}
|
||||
}
|
||||
|
||||
Log.i("MainActivity", "Log in as ${client!!.userId}")
|
||||
client!!.startSync()
|
||||
isClientReady = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
CoroutineScope(Dispatchers.Main).launch {
|
||||
client?.stopSync()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("FlowOperatorInvokedInComposition")
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun PixelDragonTopBar(
|
||||
navController: NavHostController,
|
||||
syncState: SyncState,
|
||||
) {
|
||||
val backStackEntry by navController.currentBackStackEntryAsState()
|
||||
val route = backStackEntry?.destination?.route
|
||||
val rid = backStackEntry?.arguments?.getString("rid")
|
||||
val isRoomLikeScreen = route == "room/{rid}" || route == "space/{rid}"
|
||||
|
||||
val roomsFlow = remember(client) {
|
||||
client!!.room.getAll().flattenValues().map { it.toList() }
|
||||
}
|
||||
val rooms by roomsFlow.collectAsState(initial = emptyList())
|
||||
|
||||
val currentRoom = remember(isRoomLikeScreen, rid, rooms) {
|
||||
if (isRoomLikeScreen && rid != null) {
|
||||
rooms.firstOrNull { it.roomId.toString() == rid }
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
if (isRoomLikeScreen) {
|
||||
RoomTopBar(
|
||||
room = currentRoom,
|
||||
fallbackTitle = rid ?: stringResource(R.string.app_name),
|
||||
onBack = { navController.popBackStack() },
|
||||
)
|
||||
} else {
|
||||
HomeTopBar(syncState = syncState)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun HomeTopBar(syncState: SyncState) {
|
||||
CenterAlignedTopAppBar(
|
||||
colors = topAppBarColors(
|
||||
containerColor = MaterialTheme.colorScheme.primaryContainer,
|
||||
titleContentColor = MaterialTheme.colorScheme.primary,
|
||||
),
|
||||
title = {
|
||||
Text("Top app bar")
|
||||
}
|
||||
)
|
||||
Text(syncState.toStatusTitle() ?: stringResource(R.string.app_name))
|
||||
},
|
||||
) { innerPadding ->
|
||||
Greeting(
|
||||
name = "Android",
|
||||
modifier = Modifier.padding(innerPadding)
|
||||
actions = {
|
||||
val client = client!!
|
||||
|
||||
var userName by remember { mutableStateOf("?") }
|
||||
var userAvatar: String? by remember { mutableStateOf(null) }
|
||||
|
||||
LaunchedEffect(client, syncState) {
|
||||
val profile = client.api.user
|
||||
.getProfile(client.userId)
|
||||
.getOrNull()
|
||||
|
||||
userName = profile?.displayName ?: "?"
|
||||
userAvatar = profile?.avatarUrl
|
||||
}
|
||||
|
||||
Avatar(
|
||||
Modifier.size(32.dp).clip(CircleShape),
|
||||
userAvatar,
|
||||
userName,
|
||||
MaterialTheme.colorScheme.background
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun Greeting(name: String, modifier: Modifier = Modifier) {
|
||||
private fun RoomTopBar(
|
||||
room: TrixnityRoom?,
|
||||
fallbackTitle: String,
|
||||
onBack: () -> Unit,
|
||||
) {
|
||||
CenterAlignedTopAppBar(
|
||||
colors = topAppBarColors(
|
||||
containerColor = MaterialTheme.colorScheme.surface.copy(.5f),
|
||||
titleContentColor = MaterialTheme.colorScheme.onSurface,
|
||||
navigationIconContentColor = MaterialTheme.colorScheme.primary,
|
||||
),
|
||||
navigationIcon = {
|
||||
IconButton(onClick = onBack) {
|
||||
Icon(
|
||||
Iconsax.AutoMirrored.Outline.ArrowLeft2,
|
||||
"To home",
|
||||
)
|
||||
}
|
||||
},
|
||||
title = {
|
||||
if (room != null) {
|
||||
RoomTopBarTitle(room)
|
||||
} else {
|
||||
Text(
|
||||
text = "Hello $name!",
|
||||
modifier = modifier
|
||||
text = fallbackTitle,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
fun GreetingPreview() {
|
||||
PixelDragonTheme {
|
||||
Greeting("Android")
|
||||
private fun RoomTopBarTitle(room: TrixnityRoom) {
|
||||
val title = remember(room) { room.displayName() }
|
||||
val isSpace = room.type == CreateEventContent.RoomType.Space
|
||||
val avatarShape = if (isSpace) RoundedCornerShape(8.dp) else CircleShape
|
||||
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Avatar(
|
||||
Modifier
|
||||
.size(36.dp)
|
||||
.clip(avatarShape),
|
||||
room.avatarUrl,
|
||||
title
|
||||
)
|
||||
|
||||
Spacer(Modifier.width(10.dp))
|
||||
|
||||
Text(
|
||||
text = title,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun TrixnityRoom.displayName(): String {
|
||||
return name?.explicitName
|
||||
?: name?.heroes?.firstNotNullOfOrNull { it.localpart }
|
||||
?: roomId.toString()
|
||||
}
|
||||
|
||||
private fun SyncState.toStatusTitle(): String? {
|
||||
return when (this) {
|
||||
SyncState.INITIAL_SYNC -> "Initial sync..."
|
||||
SyncState.STARTED -> "Syncing..."
|
||||
SyncState.TIMEOUT -> "No network connection"
|
||||
SyncState.ERROR -> "Error syncing"
|
||||
SyncState.RUNNING,
|
||||
SyncState.STOPPED -> null
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
/*
|
||||
* Created by sweetbread
|
||||
* Copyright (c) 2026. All rights reserved.
|
||||
*/
|
||||
|
||||
package ru.risdeveau.pixeldragon.ui.item
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
|
||||
@Composable
|
||||
fun Avatar(
|
||||
modifier: Modifier,
|
||||
url: String?,
|
||||
fallbackName: String,
|
||||
fallbackColor: Color = MaterialTheme.colorScheme.secondaryContainer,
|
||||
) {
|
||||
if (url != null) {
|
||||
MXCImage(
|
||||
mxcUrl = url,
|
||||
modifier = modifier,
|
||||
contentScale = ContentScale.Crop,
|
||||
contentDescription = fallbackName,
|
||||
showProgress = false,
|
||||
)
|
||||
} else {
|
||||
AvatarPlaceholder(
|
||||
modifier,
|
||||
fallbackName,
|
||||
fallbackColor,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun AvatarPlaceholder(
|
||||
modifier: Modifier = Modifier,
|
||||
fallbackName: String,
|
||||
fallbackColor: Color,
|
||||
) {
|
||||
val initial = fallbackName
|
||||
.trim()
|
||||
.firstOrNull()
|
||||
?.uppercaseChar()
|
||||
?.toString()
|
||||
?: "?"
|
||||
|
||||
Box(
|
||||
modifier = modifier.background(fallbackColor),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Text(
|
||||
text = initial,
|
||||
color = MaterialTheme.colorScheme.onSecondaryContainer,
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
)
|
||||
}
|
||||
}
|
||||
+129
@@ -0,0 +1,129 @@
|
||||
/*
|
||||
* Created by sweetbread
|
||||
* Copyright (c) 2026. 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 io.github.rabehx.iconsax.Iconsax
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import coil3.compose.rememberAsyncImagePainter
|
||||
import coil3.request.ImageRequest
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import de.connect2x.trixnity.client.media
|
||||
import de.connect2x.trixnity.clientserverapi.model.media.FileTransferProgress
|
||||
import io.github.rabehx.iconsax.outline.Warning2
|
||||
import ru.risdeveau.pixeldragon.client
|
||||
|
||||
enum class ImageLoadState {
|
||||
Loading, Success, Error
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun MXCImage(
|
||||
mxcUrl: String,
|
||||
modifier: Modifier = Modifier,
|
||||
contentScale: ContentScale = ContentScale.Fit,
|
||||
contentDescription: String = "",
|
||||
showProgress: Boolean = true
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
var imageLoadState by remember { mutableStateOf(ImageLoadState.Loading) }
|
||||
var imageBytes by remember { mutableStateOf<ByteArray?>(null) }
|
||||
|
||||
val progressFlow = remember { MutableStateFlow<FileTransferProgress?>(null) }
|
||||
|
||||
LaunchedEffect(mxcUrl) {
|
||||
if (mxcUrl.isBlank()) {
|
||||
imageLoadState = ImageLoadState.Error
|
||||
return@LaunchedEffect
|
||||
}
|
||||
imageLoadState = ImageLoadState.Loading
|
||||
progressFlow.value = null
|
||||
|
||||
val result = client!!.media.getMedia(
|
||||
uri = mxcUrl,
|
||||
progress = progressFlow.takeIf { showProgress },
|
||||
saveToCache = true
|
||||
)
|
||||
|
||||
imageLoadState = result.fold(
|
||||
onSuccess = { media ->
|
||||
val bytes = media.toByteArray()
|
||||
if (bytes != null) {
|
||||
imageBytes = bytes
|
||||
ImageLoadState.Success
|
||||
} else {
|
||||
ImageLoadState.Error
|
||||
}
|
||||
},
|
||||
onFailure = {
|
||||
ImageLoadState.Error
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
val progress by progressFlow.collectAsState()
|
||||
|
||||
val showProgressIndicator = showProgress &&
|
||||
imageLoadState == ImageLoadState.Loading &&
|
||||
progress != null
|
||||
|
||||
val painter = rememberAsyncImagePainter(
|
||||
model = ImageRequest.Builder(context)
|
||||
.data(imageBytes)
|
||||
.build()
|
||||
)
|
||||
|
||||
Box(modifier = modifier, contentAlignment = Alignment.Center) {
|
||||
if (imageBytes != null) {
|
||||
Image(
|
||||
painter = painter,
|
||||
contentDescription = contentDescription,
|
||||
contentScale = contentScale,
|
||||
modifier = Modifier.fillMaxSize()
|
||||
)
|
||||
}
|
||||
|
||||
when {
|
||||
showProgressIndicator -> {
|
||||
progress?.let { p ->
|
||||
val percent = p.total?.let { p.transferred.toFloat() / it }
|
||||
if (percent == null) {
|
||||
CircularProgressIndicator(modifier = Modifier.align(Alignment.Center))
|
||||
} else {
|
||||
CircularProgressIndicator(
|
||||
progress = percent,
|
||||
modifier = Modifier.align(Alignment.Center)
|
||||
)
|
||||
}
|
||||
} ?: CircularProgressIndicator(modifier = Modifier.align(Alignment.Center))
|
||||
}
|
||||
imageLoadState == ImageLoadState.Loading -> {
|
||||
CircularProgressIndicator(modifier = Modifier.align(Alignment.Center))
|
||||
}
|
||||
imageLoadState == ImageLoadState.Error -> {
|
||||
Icon(
|
||||
Iconsax.Outline.Warning2,
|
||||
contentDescription = "Error",
|
||||
modifier = Modifier.align(Alignment.Center)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+1775
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,128 @@
|
||||
/*
|
||||
* Created by sweetbread
|
||||
* Copyright (c) 2026. All rights reserved.
|
||||
*/
|
||||
|
||||
package ru.risdeveau.pixeldragon.ui.layout
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.util.Log
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.navigation.NavController
|
||||
import kotlinx.coroutines.flow.map
|
||||
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
|
||||
|
||||
@SuppressLint("FlowOperatorInvokedInComposition")
|
||||
@Composable
|
||||
fun RoomList(modifier: Modifier = Modifier, navController: NavController) {
|
||||
val rooms by client!!.room.getAll().flattenValues().map { it.toList() }.collectAsState(initial = emptyList())
|
||||
|
||||
val listState = rememberLazyListState()
|
||||
|
||||
// if (itemState.scrollToTop) {
|
||||
// LaunchedEffect(coroutineScope) {
|
||||
// Log.e("TAG", "TopCoinsScreen: scrollToTop" )
|
||||
// listState.scrollToItem(0)
|
||||
// }
|
||||
// }
|
||||
|
||||
// LaunchedEffect(Unit) {
|
||||
// list = withContext(Dispatchers.IO) { Room.getJoined() }
|
||||
// }
|
||||
|
||||
LazyColumn(modifier = modifier, state = listState) {
|
||||
items(rooms) { room ->
|
||||
RoomItem(room = room, navController = navController )
|
||||
}
|
||||
|
||||
item {
|
||||
if (rooms.isEmpty()) {
|
||||
Text("You have no rooms")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun RoomItem(modifier: Modifier = Modifier, room: Room, navController: NavController) {
|
||||
val name = room.name
|
||||
val isSpace = room.type == CreateEventContent.RoomType.Space
|
||||
|
||||
|
||||
Row(
|
||||
modifier
|
||||
.padding(8.dp)
|
||||
.height((52 + 8 * 2).dp)
|
||||
.fillMaxWidth()
|
||||
.background(
|
||||
color =
|
||||
if (isSpace)
|
||||
MaterialTheme.colorScheme.tertiary
|
||||
else
|
||||
MaterialTheme.colorScheme.background
|
||||
)
|
||||
.clip(RoundedCornerShape(12.dp))
|
||||
.clickable {
|
||||
if (isSpace)
|
||||
navController.navigate("space/${room.roomId}")
|
||||
else
|
||||
navController.navigate("room/${room.roomId}")
|
||||
}
|
||||
.padding(8.dp)
|
||||
) {
|
||||
Log.v("RoomItem", room.avatarUrl.toString())
|
||||
room.avatarUrl?.let { mxc ->
|
||||
MXCImage(
|
||||
mxc,
|
||||
modifier = Modifier
|
||||
.padding(end = 4.dp)
|
||||
.height(52.dp)
|
||||
.width(52.dp)
|
||||
.let {
|
||||
if (isSpace)
|
||||
it.clip(RoundedCornerShape(12.dp))
|
||||
else
|
||||
it.clip(CircleShape)
|
||||
},
|
||||
ContentScale.Crop
|
||||
)
|
||||
}
|
||||
Column {
|
||||
(name?.explicitName ?: name?.heroes?.firstNotNullOf {it.localpart})?.let {
|
||||
Text(
|
||||
it,
|
||||
maxLines = 1,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
fontSize = MaterialTheme.typography.titleLarge.fontSize
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
* Created by sweetbread
|
||||
* Copyright (c) 2026. All rights reserved.
|
||||
*/
|
||||
|
||||
package ru.risdeveau.pixeldragon.util
|
||||
|
||||
import android.content.Context
|
||||
import androidx.room.Room
|
||||
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 splitties.init.appCtx
|
||||
|
||||
fun getMediaStore() = MediaStoreModule.okio(appCtx.filesDir.resolve("media").absolutePath.toPath())
|
||||
fun getRoomStore(context: Context) = RepositoriesModule.room(
|
||||
databaseBuilder = Room.databaseBuilder<TrixnityRoomDatabase>(
|
||||
context,
|
||||
"trixnity.db"
|
||||
)
|
||||
)
|
||||
+2
-3
@@ -1,7 +1,6 @@
|
||||
/*
|
||||
* Created by sweetbread on 21.02.2025, 12:00
|
||||
* Copyright (c) 2025. All rights reserved.
|
||||
* Last modified 21.02.2025, 12:00
|
||||
* Created by sweetbread
|
||||
* Copyright (c) 2026. All rights reserved.
|
||||
*/
|
||||
|
||||
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||
|
||||
@@ -1,21 +1,38 @@
|
||||
[versions]
|
||||
agp = "8.7.3"
|
||||
coil = "3.1.0"
|
||||
kotlin = "2.0.0"
|
||||
agp = "9.1.0"
|
||||
coil = "3.4.0"
|
||||
iconsaxCompose = "0.0.5"
|
||||
jsoup = "1.22.1"
|
||||
kotlin = "2.2.21"
|
||||
coreKtx = "1.15.0"
|
||||
junit = "4.13.2"
|
||||
junitVersion = "1.2.1"
|
||||
espressoCore = "3.6.1"
|
||||
ktor = "3.1.0"
|
||||
junitVersion = "1.3.0"
|
||||
espressoCore = "3.7.0"
|
||||
kotlinxSerializationJson = "1.7.3"
|
||||
ktor = "3.4.1"
|
||||
lifecycleRuntimeKtx = "2.8.7"
|
||||
activityCompose = "1.10.0"
|
||||
composeBom = "2025.02.00"
|
||||
composeBom = "2026.03.00"
|
||||
navigationCompose = "2.9.7"
|
||||
room = "2.6.1"
|
||||
splittiesFunPackAndroidBase = "3.0.0"
|
||||
trixnityClient = "5.2.0"
|
||||
uiUnit = "1.10.6"
|
||||
|
||||
[libraries]
|
||||
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
|
||||
androidx-lifecycle-viewmodel-ktx = { module = "androidx.lifecycle:lifecycle-viewmodel-ktx", version.ref = "room" }
|
||||
androidx-navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "navigationCompose" }
|
||||
androidx-navigation-dynamic-features-fragment = { module = "androidx.navigation:navigation-dynamic-features-fragment", version.ref = "navigationCompose" }
|
||||
androidx-navigation-fragment = { module = "androidx.navigation:navigation-fragment", version.ref = "navigationCompose" }
|
||||
androidx-navigation-testing = { module = "androidx.navigation:navigation-testing", version.ref = "navigationCompose" }
|
||||
androidx-navigation-ui = { module = "androidx.navigation:navigation-ui", version.ref = "navigationCompose" }
|
||||
androidx-room-ktx = { module = "androidx.room:room-ktx", version.ref = "room" }
|
||||
androidx-room-runtime = { module = "androidx.room:room-runtime", version.ref = "room" }
|
||||
coil-compose = { module = "io.coil-kt.coil3:coil-compose", version.ref = "coil" }
|
||||
coil-network-okhttp = { module = "io.coil-kt.coil3:coil-network-okhttp", version.ref = "coil" }
|
||||
iconsax-compose = { module = "io.github.rabehx:iconsax-compose", version.ref = "iconsaxCompose" }
|
||||
jsoup = { module = "org.jsoup:jsoup", version.ref = "jsoup" }
|
||||
junit = { group = "junit", name = "junit", version.ref = "junit" }
|
||||
androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
|
||||
androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
|
||||
@@ -29,12 +46,17 @@ androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-toolin
|
||||
androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
|
||||
androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
|
||||
androidx-material3 = { group = "androidx.compose.material3", name = "material3" }
|
||||
ktor-client-cio = { module = "io.ktor:ktor-client-cio", version.ref = "ktor" }
|
||||
ktor-client-okhttp = { module = "io.ktor:ktor-client-okhttp", version.ref = "ktor" }
|
||||
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 = "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" }
|
||||
androidx-compose-ui-unit = { group = "androidx.compose.ui", name = "ui-unit", version.ref = "uiUnit" }
|
||||
|
||||
[plugins]
|
||||
android-application = { id = "com.android.application", version.ref = "agp" }
|
||||
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
|
||||
kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
|
||||
|
||||
|
||||
+2
-3
@@ -1,12 +1,11 @@
|
||||
#
|
||||
# Created by sweetbread on 21.02.2025, 12:00
|
||||
# Created by sweetbread
|
||||
# Copyright (c) 2025. All rights reserved.
|
||||
# Last modified 21.02.2025, 12:00
|
||||
#
|
||||
|
||||
#Thu Feb 20 10:45:47 GMT 2025
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.1-bin.zip
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
|
||||
+6
-3
@@ -1,7 +1,6 @@
|
||||
/*
|
||||
* Created by sweetbread on 21.02.2025, 12:00
|
||||
* Copyright (c) 2025. All rights reserved.
|
||||
* Last modified 21.02.2025, 12:00
|
||||
* Created by sweetbread
|
||||
* Copyright (c) 2026. All rights reserved.
|
||||
*/
|
||||
|
||||
pluginManagement {
|
||||
@@ -17,11 +16,15 @@ pluginManagement {
|
||||
gradlePluginPortal()
|
||||
}
|
||||
}
|
||||
plugins {
|
||||
id("org.gradle.toolchains.foojay-resolver-convention") version "1.0.0"
|
||||
}
|
||||
dependencyResolutionManagement {
|
||||
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
maven("https://gitlab.com/api/v4/projects/47538655/packages/maven")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user