Compare commits
28 Commits
master
..
1cfad2ca4e
| Author | SHA1 | Date | |
|---|---|---|---|
| 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">
|
<component name="CopyrightManager">
|
||||||
<settings default="My">
|
<settings default="My" />
|
||||||
<module2copyright>
|
|
||||||
<element module="All" copyright="My" />
|
|
||||||
</module2copyright>
|
|
||||||
</settings>
|
|
||||||
</component>
|
</component>
|
||||||
Generated
+8
@@ -4,6 +4,14 @@
|
|||||||
<selectionStates>
|
<selectionStates>
|
||||||
<SelectionState runConfigName="app">
|
<SelectionState runConfigName="app">
|
||||||
<option name="selectionMode" value="DROPDOWN" />
|
<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>
|
</SelectionState>
|
||||||
</selectionStates>
|
</selectionStates>
|
||||||
</component>
|
</component>
|
||||||
|
|||||||
Generated
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<project version="4">
|
<project version="4">
|
||||||
<component name="KotlinJpsPluginSettings">
|
<component name="KotlinJpsPluginSettings">
|
||||||
<option name="version" value="2.0.0" />
|
<option name="version" value="2.0.21" />
|
||||||
</component>
|
</component>
|
||||||
</project>
|
</project>
|
||||||
Generated
+4
@@ -5,8 +5,12 @@
|
|||||||
<set>
|
<set>
|
||||||
<option value="com.intellij.execution.junit.AbstractAllInDirectoryConfigurationProducer" />
|
<option value="com.intellij.execution.junit.AbstractAllInDirectoryConfigurationProducer" />
|
||||||
<option value="com.intellij.execution.junit.AllInPackageConfigurationProducer" />
|
<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.TestInClassConfigurationProducer" />
|
||||||
<option value="com.intellij.execution.junit.UniqueIdConfigurationProducer" />
|
<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>
|
</set>
|
||||||
</option>
|
</option>
|
||||||
</component>
|
</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>
|
||||||
+24
-9
@@ -1,13 +1,13 @@
|
|||||||
/*
|
/*
|
||||||
* Created by sweetbread on 21.02.2025, 12:01
|
* Created by sweetbread
|
||||||
* Copyright (c) 2025. All rights reserved.
|
* Copyright (c) 2025. All rights reserved.
|
||||||
* Last modified 21.02.2025, 12:01
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
alias(libs.plugins.android.application)
|
alias(libs.plugins.android.application)
|
||||||
alias(libs.plugins.kotlin.android)
|
alias(libs.plugins.kotlin.android)
|
||||||
alias(libs.plugins.kotlin.compose)
|
alias(libs.plugins.kotlin.compose)
|
||||||
|
id("com.google.devtools.ksp")
|
||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
@@ -16,7 +16,7 @@ android {
|
|||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId = "ru.risdeveau.pixeldragon"
|
applicationId = "ru.risdeveau.pixeldragon"
|
||||||
minSdk = 24
|
minSdk = 26
|
||||||
targetSdk = 35
|
targetSdk = 35
|
||||||
versionCode = 1
|
versionCode = 1
|
||||||
versionName = "1.0"
|
versionName = "1.0"
|
||||||
@@ -34,11 +34,11 @@ android {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
compileOptions {
|
compileOptions {
|
||||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
sourceCompatibility = JavaVersion.VERSION_17
|
||||||
targetCompatibility = JavaVersion.VERSION_1_8
|
targetCompatibility = JavaVersion.VERSION_17
|
||||||
}
|
}
|
||||||
kotlinOptions {
|
kotlinOptions {
|
||||||
jvmTarget = "1.8"
|
jvmTarget = "17"
|
||||||
}
|
}
|
||||||
buildFeatures {
|
buildFeatures {
|
||||||
compose = true
|
compose = true
|
||||||
@@ -64,12 +64,27 @@ dependencies {
|
|||||||
|
|
||||||
// Ktor - web client
|
// Ktor - web client
|
||||||
implementation(libs.ktor.client.core)
|
implementation(libs.ktor.client.core)
|
||||||
implementation(libs.ktor.client.cio)
|
implementation(libs.ktor.client.okhttp)
|
||||||
|
implementation(libs.ktor.client.logging)
|
||||||
|
|
||||||
// Coil - image loader
|
// Coil - image loader
|
||||||
implementation(libs.coil.compose)
|
implementation(libs.coil.compose)
|
||||||
implementation(libs.coil.network.okhttp)
|
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
|
// Others
|
||||||
implementation(libs.splitties.base) // Syntax sugar
|
implementation(libs.splitties.base) // Syntax sugar
|
||||||
|
implementation(libs.jsoup) // HTML parser
|
||||||
}
|
}
|
||||||
@@ -1,8 +1,7 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?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.
|
~ Copyright (c) 2025. All rights reserved.
|
||||||
~ Last modified 21.02.2025, 12:07
|
|
||||||
-->
|
-->
|
||||||
|
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
@@ -20,15 +19,9 @@
|
|||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
android:theme="@style/Theme.PixelDragon"
|
android:theme="@style/Theme.PixelDragon"
|
||||||
tools:targetApi="31">
|
tools:targetApi="31">
|
||||||
<activity
|
|
||||||
android:name=".ui.activity.Login"
|
|
||||||
android:exported="false"
|
|
||||||
android:label="@string/title_activity_login"
|
|
||||||
android:theme="@style/Theme.PixelDragon" />
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.activity.MainActivity"
|
android:name=".ui.activity.MainActivity"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:label="@string/app_name"
|
|
||||||
android:theme="@style/Theme.PixelDragon">
|
android:theme="@style/Theme.PixelDragon">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
@@ -36,6 +29,11 @@
|
|||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:name=".ui.activity.Login"
|
||||||
|
android:exported="false"
|
||||||
|
android:theme="@style/Theme.PixelDragon" />
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
||||||
@@ -1,43 +1,53 @@
|
|||||||
/*
|
/*
|
||||||
* Created by sweetbread on 21.02.2025, 12:01
|
* Created by sweetbread
|
||||||
* Copyright (c) 2025. All rights reserved.
|
* Copyright (c) 2025. All rights reserved.
|
||||||
* Last modified 21.02.2025, 12:01
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package ru.risdeveau.pixeldragon
|
package ru.risdeveau.pixeldragon
|
||||||
|
|
||||||
import android.content.Context
|
import android.util.Log
|
||||||
import io.ktor.client.HttpClient
|
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 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 ru.risdeveau.pixeldragon.api.MatrixSyncService
|
||||||
import ru.risdeveau.pixeldragon.api.getMe
|
import ru.risdeveau.pixeldragon.api.getMe
|
||||||
import splitties.init.appCtx
|
import splitties.preferences.Preferences
|
||||||
|
|
||||||
val client = HttpClient(CIO) {
|
val client = HttpClient {
|
||||||
engine {
|
install(Logging) {
|
||||||
endpoint {
|
logger = object : Logger {
|
||||||
maxConnectionsPerRoute = 100
|
override fun log(message: String) {
|
||||||
pipelineMaxSize = 20
|
Log.i("Ktor", message)
|
||||||
keepAliveTime = 30000
|
}
|
||||||
connectTimeout = 15000
|
|
||||||
connectAttempts = 5
|
|
||||||
}
|
}
|
||||||
|
level = LogLevel.ALL
|
||||||
}
|
}
|
||||||
|
|
||||||
install(HttpCache)
|
install(HttpCache)
|
||||||
}
|
}
|
||||||
|
|
||||||
val accountData = appCtx.getSharedPreferences("settings", Context.MODE_PRIVATE)
|
lateinit var homeserver: String
|
||||||
lateinit var urlBase: String
|
lateinit var baseUrl: String
|
||||||
lateinit var token: String
|
lateinit var token: String
|
||||||
|
|
||||||
suspend fun initCheck(): Boolean {
|
object AccountData : Preferences("system_parameters") {
|
||||||
if (!accountData.contains("token")) return false
|
var token by stringOrNullPref("token", null)
|
||||||
if (!accountData.contains("homeserver")) return false
|
var homeserver by stringOrNullPref("homeserver", null)
|
||||||
|
var syncLastBatch by stringOrNullPref("next_batch", null)
|
||||||
|
var filter by stringOrNullPref("filter", null)
|
||||||
|
}
|
||||||
|
|
||||||
token = accountData.getString("token", "").toString()
|
val syncService = MatrixSyncService()
|
||||||
urlBase = "https://${accountData.getString("homeserver", "")}/_matrix/client/v3"
|
|
||||||
|
suspend fun initCheck(): Boolean {
|
||||||
|
Log.d("initCheck", "checking...")
|
||||||
|
|
||||||
|
token = AccountData.token ?: return false
|
||||||
|
homeserver = AccountData.homeserver ?: return false
|
||||||
|
|
||||||
|
baseUrl = "$homeserver/_matrix/client/v3"
|
||||||
|
|
||||||
return getMe() != null
|
return getMe() != null
|
||||||
}
|
}
|
||||||
|
|||||||
+58
@@ -0,0 +1,58 @@
|
|||||||
|
/*
|
||||||
|
* Created by sweetbread
|
||||||
|
* Copyright (c) 2025. All rights reserved.
|
||||||
|
* Last modified 03.03.2025, 20:21
|
||||||
|
*/
|
||||||
|
|
||||||
|
package ru.risdeveau.pixeldragon.api
|
||||||
|
|
||||||
|
import io.ktor.client.request.bearerAuth
|
||||||
|
import io.ktor.client.request.get
|
||||||
|
import io.ktor.client.request.parameter
|
||||||
|
import io.ktor.client.statement.bodyAsText
|
||||||
|
import org.json.JSONObject
|
||||||
|
import ru.risdeveau.pixeldragon.baseUrl
|
||||||
|
import ru.risdeveau.pixeldragon.client
|
||||||
|
import ru.risdeveau.pixeldragon.token
|
||||||
|
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()
|
||||||
|
// )
|
||||||
|
//}
|
||||||
+97
@@ -0,0 +1,97 @@
|
|||||||
|
/*
|
||||||
|
* Created by sweetbread
|
||||||
|
* Copyright (c) 2025. All rights reserved.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package ru.risdeveau.pixeldragon.api
|
||||||
|
|
||||||
|
import io.ktor.client.request.bearerAuth
|
||||||
|
import io.ktor.client.request.get
|
||||||
|
import io.ktor.client.statement.bodyAsText
|
||||||
|
import io.ktor.http.HttpStatusCode
|
||||||
|
import kotlinx.coroutines.async
|
||||||
|
import kotlinx.coroutines.coroutineScope
|
||||||
|
import org.json.JSONObject
|
||||||
|
import ru.risdeveau.pixeldragon.baseUrl
|
||||||
|
import ru.risdeveau.pixeldragon.client
|
||||||
|
import ru.risdeveau.pixeldragon.repo.Room
|
||||||
|
import ru.risdeveau.pixeldragon.repo.User
|
||||||
|
import ru.risdeveau.pixeldragon.token
|
||||||
|
|
||||||
|
//fun getRooms(): List<Room> {
|
||||||
|
// return db.roomDoa().getAllJoined()
|
||||||
|
//}
|
||||||
|
//
|
||||||
|
//fun updateRooms(): List<Room> {
|
||||||
|
//
|
||||||
|
//}
|
||||||
|
|
||||||
|
suspend fun getJoinedRooms(): List<Room> {
|
||||||
|
val r = client.get("$baseUrl/joined_rooms")
|
||||||
|
{ bearerAuth(token) }
|
||||||
|
val rooms = JSONObject(r.bodyAsText()).getJSONArray("joined_rooms")
|
||||||
|
return List(
|
||||||
|
rooms.length()
|
||||||
|
) { i -> getRoom(rooms.getString(i), true) }
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun isJoined(id: String): Boolean {
|
||||||
|
val r = client.get("$baseUrl/joined_rooms")
|
||||||
|
{ bearerAuth(token) }
|
||||||
|
val rooms = JSONObject(r.bodyAsText()).getJSONArray("joined_rooms")
|
||||||
|
return List<String>(
|
||||||
|
rooms.length()
|
||||||
|
) { i -> rooms.getString(i) }.contains(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun getRoom(rid: String, joined: Boolean? = null): Room {
|
||||||
|
val direct = getAccountData(getMe()!!.userId, "m.direct")
|
||||||
|
var directWith = ""
|
||||||
|
direct?.let {
|
||||||
|
for (user in direct.keys()) {
|
||||||
|
val roomsWithUser = direct.getJSONArray(user)
|
||||||
|
for (i in 0 until roomsWithUser.length()) {
|
||||||
|
if (rid == roomsWithUser.getString(i)) {
|
||||||
|
directWith = user
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (directWith.isNotEmpty()) break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return coroutineScope {
|
||||||
|
val name = async { getState(rid, "m.room.name", "name") }
|
||||||
|
val type = async { getState(rid, "m.room.create", "type") ?: "m.room" }
|
||||||
|
val creator = async { getState(rid, "m.room.create", "creator") }
|
||||||
|
val avatar = async { getState(rid, "m.room.avatar", "url") }
|
||||||
|
val joined = async { joined ?: isJoined(rid) }
|
||||||
|
val direct = async { if (directWith.isNotEmpty()) User.getById(directWith) else null }
|
||||||
|
|
||||||
|
Room(
|
||||||
|
rid,
|
||||||
|
name.await(),
|
||||||
|
type.await(),
|
||||||
|
creator.await(),
|
||||||
|
null,
|
||||||
|
avatar.await(),
|
||||||
|
null,
|
||||||
|
joined.await(),
|
||||||
|
direct.await()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun getState(rid: String, state: String, key: String): String? {
|
||||||
|
val r = client.get("$baseUrl/rooms/$rid/state/$state") { bearerAuth(token) }
|
||||||
|
if (r.status != HttpStatusCode.OK) return null
|
||||||
|
val json = JSONObject(r.bodyAsText())
|
||||||
|
if (!json.has(key)) return null
|
||||||
|
return json.getString(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun getAccountData(user: String, room: String, state: String): JSONObject? {
|
||||||
|
val r = client.get("$baseUrl/user/$user/rooms/$room/account_data/$state") { bearerAuth(token) }
|
||||||
|
if (r.status != HttpStatusCode.OK) return null
|
||||||
|
return JSONObject(r.bodyAsText())
|
||||||
|
}
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
* Created by sweetbread on 21.02.2025, 12:01
|
* Created by sweetbread
|
||||||
* Copyright (c) 2025. All rights reserved.
|
* Copyright (c) 2025. All rights reserved.
|
||||||
* Last modified 21.02.2025, 12:01
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package ru.risdeveau.pixeldragon.api
|
package ru.risdeveau.pixeldragon.api
|
||||||
@@ -11,13 +10,28 @@ import io.ktor.client.statement.bodyAsText
|
|||||||
import org.json.JSONException
|
import org.json.JSONException
|
||||||
import org.json.JSONObject
|
import org.json.JSONObject
|
||||||
import ru.risdeveau.pixeldragon.client
|
import ru.risdeveau.pixeldragon.client
|
||||||
|
import ru.risdeveau.pixeldragon.homeserver
|
||||||
|
|
||||||
suspend fun isMatrixServer(url: String): Boolean {
|
suspend fun getHomeserver(url: String): String? {
|
||||||
val r = try { client.get("https://$url/.well-known/matrix/client") }
|
val r = try { client.get("https://$url/.well-known/matrix/client") }
|
||||||
catch (_: Exception) { return false }
|
catch (_: Exception) { return null }
|
||||||
|
|
||||||
try { JSONObject(r.bodyAsText()) }
|
val json = try { JSONObject(r.bodyAsText()) }
|
||||||
catch (_: JSONException) { return false }
|
catch (_: JSONException) { 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,12 +1,10 @@
|
|||||||
/*
|
/*
|
||||||
* Created by sweetbread on 21.02.2025, 12:09
|
* Created by sweetbread
|
||||||
* Copyright (c) 2025. All rights reserved.
|
* Copyright (c) 2025. All rights reserved.
|
||||||
* Last modified 21.02.2025, 12:01
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package ru.risdeveau.pixeldragon.api
|
package ru.risdeveau.pixeldragon.api
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import io.ktor.client.request.bearerAuth
|
import io.ktor.client.request.bearerAuth
|
||||||
import io.ktor.client.request.get
|
import io.ktor.client.request.get
|
||||||
@@ -17,20 +15,23 @@ import io.ktor.http.ContentType
|
|||||||
import io.ktor.http.HttpStatusCode
|
import io.ktor.http.HttpStatusCode
|
||||||
import io.ktor.http.contentType
|
import io.ktor.http.contentType
|
||||||
import org.json.JSONObject
|
import org.json.JSONObject
|
||||||
import ru.risdeveau.pixeldragon.accountData
|
import ru.risdeveau.pixeldragon.AccountData
|
||||||
|
import ru.risdeveau.pixeldragon.baseUrl
|
||||||
import ru.risdeveau.pixeldragon.client
|
import ru.risdeveau.pixeldragon.client
|
||||||
import ru.risdeveau.pixeldragon.initCheck
|
import ru.risdeveau.pixeldragon.initCheck
|
||||||
import ru.risdeveau.pixeldragon.token
|
import ru.risdeveau.pixeldragon.token
|
||||||
import ru.risdeveau.pixeldragon.urlBase
|
import splitties.experimental.ExperimentalSplittiesApi
|
||||||
import splitties.init.appCtx
|
import splitties.init.appCtx
|
||||||
|
import splitties.preferences.edit
|
||||||
|
|
||||||
data class Me (val userId: String, val deviceId: String)
|
data class Me (val userId: String, val deviceId: String)
|
||||||
|
data class UserProfile (val displayName: String, val avatarUrl: String, val other: JSONObject)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This func is to validate the token
|
* This func is to validate the token
|
||||||
*/
|
*/
|
||||||
suspend fun getMe(): Me? {
|
suspend fun getMe(): Me? {
|
||||||
val r = client.get("$urlBase/account/whoami") { bearerAuth(token) }
|
val r = client.get("$baseUrl/account/whoami") { bearerAuth(token) }
|
||||||
if (r.status != HttpStatusCode.OK) {
|
if (r.status != HttpStatusCode.OK) {
|
||||||
Log.e("getMe", r.bodyAsText())
|
Log.e("getMe", r.bodyAsText())
|
||||||
return null
|
return null
|
||||||
@@ -40,9 +41,11 @@ suspend fun getMe(): Me? {
|
|||||||
return Me(json.getString("user_id"), json.getString("device_id"))
|
return Me(json.getString("user_id"), json.getString("device_id"))
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("ApplySharedPref")
|
@OptIn(ExperimentalSplittiesApi::class)
|
||||||
suspend fun login(homeserver: String, login: String, pass: String): Boolean {
|
suspend fun login(server: String, login: String, pass: String): Boolean {
|
||||||
val pinfo = appCtx.packageManager.getPackageInfo(appCtx.packageName, 0);
|
val hs = getHomeserver(server)!!
|
||||||
|
|
||||||
|
val pinfo = appCtx.packageManager.getPackageInfo(appCtx.packageName, 0)
|
||||||
|
|
||||||
val pattern = """
|
val pattern = """
|
||||||
{
|
{
|
||||||
@@ -58,7 +61,7 @@ suspend fun login(homeserver: String, login: String, pass: String): Boolean {
|
|||||||
json.put("password", pass)
|
json.put("password", pass)
|
||||||
|
|
||||||
val r = try {
|
val r = try {
|
||||||
client.post("https://$homeserver/_matrix/client/v3/login") {
|
client.post("$hs/_matrix/client/v3/login") {
|
||||||
setBody(json.toString())
|
setBody(json.toString())
|
||||||
contentType(ContentType.Application.Json)
|
contentType(ContentType.Application.Json)
|
||||||
}
|
}
|
||||||
@@ -73,10 +76,25 @@ suspend fun login(homeserver: String, login: String, pass: String): Boolean {
|
|||||||
}
|
}
|
||||||
|
|
||||||
val res = JSONObject(r.bodyAsText())
|
val res = JSONObject(r.bodyAsText())
|
||||||
val editor = accountData.edit()
|
AccountData.edit {
|
||||||
editor.putString("token", res.getString("access_token"))
|
token = res.getString("access_token")
|
||||||
editor.putString("homeserver", res.getString("home_server"))
|
homeserver = hs
|
||||||
editor.commit()
|
}
|
||||||
|
|
||||||
return initCheck()
|
return initCheck()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun getAccountData(user: String, state: String): JSONObject? {
|
||||||
|
val r = client.get("$baseUrl/user/$user/account_data/$state") { bearerAuth(token) }
|
||||||
|
if (r.status != HttpStatusCode.OK) return null
|
||||||
|
return JSONObject(r.bodyAsText())
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun getUserProfile(userId: String): UserProfile? {
|
||||||
|
val r = client.get("$baseUrl/profile/$userId") { bearerAuth(token) }
|
||||||
|
if (r.status != HttpStatusCode.OK) return null
|
||||||
|
val json = JSONObject(r.bodyAsText())
|
||||||
|
val name = json.optString("displayname", ""); json.remove("displayname")
|
||||||
|
val avatar = json.optString("avatar_url", ""); json.remove("avatar_url")
|
||||||
|
return UserProfile(name, avatar, json)
|
||||||
|
}
|
||||||
+237
@@ -0,0 +1,237 @@
|
|||||||
|
/*
|
||||||
|
* Created by sweetbread
|
||||||
|
* Copyright (c) 2025. All rights reserved.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package ru.risdeveau.pixeldragon.api
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import io.ktor.client.request.bearerAuth
|
||||||
|
import io.ktor.client.request.get
|
||||||
|
import io.ktor.client.request.header
|
||||||
|
import io.ktor.client.request.parameter
|
||||||
|
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 kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import kotlinx.coroutines.isActive
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import org.json.JSONObject
|
||||||
|
import ru.risdeveau.pixeldragon.AccountData.syncLastBatch
|
||||||
|
import ru.risdeveau.pixeldragon.baseUrl
|
||||||
|
import ru.risdeveau.pixeldragon.client
|
||||||
|
import ru.risdeveau.pixeldragon.db.isConnected
|
||||||
|
import ru.risdeveau.pixeldragon.token
|
||||||
|
import ru.risdeveau.pixeldragon.ui.activity.ME
|
||||||
|
import splitties.init.appCtx
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
|
||||||
|
class MatrixSyncService {
|
||||||
|
private val _syncState = MutableStateFlow<SyncState>(SyncState.Idle)
|
||||||
|
val syncState: StateFlow<SyncState> = _syncState.asStateFlow()
|
||||||
|
|
||||||
|
private var syncJob: Job? = null
|
||||||
|
// private var isInitialized = false
|
||||||
|
//
|
||||||
|
// fun initialize() {
|
||||||
|
// isInitialized = true
|
||||||
|
// }
|
||||||
|
|
||||||
|
fun startSync() {
|
||||||
|
// if (!isInitialized)
|
||||||
|
// throw IllegalStateException("Sync service not initialized")
|
||||||
|
|
||||||
|
if (syncJob?.isActive == true) return
|
||||||
|
|
||||||
|
syncJob = CoroutineScope(Dispatchers.IO).launch {
|
||||||
|
if (syncLastBatch == "") {
|
||||||
|
_syncState.value = SyncState.Syncing
|
||||||
|
try {
|
||||||
|
processSyncResponse(initialSync())
|
||||||
|
_syncState.value = SyncState.Idle
|
||||||
|
} catch (e: Exception) {
|
||||||
|
_syncState.value = SyncState.Error(e.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// while (isActive) {
|
||||||
|
// try {
|
||||||
|
// val response = sync (
|
||||||
|
// timeout = 30000,
|
||||||
|
// filter = getFilter()
|
||||||
|
// )
|
||||||
|
//
|
||||||
|
// processSyncResponse(response)
|
||||||
|
//
|
||||||
|
// } catch (e: Exception) {
|
||||||
|
// Log.w("sync", e.message.toString())
|
||||||
|
// delay(5000) // Wait before retry
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun stopSync() {
|
||||||
|
syncJob?.cancel()
|
||||||
|
_syncState.value = SyncState.Idle
|
||||||
|
}
|
||||||
|
|
||||||
|
fun pauseSync() {
|
||||||
|
// Called when app goes to background
|
||||||
|
syncJob?.cancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun resumeSync() {
|
||||||
|
// Called when app comes to foreground
|
||||||
|
if (!isSyncActive()) {
|
||||||
|
startSync()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isSyncActive(): Boolean = syncJob?.isActive == true
|
||||||
|
|
||||||
|
private suspend fun processSyncResponse(response: JSONObject) {
|
||||||
|
// Log.d("syncResponse", response.toString(2))
|
||||||
|
// val newMessages = mutableListOf<Message>()
|
||||||
|
// val roomUpdates = mutableListOf<RoomUpdate>()
|
||||||
|
//
|
||||||
|
// response.rooms?.join?.forEach { (roomId, roomData) ->
|
||||||
|
// // Process timeline events (messages)
|
||||||
|
// roomData.timeline?.events?.forEach { event ->
|
||||||
|
// val message = event.toMessage(roomId)
|
||||||
|
// database.messageDao().insertMessage(message)
|
||||||
|
// newMessages.add(message)
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// // Process room state updates
|
||||||
|
// roomData.state?.events?.forEach { event ->
|
||||||
|
// when (event.type) {
|
||||||
|
// "m.room.name", "m.room.avatar" -> {
|
||||||
|
// roomUpdates.add(RoomUpdate(roomId, event.type, event.content))
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// // Process ephemeral events (typing, receipts)
|
||||||
|
// roomData.ephemeral?.events?.forEach { event ->
|
||||||
|
// when (event.type) {
|
||||||
|
// "m.typing" -> handleTypingEvent(roomId, event)
|
||||||
|
// "m.receipt" -> handleReceiptEvent(roomId, event)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// // Notify the app about new data
|
||||||
|
// if (newMessages.isNotEmpty()) {
|
||||||
|
// _newMessages.emit(newMessages)
|
||||||
|
// }
|
||||||
|
// if (roomUpdates.isNotEmpty()) {
|
||||||
|
// _roomUpdates.emit(roomUpdates)
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed class SyncState {
|
||||||
|
object Idle : SyncState()
|
||||||
|
object Syncing : SyncState()
|
||||||
|
object Success : SyncState()
|
||||||
|
data class Error(val message: String?) : SyncState()
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun sync(): JSONObject {
|
||||||
|
|
||||||
|
return JSONObject()
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun initialSync(): JSONObject {
|
||||||
|
val initialFilter = """
|
||||||
|
{
|
||||||
|
"room": {
|
||||||
|
"state": {
|
||||||
|
"types": [
|
||||||
|
"m.room.name",
|
||||||
|
"m.room.avatar",
|
||||||
|
"m.room.canonical_alias",
|
||||||
|
"m.room.encryption",
|
||||||
|
"m.room.tombstone",
|
||||||
|
"m.room.power_levels",
|
||||||
|
"m.room.member"
|
||||||
|
],
|
||||||
|
"lazy_load_members": true,
|
||||||
|
"not_types": []
|
||||||
|
},
|
||||||
|
"timeline": {
|
||||||
|
"limit": 10,
|
||||||
|
"types": ["m.room.message"],
|
||||||
|
"not_types": [
|
||||||
|
"m.room.name",
|
||||||
|
"m.room.avatar",
|
||||||
|
"m.room.canonical_alias",
|
||||||
|
"m.room.encryption",
|
||||||
|
"m.room.tombstone",
|
||||||
|
"m.room.power_levels",
|
||||||
|
"m.room.member",
|
||||||
|
"m.call.*"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"ephemeral": {
|
||||||
|
"types": [],
|
||||||
|
"not_types": ["m.typing", "m.receipt"]
|
||||||
|
},
|
||||||
|
"include_leave": false
|
||||||
|
},
|
||||||
|
"presence": {
|
||||||
|
"types": [],
|
||||||
|
"not_types": ["*"]
|
||||||
|
},
|
||||||
|
"event_format": "client",
|
||||||
|
"event_fields": [
|
||||||
|
"type",
|
||||||
|
"content",
|
||||||
|
"sender",
|
||||||
|
"state_key",
|
||||||
|
"room_id",
|
||||||
|
"origin_server_ts"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
""".trimIndent()
|
||||||
|
|
||||||
|
var r = client.post("$baseUrl/user/${ME!!.userId}/filter") {
|
||||||
|
setBody(initialFilter)
|
||||||
|
contentType(ContentType.Application.Json)
|
||||||
|
bearerAuth(token)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (r.status != HttpStatusCode.OK)
|
||||||
|
throw IllegalStateException("Failed to create a filter")
|
||||||
|
val filterId = JSONObject(r.bodyAsText()).getString("filter_id")
|
||||||
|
// val filterId = "vmNk"
|
||||||
|
|
||||||
|
/*val*/ r = client.get("$baseUrl/sync") {
|
||||||
|
bearerAuth(token)
|
||||||
|
url {
|
||||||
|
parameter("filter", filterId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (r.status != HttpStatusCode.OK)
|
||||||
|
throw IllegalStateException("Failed to sync")
|
||||||
|
Log.d("initialSync", "Response size: ${r.bodyAsText().length}")
|
||||||
|
|
||||||
|
val json = JSONObject(r.bodyAsText())
|
||||||
|
syncLastBatch = json.getString("next_batch")
|
||||||
|
|
||||||
|
return json
|
||||||
|
}
|
||||||
+41
@@ -0,0 +1,41 @@
|
|||||||
|
/*
|
||||||
|
* Created by sweetbread
|
||||||
|
* Copyright (c) 2025. All rights reserved.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package ru.risdeveau.pixeldragon.db
|
||||||
|
|
||||||
|
import androidx.room.Database
|
||||||
|
import androidx.room.Room
|
||||||
|
import androidx.room.RoomDatabase
|
||||||
|
import androidx.room.TypeConverter
|
||||||
|
import splitties.init.appCtx
|
||||||
|
import splitties.systemservices.connectivityManager
|
||||||
|
import java.time.Instant
|
||||||
|
|
||||||
|
@Database(entities = [RoomDB::class, SpaceToRoom::class, UserDB::class], version = 1)
|
||||||
|
abstract class AppDatabase : RoomDatabase() {
|
||||||
|
abstract fun roomDoa(): RoomDao
|
||||||
|
abstract fun userDoa(): UserDao
|
||||||
|
}
|
||||||
|
|
||||||
|
class Converters {
|
||||||
|
@TypeConverter
|
||||||
|
fun fromInstant(value: Instant?): String? {
|
||||||
|
return value?.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
@TypeConverter
|
||||||
|
fun toInstant(value: String?): Instant? {
|
||||||
|
return value?.let { Instant.parse(it) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isConnected(): Boolean = connectivityManager.isDefaultNetworkActive
|
||||||
|
|
||||||
|
val cacheDb = Room.databaseBuilder(
|
||||||
|
appCtx,
|
||||||
|
AppDatabase::class.java, "cache"
|
||||||
|
)
|
||||||
|
.fallbackToDestructiveMigration()
|
||||||
|
.build()
|
||||||
+101
@@ -0,0 +1,101 @@
|
|||||||
|
/*
|
||||||
|
* Created by sweetbread
|
||||||
|
* Copyright (c) 2025. All rights reserved.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package ru.risdeveau.pixeldragon.db
|
||||||
|
|
||||||
|
import androidx.room.Dao
|
||||||
|
import androidx.room.Delete
|
||||||
|
import androidx.room.Embedded
|
||||||
|
import androidx.room.Entity
|
||||||
|
import androidx.room.Insert
|
||||||
|
import androidx.room.Junction
|
||||||
|
import androidx.room.OnConflictStrategy
|
||||||
|
import androidx.room.PrimaryKey
|
||||||
|
import androidx.room.Query
|
||||||
|
import androidx.room.Relation
|
||||||
|
import androidx.room.TypeConverters
|
||||||
|
import ru.risdeveau.pixeldragon.repo.Room
|
||||||
|
import ru.risdeveau.pixeldragon.repo.User
|
||||||
|
import java.time.Instant
|
||||||
|
|
||||||
|
@Entity(tableName = "room")
|
||||||
|
@TypeConverters(Converters::class)
|
||||||
|
data class RoomDB (
|
||||||
|
@PrimaryKey val id: String,
|
||||||
|
val updatedAt: Instant,
|
||||||
|
val name: String?,
|
||||||
|
val type: String,
|
||||||
|
val creatorId: String?,
|
||||||
|
val createTime: Long?,
|
||||||
|
val avatarUrl: String?,
|
||||||
|
val members: Int?,
|
||||||
|
val joined: Boolean,
|
||||||
|
val direct: String?
|
||||||
|
)
|
||||||
|
|
||||||
|
fun RoomDB.isExpired(cacheDuration: Long = 60 * 60): Boolean {
|
||||||
|
return Instant.now().minusSeconds(cacheDuration) > updatedAt
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun RoomDB.toDomain(): Room = Room(
|
||||||
|
id = id,
|
||||||
|
name = name,
|
||||||
|
type = type,
|
||||||
|
creatorId = creatorId,
|
||||||
|
createTime = createTime,
|
||||||
|
avatarUrl = avatarUrl,
|
||||||
|
members = members,
|
||||||
|
joined = joined,
|
||||||
|
direct = direct?.let { User.getById(it) }
|
||||||
|
)
|
||||||
|
|
||||||
|
fun Room.toEntity(): RoomDB = RoomDB(
|
||||||
|
id = id,
|
||||||
|
updatedAt = Instant.now(),
|
||||||
|
name = name,
|
||||||
|
type = type,
|
||||||
|
creatorId = creatorId,
|
||||||
|
createTime = createTime,
|
||||||
|
avatarUrl = avatarUrl,
|
||||||
|
members = members,
|
||||||
|
joined = joined,
|
||||||
|
direct = direct?.id
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@Dao
|
||||||
|
interface RoomDao {
|
||||||
|
@Query("SELECT * FROM room WHERE id LIKE :id LIMIT 1")
|
||||||
|
fun getById(id: String): RoomDB?
|
||||||
|
|
||||||
|
// @Transaction
|
||||||
|
// @Query("SELECT * FROM room WHERE ")
|
||||||
|
// fun getSpace(rid: String): Space
|
||||||
|
|
||||||
|
@Query("SELECT * FROM room WHERE joined = 1 AND id NOT IN (SELECT id FROM SpaceToRoom)")
|
||||||
|
fun getAllJoined(): List<RoomDB>
|
||||||
|
|
||||||
|
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||||
|
fun insert(vararg rooms: RoomDB)
|
||||||
|
|
||||||
|
@Delete
|
||||||
|
fun delete(room: RoomDB)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Entity(primaryKeys = ["spaceId", "roomId"])
|
||||||
|
data class SpaceToRoom(
|
||||||
|
val spaceId: String,
|
||||||
|
val roomId: String
|
||||||
|
)
|
||||||
|
|
||||||
|
data class Space(
|
||||||
|
@Embedded val space: RoomDB,
|
||||||
|
@Relation(
|
||||||
|
parentColumn = "spaceId",
|
||||||
|
entityColumn = "roomId",
|
||||||
|
associateBy = Junction(SpaceToRoom::class)
|
||||||
|
)
|
||||||
|
val children: List<RoomDB>
|
||||||
|
)
|
||||||
+60
@@ -0,0 +1,60 @@
|
|||||||
|
/*
|
||||||
|
* Created by sweetbread
|
||||||
|
* Copyright (c) 2025. All rights reserved.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package ru.risdeveau.pixeldragon.db
|
||||||
|
|
||||||
|
import androidx.room.Dao
|
||||||
|
import androidx.room.Delete
|
||||||
|
import androidx.room.Entity
|
||||||
|
import androidx.room.Insert
|
||||||
|
import androidx.room.OnConflictStrategy
|
||||||
|
import androidx.room.PrimaryKey
|
||||||
|
import androidx.room.Query
|
||||||
|
import androidx.room.TypeConverters
|
||||||
|
import org.json.JSONObject
|
||||||
|
import ru.risdeveau.pixeldragon.repo.User
|
||||||
|
import java.time.Instant
|
||||||
|
|
||||||
|
@Entity(tableName = "user")
|
||||||
|
@TypeConverters(Converters::class)
|
||||||
|
data class UserDB (
|
||||||
|
@PrimaryKey val id: String,
|
||||||
|
val updatedAt: Instant,
|
||||||
|
val name: String?,
|
||||||
|
val avatar: String?,
|
||||||
|
val attrs: String,
|
||||||
|
)
|
||||||
|
|
||||||
|
fun UserDB.isExpired(cacheDuration: Long = 60 * 60): Boolean {
|
||||||
|
return Instant.now().minusSeconds(cacheDuration) > updatedAt
|
||||||
|
}
|
||||||
|
|
||||||
|
fun UserDB.toDomain(): User = User(
|
||||||
|
id = id,
|
||||||
|
name = name,
|
||||||
|
avatarUrl = avatar,
|
||||||
|
attrs = JSONObject(attrs),
|
||||||
|
)
|
||||||
|
|
||||||
|
fun User.toEntity(): UserDB = UserDB(
|
||||||
|
id = id,
|
||||||
|
updatedAt = Instant.now(),
|
||||||
|
name = name,
|
||||||
|
avatar = avatarUrl,
|
||||||
|
attrs = attrs.toString().trim(),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@Dao
|
||||||
|
interface UserDao {
|
||||||
|
@Query("SELECT * FROM user WHERE id LIKE :id LIMIT 1")
|
||||||
|
fun getById(id: String): UserDB?
|
||||||
|
|
||||||
|
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||||
|
fun insert(vararg users: UserDB)
|
||||||
|
|
||||||
|
@Delete
|
||||||
|
fun delete(user: UserDB)
|
||||||
|
}
|
||||||
+62
@@ -0,0 +1,62 @@
|
|||||||
|
/*
|
||||||
|
* Created by sweetbread
|
||||||
|
* Copyright (c) 2025. All rights reserved.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package ru.risdeveau.pixeldragon.repo
|
||||||
|
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import ru.risdeveau.pixeldragon.api.getJoinedRooms
|
||||||
|
import ru.risdeveau.pixeldragon.api.getRoom
|
||||||
|
import ru.risdeveau.pixeldragon.db.cacheDb
|
||||||
|
import ru.risdeveau.pixeldragon.db.isConnected
|
||||||
|
import ru.risdeveau.pixeldragon.db.isExpired
|
||||||
|
import ru.risdeveau.pixeldragon.db.toDomain
|
||||||
|
import ru.risdeveau.pixeldragon.db.toEntity
|
||||||
|
|
||||||
|
class Room (
|
||||||
|
val id: String,
|
||||||
|
val name: String?,
|
||||||
|
val type: String,
|
||||||
|
val creatorId: String?,
|
||||||
|
val createTime: Long?,
|
||||||
|
val avatarUrl: String?,
|
||||||
|
val members: Int?,
|
||||||
|
val joined: Boolean,
|
||||||
|
val direct: User?,
|
||||||
|
|
||||||
|
) {
|
||||||
|
companion object {
|
||||||
|
suspend fun getById(id: String, cached: Boolean = true): Room {
|
||||||
|
val cachedRoom = cacheDb.roomDoa().getById(id)
|
||||||
|
if (!isConnected() and
|
||||||
|
(!cached or (cachedRoom == null) or (cachedRoom?.isExpired() == true))
|
||||||
|
) {
|
||||||
|
val room = getRoom(id)
|
||||||
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
|
val cacheRoom = room.toEntity()
|
||||||
|
cacheDb.roomDoa().insert(cacheRoom)
|
||||||
|
}
|
||||||
|
return room
|
||||||
|
}
|
||||||
|
return cachedRoom!!.toDomain()
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun getJoined(cached: Boolean = true): List<Room> {
|
||||||
|
val cacheJoined = cacheDb.roomDoa().getAllJoined()
|
||||||
|
if (isConnected() and
|
||||||
|
(!cached or cacheJoined.isEmpty() or (cacheJoined.any { it.isExpired() }))
|
||||||
|
) {
|
||||||
|
val rooms = getJoinedRooms()
|
||||||
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
|
val roomsDb = List(rooms.size) { i -> rooms[i].toEntity() }
|
||||||
|
cacheDb.roomDoa().insert(*roomsDb.toTypedArray())
|
||||||
|
}
|
||||||
|
return rooms
|
||||||
|
}
|
||||||
|
return List(cacheJoined.size) { i -> cacheJoined[i].toDomain() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+51
@@ -0,0 +1,51 @@
|
|||||||
|
/*
|
||||||
|
* Created by sweetbread
|
||||||
|
* Copyright (c) 2025. All rights reserved.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package ru.risdeveau.pixeldragon.repo
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import org.json.JSONObject
|
||||||
|
import ru.risdeveau.pixeldragon.api.getUserProfile
|
||||||
|
import ru.risdeveau.pixeldragon.db.cacheDb
|
||||||
|
import ru.risdeveau.pixeldragon.db.isConnected
|
||||||
|
import ru.risdeveau.pixeldragon.db.isExpired
|
||||||
|
import ru.risdeveau.pixeldragon.db.toDomain
|
||||||
|
import ru.risdeveau.pixeldragon.db.toEntity
|
||||||
|
|
||||||
|
class User (
|
||||||
|
val id: String,
|
||||||
|
val name: String?,
|
||||||
|
val avatarUrl: String?,
|
||||||
|
val attrs: JSONObject
|
||||||
|
) {
|
||||||
|
companion object {
|
||||||
|
suspend fun getById(id: String, cached: Boolean = true): User? {
|
||||||
|
val cachedUser = cacheDb.userDoa().getById(id)
|
||||||
|
if (isConnected() and
|
||||||
|
(!cached or (cachedUser == null) or (cachedUser?.isExpired() == true))
|
||||||
|
) {
|
||||||
|
val userProfile = getUserProfile(id)
|
||||||
|
if (userProfile == null) {
|
||||||
|
Log.i("User.getById", "User $id not found")
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
val user = User(
|
||||||
|
id,
|
||||||
|
userProfile.displayName,
|
||||||
|
userProfile.avatarUrl,
|
||||||
|
userProfile.other
|
||||||
|
)
|
||||||
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
|
cacheDb.userDoa().insert(user.toEntity())
|
||||||
|
}
|
||||||
|
return user
|
||||||
|
}
|
||||||
|
return cachedUser!!.toDomain()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
* Created by sweetbread on 21.02.2025, 12:08
|
* Created by sweetbread
|
||||||
* Copyright (c) 2025. All rights reserved.
|
* Copyright (c) 2025. All rights reserved.
|
||||||
* Last modified 21.02.2025, 12:08
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package ru.risdeveau.pixeldragon.ui.activity
|
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.KeyboardType
|
||||||
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||||
import kotlinx.coroutines.launch
|
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.api.login
|
||||||
import ru.risdeveau.pixeldragon.ui.theme.PixelDragonTheme
|
import ru.risdeveau.pixeldragon.ui.theme.PixelDragonTheme
|
||||||
import splitties.activities.start
|
import splitties.activities.start
|
||||||
|
|
||||||
class Login : ComponentActivity() {
|
class Login : ComponentActivity() {
|
||||||
|
@OptIn(DelicateCoroutinesApi::class)
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
enableEdgeToEdge()
|
enableEdgeToEdge()
|
||||||
setContent {
|
setContent {
|
||||||
PixelDragonTheme {
|
PixelDragonTheme {
|
||||||
@@ -56,10 +58,16 @@ class Login : ComponentActivity() {
|
|||||||
SnackbarHost(hostState = snackbarHostState)
|
SnackbarHost(hostState = snackbarHostState)
|
||||||
}
|
}
|
||||||
) { innerPadding ->
|
) { innerPadding ->
|
||||||
Box(Modifier.fillMaxSize().padding(innerPadding)) {
|
Box(
|
||||||
|
Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(innerPadding)) {
|
||||||
LoginField(
|
LoginField(
|
||||||
Modifier.align(Alignment.Center),
|
Modifier.align(Alignment.Center),
|
||||||
{ start<MainActivity>() },
|
{
|
||||||
|
start<MainActivity>()
|
||||||
|
finish()
|
||||||
|
},
|
||||||
{
|
{
|
||||||
scope.launch {
|
scope.launch {
|
||||||
snackbarHostState
|
snackbarHostState
|
||||||
@@ -89,7 +97,7 @@ fun LoginField(modifier: Modifier = Modifier, ok: () -> Unit, err: () -> Unit) {
|
|||||||
var login by remember { mutableStateOf("") }
|
var login by remember { mutableStateOf("") }
|
||||||
var pass by remember { mutableStateOf("") }
|
var pass by remember { mutableStateOf("") }
|
||||||
|
|
||||||
var hmsValid by remember { mutableStateOf(false) }
|
var hmsValid: Boolean by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
modifier = Modifier.padding(4.dp),
|
modifier = Modifier.padding(4.dp),
|
||||||
@@ -119,18 +127,17 @@ fun LoginField(modifier: Modifier = Modifier, ok: () -> Unit, err: () -> Unit) {
|
|||||||
Button(
|
Button(
|
||||||
enabled = hmsValid && !login.isEmpty() && !pass.isEmpty(),
|
enabled = hmsValid && !login.isEmpty() && !pass.isEmpty(),
|
||||||
onClick = {
|
onClick = {
|
||||||
var loginSuccess = false
|
scope.launch {
|
||||||
scope.launch { loginSuccess = login(homeserver, login, pass) }
|
if (login(homeserver, login, pass)) ok()
|
||||||
|
else err()
|
||||||
if (loginSuccess) ok()
|
}
|
||||||
else err()
|
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
Text("Login")
|
Text("Login")
|
||||||
}
|
}
|
||||||
|
|
||||||
LaunchedEffect(homeserver) {
|
LaunchedEffect(homeserver) {
|
||||||
scope.launch { hmsValid = isMatrixServer(homeserver) }
|
scope.launch { hmsValid = (getHomeserver(homeserver) != null) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
* Created by sweetbread on 21.02.2025, 12:08
|
* Created by sweetbread
|
||||||
* Copyright (c) 2025. All rights reserved.
|
* Copyright (c) 2025. All rights reserved.
|
||||||
* Last modified 21.02.2025, 12:07
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package ru.risdeveau.pixeldragon.ui.activity
|
package ru.risdeveau.pixeldragon.ui.activity
|
||||||
@@ -18,23 +17,29 @@ 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.Composable
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.navigation.NavType
|
||||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
import androidx.navigation.compose.NavHost
|
||||||
import kotlinx.coroutines.GlobalScope
|
import androidx.navigation.compose.composable
|
||||||
import kotlinx.coroutines.launch
|
import androidx.navigation.compose.rememberNavController
|
||||||
|
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.initCheck
|
import ru.risdeveau.pixeldragon.initCheck
|
||||||
|
import ru.risdeveau.pixeldragon.syncService
|
||||||
|
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.ui.theme.PixelDragonTheme
|
||||||
import splitties.activities.start
|
import splitties.activities.start
|
||||||
|
|
||||||
class MainActivity : ComponentActivity() {
|
var ME: Me? = null
|
||||||
@OptIn(ExperimentalMaterial3Api::class, DelicateCoroutinesApi::class)
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
GlobalScope.launch {
|
|
||||||
if (!initCheck()) start<Login>()
|
|
||||||
}
|
|
||||||
|
|
||||||
|
class MainActivity : ComponentActivity() {
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
enableEdgeToEdge()
|
enableEdgeToEdge()
|
||||||
@@ -54,28 +59,43 @@ class MainActivity : ComponentActivity() {
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
) { innerPadding ->
|
) { innerPadding ->
|
||||||
Greeting(
|
val navController = rememberNavController()
|
||||||
name = "Android",
|
|
||||||
modifier = Modifier.padding(innerPadding)
|
LaunchedEffect(Unit) {
|
||||||
)
|
if (initCheck()) {
|
||||||
|
ME = withContext(Dispatchers.IO) { getMe() }
|
||||||
|
if (ME != null) {
|
||||||
|
syncService.startSync()
|
||||||
|
navController.navigate("rooms")
|
||||||
|
} else {
|
||||||
|
start<Login>()
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
start<Login>()
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
NavHost(navController = navController, startDestination = "none") {
|
||||||
|
composable("none") { }
|
||||||
|
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 })
|
||||||
|
) { navBackStackEntry ->
|
||||||
|
Text(modifier = Modifier.padding(innerPadding), text = "Not implemented") }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun Greeting(name: String, modifier: Modifier = Modifier) {
|
|
||||||
Text(
|
|
||||||
text = "Hello $name!",
|
|
||||||
modifier = modifier
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Preview(showBackground = true)
|
|
||||||
@Composable
|
|
||||||
fun GreetingPreview() {
|
|
||||||
PixelDragonTheme {
|
|
||||||
Greeting("Android")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
/*
|
||||||
|
* 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.Alignment
|
||||||
|
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 = ""
|
||||||
|
) {
|
||||||
|
mxcToUrl(mxcUrl)?.let { url ->
|
||||||
|
val loadState = remember { mutableStateOf(ImageLoadState.Loading) }
|
||||||
|
val painter = rememberAsyncImagePainter(
|
||||||
|
model = ImageRequest.Builder(appCtx)
|
||||||
|
.data(url)
|
||||||
|
.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(),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,342 @@
|
|||||||
|
/*
|
||||||
|
* Created by sweetbread
|
||||||
|
* Copyright (c) 2025. All rights reserved.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package ru.risdeveau.pixeldragon.ui.layout
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.net.Uri
|
||||||
|
import android.webkit.WebResourceRequest
|
||||||
|
import android.webkit.WebView
|
||||||
|
import android.webkit.WebViewClient
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.fillMaxHeight
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.DisposableEffect
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.toArgb
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.viewinterop.AndroidView
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import org.jsoup.Jsoup
|
||||||
|
import org.jsoup.safety.Safelist
|
||||||
|
import ru.risdeveau.pixeldragon.api.Event
|
||||||
|
import ru.risdeveau.pixeldragon.api.getAccountData
|
||||||
|
import ru.risdeveau.pixeldragon.api.getEventsAround
|
||||||
|
import ru.risdeveau.pixeldragon.ui.activity.ME
|
||||||
|
import ru.risdeveau.pixeldragon.ui.item.MXCImage
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun Room(modifier: Modifier = Modifier, rid: String) {
|
||||||
|
var eventsId by remember { mutableStateOf(listOf<Event>()) }
|
||||||
|
val listState = rememberLazyListState()
|
||||||
|
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
val readMark = getAccountData(ME!!.userId, rid, "m.fully_read")
|
||||||
|
val eventsAround = getEventsAround(rid, readMark!!.getString("event_id")) //FIXME: Null check
|
||||||
|
eventsId = eventsAround.let {
|
||||||
|
it.before + listOf(it.base) + it.after
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LazyColumn(modifier = modifier, state = listState, reverseLayout = true) {
|
||||||
|
items(eventsId.reversed()) { event ->
|
||||||
|
EventItem(event)
|
||||||
|
}
|
||||||
|
|
||||||
|
item {
|
||||||
|
if (eventsId.isEmpty()) {
|
||||||
|
Text("Empty room")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun EventItem(event: Event) {
|
||||||
|
Box (Modifier.fillMaxWidth()) {
|
||||||
|
when (event.type) {
|
||||||
|
"m.room.message" -> Column(
|
||||||
|
Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.then(
|
||||||
|
if (event.sender != ME!!.userId)
|
||||||
|
Modifier.padding(end = 16.dp)
|
||||||
|
else
|
||||||
|
Modifier.padding(start = 16.dp)
|
||||||
|
)
|
||||||
|
.padding(4.dp)
|
||||||
|
.background(
|
||||||
|
if (event.sender != ME?.userId)
|
||||||
|
MaterialTheme.colorScheme.surfaceContainer
|
||||||
|
else
|
||||||
|
MaterialTheme.colorScheme.primaryContainer
|
||||||
|
)
|
||||||
|
.clip(RoundedCornerShape(16.dp))
|
||||||
|
.padding(4.dp)
|
||||||
|
) {
|
||||||
|
Text(event.sender, maxLines = 1, fontWeight = FontWeight.Bold)
|
||||||
|
|
||||||
|
when (val msgtype = event.content.optString("msgtype", null)) {
|
||||||
|
"m.text" -> when (event.content.optString("format")) {
|
||||||
|
"org.matrix.custom.html" -> {
|
||||||
|
if (event.content.getString("body") == event.content.getString("formatted_body"))
|
||||||
|
Text(event.content.getString("body"))
|
||||||
|
HtmlRenderer(event.content.getString("formatted_body"))
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> Text(event.content.getString("body"))
|
||||||
|
}
|
||||||
|
|
||||||
|
"m.image" ->
|
||||||
|
MXCImage(event.content.getString("url"), Modifier.fillMaxWidth(.9f))
|
||||||
|
|
||||||
|
null -> Text(event.content.toString(2))
|
||||||
|
|
||||||
|
else -> Text("Unknown type: $msgtype", color = MaterialTheme.colorScheme.error)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> Text(event.type,
|
||||||
|
Modifier
|
||||||
|
.fillMaxHeight()
|
||||||
|
.padding(4.dp)
|
||||||
|
.background(MaterialTheme.colorScheme.errorContainer)
|
||||||
|
.padding(4.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun String.sanitizeHTML(): String {
|
||||||
|
val matrixSafelist = Safelist()
|
||||||
|
.addTags(
|
||||||
|
"h1", "h2", "h3", "h4", "h5", "h6",
|
||||||
|
"b", "i", "u", "strong", "s", "del",
|
||||||
|
"sup", "sub", "code",
|
||||||
|
"table", "thead", "tbody",
|
||||||
|
"tr", "th", "td", "ul", "ol", "li",
|
||||||
|
"blockquote", "details", "summary",
|
||||||
|
"em", "code", "div", "pre", "span", "img"
|
||||||
|
)
|
||||||
|
// .addAttributes("span",
|
||||||
|
// "data-mx-bg-color", "data-mx-color",
|
||||||
|
// "data-mx-spoiler", "data-mx-maths"
|
||||||
|
// )
|
||||||
|
.addAttributes("a",
|
||||||
|
"target", "href"
|
||||||
|
)
|
||||||
|
.addAttributes("img",
|
||||||
|
"width", "height", "alt", "title", "src"
|
||||||
|
)
|
||||||
|
.addAttributes("ol", "start")
|
||||||
|
.addAttributes("code", "class")
|
||||||
|
.addAttributes("div", "data-mx-maths")
|
||||||
|
|
||||||
|
val doc = Jsoup.parse(this)
|
||||||
|
doc.select("mx-reply").remove()
|
||||||
|
|
||||||
|
val out = Jsoup.clean(doc.body().toString(), matrixSafelist)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun HtmlRenderer(
|
||||||
|
htmlContent: String,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
val webView = remember { WebView(context).apply {
|
||||||
|
settings.apply {
|
||||||
|
javaScriptEnabled = false
|
||||||
|
loadWithOverviewMode = true
|
||||||
|
useWideViewPort = true
|
||||||
|
}
|
||||||
|
isVerticalScrollBarEnabled = false
|
||||||
|
setBackgroundColor(Color.Transparent.toArgb())
|
||||||
|
} }
|
||||||
|
|
||||||
|
val css = """
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, sans-serif;
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: ${colorToCss(MaterialTheme.colorScheme.onBackground)};
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
h1, h2, h3, h4, h5, h6 {
|
||||||
|
font-weight: bold;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
h1 { font-size: 24px; }
|
||||||
|
h2 { font-size: 22px; }
|
||||||
|
h3 { font-size: 20px; }
|
||||||
|
h4 { font-size: 18px; }
|
||||||
|
h5 { font-size: 16px; }
|
||||||
|
h6 { font-size: 14px; }
|
||||||
|
a {
|
||||||
|
color: ${colorToCss(MaterialTheme.colorScheme.primary)};
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
img {
|
||||||
|
max-width: 100%;
|
||||||
|
height: auto;
|
||||||
|
display: block;
|
||||||
|
margin: 12px 0;
|
||||||
|
}
|
||||||
|
table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
margin: 16px 0;
|
||||||
|
}
|
||||||
|
th, td {
|
||||||
|
border: 1px solid ${colorToCss(MaterialTheme.colorScheme.outline)};
|
||||||
|
padding: 12px;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
th {
|
||||||
|
background-color: ${colorToCss(MaterialTheme.colorScheme.surfaceVariant)};
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
blockquote {
|
||||||
|
border-left: 4px solid ${colorToCss(MaterialTheme.colorScheme.primary)};
|
||||||
|
padding-left: 16px;
|
||||||
|
margin-left: 0;
|
||||||
|
color: ${colorToCss(MaterialTheme.colorScheme.onSurfaceVariant)};
|
||||||
|
}
|
||||||
|
pre {
|
||||||
|
background-color: ${colorToCss(MaterialTheme.colorScheme.surfaceVariant)};
|
||||||
|
padding: 16px;
|
||||||
|
overflow: auto;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
code {
|
||||||
|
font-family: monospace;
|
||||||
|
background-color: ${colorToCss(MaterialTheme.colorScheme.surfaceVariant)};
|
||||||
|
padding: 2px 4px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
hr {
|
||||||
|
border: 0;
|
||||||
|
height: 1px;
|
||||||
|
background-color: ${colorToCss(MaterialTheme.colorScheme.outline)};
|
||||||
|
margin: 24px 0;
|
||||||
|
}
|
||||||
|
ul, ol {
|
||||||
|
padding-left: 24px;
|
||||||
|
margin: 12px 0;
|
||||||
|
}
|
||||||
|
li {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
details {
|
||||||
|
margin: 12px 0;
|
||||||
|
}
|
||||||
|
summary {
|
||||||
|
font-weight: bold;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
:root {
|
||||||
|
--border-color: ${colorToCss(MaterialTheme.colorScheme.outline)};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""".trimIndent()
|
||||||
|
|
||||||
|
LaunchedEffect(htmlContent) {
|
||||||
|
webView.loadDataWithBaseURL(
|
||||||
|
null,
|
||||||
|
wrapHtml(htmlContent, css),
|
||||||
|
"text/html",
|
||||||
|
"UTF-8",
|
||||||
|
null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
DisposableEffect(webView) {
|
||||||
|
onDispose {
|
||||||
|
webView.destroy()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
AndroidView(
|
||||||
|
factory = { webView },
|
||||||
|
modifier = modifier,
|
||||||
|
update = { view ->
|
||||||
|
view.webViewClient = SafeWebViewClient(context)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private class SafeWebViewClient(
|
||||||
|
private val context: Context
|
||||||
|
) : WebViewClient() {
|
||||||
|
override fun shouldOverrideUrlLoading(
|
||||||
|
view: WebView,
|
||||||
|
request: WebResourceRequest
|
||||||
|
): Boolean {
|
||||||
|
val url = request.url.toString()
|
||||||
|
try {
|
||||||
|
// Открываем ссылки во внешнем браузере
|
||||||
|
context.startActivity(
|
||||||
|
Intent(Intent.ACTION_VIEW, Uri.parse(url))
|
||||||
|
)
|
||||||
|
return true
|
||||||
|
} catch (e: Exception) {
|
||||||
|
// Обработка ошибок открытия ссылки
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun wrapHtml(content: String, css: String): String {
|
||||||
|
return """
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<style>
|
||||||
|
$css
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
${content.sanitizeHTML()}
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
""".trimIndent()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun colorToCss(color: Color): String {
|
||||||
|
val argb = color.toArgb()
|
||||||
|
return String.format("#%06X", 0xFFFFFF and argb)
|
||||||
|
}
|
||||||
@@ -0,0 +1,118 @@
|
|||||||
|
/*
|
||||||
|
* Created by sweetbread
|
||||||
|
* Copyright (c) 2025. All rights reserved.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package ru.risdeveau.pixeldragon.ui.layout
|
||||||
|
|
||||||
|
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.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.layout.ContentScale
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.navigation.NavController
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import ru.risdeveau.pixeldragon.repo.Room
|
||||||
|
import ru.risdeveau.pixeldragon.ui.item.MXCImage
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun RoomList(modifier: Modifier = Modifier, navController: NavController) {
|
||||||
|
var list by remember { mutableStateOf(listOf<Room>()) }
|
||||||
|
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(list) { room ->
|
||||||
|
RoomItem(room = room, navController = navController )
|
||||||
|
}
|
||||||
|
|
||||||
|
item {
|
||||||
|
if (list.isEmpty()) {
|
||||||
|
Text("You have no rooms")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun RoomItem(modifier: Modifier = Modifier, room: Room, navController: NavController) {
|
||||||
|
val avatarUrl = room.avatarUrl ?: (room.direct?.avatarUrl ?: "")
|
||||||
|
val name = room.name ?: (room.direct?.name ?: "Unnamed")
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier
|
||||||
|
.padding(8.dp)
|
||||||
|
.height((52 + 8 * 2).dp)
|
||||||
|
.fillMaxWidth()
|
||||||
|
.background(
|
||||||
|
color =
|
||||||
|
if (room.type == "m.space")
|
||||||
|
MaterialTheme.colorScheme.tertiary
|
||||||
|
else
|
||||||
|
MaterialTheme.colorScheme.background
|
||||||
|
)
|
||||||
|
.clip(RoundedCornerShape(12.dp))
|
||||||
|
.clickable {
|
||||||
|
if (room.type == "m.space")
|
||||||
|
navController.navigate("space/${room.id}")
|
||||||
|
else
|
||||||
|
navController.navigate("room/${room.id}")
|
||||||
|
}
|
||||||
|
.padding(8.dp)
|
||||||
|
) {
|
||||||
|
MXCImage(
|
||||||
|
avatarUrl,
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(end = 4.dp)
|
||||||
|
.height(52.dp)
|
||||||
|
.width(52.dp)
|
||||||
|
.let {
|
||||||
|
if (room.type == "m.space")
|
||||||
|
it.clip(RoundedCornerShape(12.dp))
|
||||||
|
else
|
||||||
|
it.clip(CircleShape)
|
||||||
|
},
|
||||||
|
ContentScale.Crop
|
||||||
|
)
|
||||||
|
Column {
|
||||||
|
Text(room.type)
|
||||||
|
Text(
|
||||||
|
name,
|
||||||
|
maxLines = 1,
|
||||||
|
color = MaterialTheme.colorScheme.primary,
|
||||||
|
fontSize = MaterialTheme.typography.titleLarge.fontSize
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+3
-2
@@ -1,7 +1,7 @@
|
|||||||
/*
|
/*
|
||||||
* Created by sweetbread on 21.02.2025, 12:00
|
* Created by sweetbread on 22.02.2025, 15:45
|
||||||
* Copyright (c) 2025. All rights reserved.
|
* Copyright (c) 2025. All rights reserved.
|
||||||
* Last modified 21.02.2025, 12:00
|
* Last modified 21.02.2025, 12:21
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||||
@@ -9,4 +9,5 @@ plugins {
|
|||||||
alias(libs.plugins.android.application) apply false
|
alias(libs.plugins.android.application) apply false
|
||||||
alias(libs.plugins.kotlin.android) apply false
|
alias(libs.plugins.kotlin.android) apply false
|
||||||
alias(libs.plugins.kotlin.compose) apply false
|
alias(libs.plugins.kotlin.compose) apply false
|
||||||
|
id("com.google.devtools.ksp") version "2.0.21-1.0.27" apply false
|
||||||
}
|
}
|
||||||
@@ -1,21 +1,35 @@
|
|||||||
[versions]
|
[versions]
|
||||||
agp = "8.7.3"
|
agp = "8.13.0"
|
||||||
coil = "3.1.0"
|
coil = "3.1.0"
|
||||||
kotlin = "2.0.0"
|
jsoup = "1.20.1"
|
||||||
|
kotlin = "2.0.21"
|
||||||
coreKtx = "1.15.0"
|
coreKtx = "1.15.0"
|
||||||
junit = "4.13.2"
|
junit = "4.13.2"
|
||||||
junitVersion = "1.2.1"
|
junitVersion = "1.2.1"
|
||||||
espressoCore = "3.6.1"
|
espressoCore = "3.6.1"
|
||||||
|
kotlinxSerializationJson = "1.7.3"
|
||||||
ktor = "3.1.0"
|
ktor = "3.1.0"
|
||||||
lifecycleRuntimeKtx = "2.8.7"
|
lifecycleRuntimeKtx = "2.8.7"
|
||||||
activityCompose = "1.10.0"
|
activityCompose = "1.10.0"
|
||||||
composeBom = "2025.02.00"
|
composeBom = "2025.02.00"
|
||||||
|
navigationCompose = "2.8.8"
|
||||||
|
room = "2.6.1"
|
||||||
splittiesFunPackAndroidBase = "3.0.0"
|
splittiesFunPackAndroidBase = "3.0.0"
|
||||||
|
|
||||||
[libraries]
|
[libraries]
|
||||||
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
|
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-compiler = { module = "androidx.room:room-compiler", version.ref = "room" }
|
||||||
|
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-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" }
|
||||||
@@ -29,8 +43,10 @@ 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-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
|
||||||
androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
|
androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
|
||||||
androidx-material3 = { group = "androidx.compose.material3", name = "material3" }
|
androidx-material3 = { group = "androidx.compose.material3", name = "material3" }
|
||||||
ktor-client-cio = { module = "io.ktor:ktor-client-cio", version.ref = "ktor" }
|
kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinxSerializationJson" }
|
||||||
|
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-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" }
|
splitties-base = { module = "com.louiscad.splitties:splitties-fun-pack-android-base", version.ref = "splittiesFunPackAndroidBase" }
|
||||||
|
|
||||||
[plugins]
|
[plugins]
|
||||||
|
|||||||
+2
-3
@@ -1,12 +1,11 @@
|
|||||||
#
|
#
|
||||||
# Created by sweetbread on 21.02.2025, 12:00
|
# Created by sweetbread
|
||||||
# Copyright (c) 2025. All rights reserved.
|
# Copyright (c) 2025. All rights reserved.
|
||||||
# Last modified 21.02.2025, 12:00
|
|
||||||
#
|
#
|
||||||
|
|
||||||
#Thu Feb 20 10:45:47 GMT 2025
|
#Thu Feb 20 10:45:47 GMT 2025
|
||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
|
|||||||
Reference in New Issue
Block a user