wip: Migrate to Trixnity
This commit is contained in:
+11
-4
@@ -1,13 +1,13 @@
|
|||||||
/*
|
/*
|
||||||
* Created by sweetbread
|
* Created by sweetbread
|
||||||
* Copyright (c) 2025. All rights reserved.
|
* Copyright (c) 2026. All rights reserved.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
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")
|
// alias(libs.plugins.ksp)
|
||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
@@ -16,7 +16,7 @@ android {
|
|||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId = "ru.risdeveau.pixeldragon"
|
applicationId = "ru.risdeveau.pixeldragon"
|
||||||
minSdk = 26
|
minSdk = 28
|
||||||
targetSdk = 35
|
targetSdk = 35
|
||||||
versionCode = 1
|
versionCode = 1
|
||||||
versionName = "1.0"
|
versionName = "1.0"
|
||||||
@@ -62,6 +62,12 @@ dependencies {
|
|||||||
debugImplementation(libs.androidx.ui.tooling)
|
debugImplementation(libs.androidx.ui.tooling)
|
||||||
debugImplementation(libs.androidx.ui.test.manifest)
|
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.messenger)
|
||||||
|
|
||||||
// Ktor - web client
|
// Ktor - web client
|
||||||
implementation(libs.ktor.client.core)
|
implementation(libs.ktor.client.core)
|
||||||
implementation(libs.ktor.client.okhttp)
|
implementation(libs.ktor.client.okhttp)
|
||||||
@@ -75,7 +81,7 @@ dependencies {
|
|||||||
implementation(libs.androidx.room.runtime)
|
implementation(libs.androidx.room.runtime)
|
||||||
implementation(libs.androidx.lifecycle.viewmodel.ktx)
|
implementation(libs.androidx.lifecycle.viewmodel.ktx)
|
||||||
implementation(libs.androidx.room.ktx)
|
implementation(libs.androidx.room.ktx)
|
||||||
ksp(libs.androidx.room.compiler)
|
// ksp(libs.androidx.room.compiler)
|
||||||
|
|
||||||
// Navigation Compose
|
// Navigation Compose
|
||||||
implementation(libs.androidx.navigation.compose)
|
implementation(libs.androidx.navigation.compose)
|
||||||
@@ -87,4 +93,5 @@ dependencies {
|
|||||||
// Others
|
// Others
|
||||||
implementation(libs.splitties.base) // Syntax sugar
|
implementation(libs.splitties.base) // Syntax sugar
|
||||||
implementation(libs.jsoup) // HTML parser
|
implementation(libs.jsoup) // HTML parser
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
* Created by sweetbread
|
* Created by sweetbread
|
||||||
* Copyright (c) 2025. All rights reserved.
|
* Copyright (c) 2026. All rights reserved.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package ru.risdeveau.pixeldragon
|
package ru.risdeveau.pixeldragon
|
||||||
@@ -11,11 +11,12 @@ import io.ktor.client.plugins.cache.HttpCache
|
|||||||
import io.ktor.client.plugins.logging.LogLevel
|
import io.ktor.client.plugins.logging.LogLevel
|
||||||
import io.ktor.client.plugins.logging.Logger
|
import io.ktor.client.plugins.logging.Logger
|
||||||
import io.ktor.client.plugins.logging.Logging
|
import io.ktor.client.plugins.logging.Logging
|
||||||
import ru.risdeveau.pixeldragon.api.MatrixSyncService
|
import net.folivo.trixnity.client.MatrixClient
|
||||||
import ru.risdeveau.pixeldragon.api.getMe
|
//import ru.risdeveau.pixeldragon.api.MatrixSyncService
|
||||||
|
//import ru.risdeveau.pixeldragon.api.getMe
|
||||||
import splitties.preferences.Preferences
|
import splitties.preferences.Preferences
|
||||||
|
|
||||||
val client = HttpClient {
|
val webClient = HttpClient {
|
||||||
install(Logging) {
|
install(Logging) {
|
||||||
logger = object : Logger {
|
logger = object : Logger {
|
||||||
override fun log(message: String) {
|
override fun log(message: String) {
|
||||||
@@ -28,9 +29,7 @@ val client = HttpClient {
|
|||||||
install(HttpCache)
|
install(HttpCache)
|
||||||
}
|
}
|
||||||
|
|
||||||
lateinit var homeserver: String
|
var client: MatrixClient? = null
|
||||||
lateinit var baseUrl: String
|
|
||||||
lateinit var token: String
|
|
||||||
|
|
||||||
object AccountData : Preferences("system_parameters") {
|
object AccountData : Preferences("system_parameters") {
|
||||||
var token by stringOrNullPref("token", null)
|
var token by stringOrNullPref("token", null)
|
||||||
@@ -40,15 +39,15 @@ object AccountData : Preferences("system_parameters") {
|
|||||||
var filter by stringOrNullPref("filter", null)
|
var filter by stringOrNullPref("filter", null)
|
||||||
}
|
}
|
||||||
|
|
||||||
val syncService = MatrixSyncService()
|
//val syncService = MatrixSyncService()
|
||||||
|
|
||||||
suspend fun initCheck(): Boolean {
|
//suspend fun initCheck(): Boolean {
|
||||||
Log.d("initCheck", "checking...")
|
// Log.d("initCheck", "checking...")
|
||||||
|
//
|
||||||
token = AccountData.token ?: return false
|
// token = AccountData.token ?: return false
|
||||||
homeserver = AccountData.homeserver ?: return false
|
// homeserver = AccountData.homeserver ?: return false
|
||||||
|
//
|
||||||
baseUrl = "$homeserver/_matrix/client/v3"
|
// baseUrl = "$homeserver/_matrix/client/v3"
|
||||||
|
//
|
||||||
return getMe() != null
|
// return getMe() != null
|
||||||
}
|
//}
|
||||||
|
|||||||
@@ -1,19 +1,11 @@
|
|||||||
/*
|
/*
|
||||||
* Created by sweetbread
|
* Created by sweetbread
|
||||||
* Copyright (c) 2025. All rights reserved.
|
* Copyright (c) 2026. All rights reserved.
|
||||||
* Last modified 03.03.2025, 20:21
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package ru.risdeveau.pixeldragon.api
|
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 org.json.JSONObject
|
||||||
import ru.risdeveau.pixeldragon.baseUrl
|
|
||||||
import ru.risdeveau.pixeldragon.client
|
|
||||||
import ru.risdeveau.pixeldragon.token
|
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
|
|
||||||
class Event (
|
class Event (
|
||||||
|
|||||||
@@ -1,97 +1,4 @@
|
|||||||
/*
|
/*
|
||||||
* Created by sweetbread
|
* Created by sweetbread
|
||||||
* Copyright (c) 2025. All rights reserved.
|
* Copyright (c) 2026. 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,6 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
* Created by sweetbread
|
* Created by sweetbread
|
||||||
* Copyright (c) 2025. All rights reserved.
|
* Copyright (c) 2026. All rights reserved.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package ru.risdeveau.pixeldragon.api
|
package ru.risdeveau.pixeldragon.api
|
||||||
@@ -9,11 +9,11 @@ import io.ktor.client.request.get
|
|||||||
import io.ktor.client.statement.bodyAsText
|
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.webClient
|
||||||
import ru.risdeveau.pixeldragon.homeserver
|
//import ru.risdeveau.pixeldragon.homeserver
|
||||||
|
|
||||||
suspend fun getHomeserver(url: String): String? {
|
suspend fun getHomeserver(url: String): String? {
|
||||||
val r = try { client.get("https://$url/.well-known/matrix/client") }
|
val r = try { webClient.get("https://$url/.well-known/matrix/client") }
|
||||||
catch (_: Exception) { return null }
|
catch (_: Exception) { return null }
|
||||||
|
|
||||||
val json = try { JSONObject(r.bodyAsText()) }
|
val json = try { JSONObject(r.bodyAsText()) }
|
||||||
@@ -27,11 +27,11 @@ suspend fun getHomeserver(url: String): String? {
|
|||||||
return homeserver
|
return homeserver
|
||||||
}
|
}
|
||||||
|
|
||||||
fun mxcToUrl(mxc: String): String? {
|
//fun mxcToUrl(mxc: String): String? {
|
||||||
val pattern = Regex("mxc://([-a-zA-Z0-9@:%._+~#=]{1,256}\\.[a-zA-Z0-9()]{1,6})/([^#?]+)")
|
// val pattern = Regex("mxc://([-a-zA-Z0-9@:%._+~#=]{1,256}\\.[a-zA-Z0-9()]{1,6})/([^#?]+)")
|
||||||
val match = pattern.find(mxc)
|
// val match = pattern.find(mxc)
|
||||||
|
//
|
||||||
if ((match?.groupValues[1] == null) or (match?.groupValues[2] == null)) return null
|
// 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]}"
|
// return "$homeserver/_matrix/client/v1/media/download/${match?.groupValues[1]}/${match?.groupValues[2]}"
|
||||||
}
|
//}
|
||||||
@@ -1,100 +1,73 @@
|
|||||||
/*
|
/*
|
||||||
* Created by sweetbread
|
* Created by sweetbread
|
||||||
* Copyright (c) 2025. All rights reserved.
|
* Copyright (c) 2026. All rights reserved.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package ru.risdeveau.pixeldragon.api
|
package ru.risdeveau.pixeldragon.api
|
||||||
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import io.ktor.client.request.bearerAuth
|
import io.ktor.http.Url
|
||||||
import io.ktor.client.request.get
|
import net.folivo.trixnity.client.MatrixClient
|
||||||
import io.ktor.client.request.post
|
import net.folivo.trixnity.client.login
|
||||||
import io.ktor.client.request.setBody
|
import net.folivo.trixnity.clientserverapi.model.authentication.IdentifierType
|
||||||
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 ru.risdeveau.pixeldragon.baseUrl
|
|
||||||
import ru.risdeveau.pixeldragon.client
|
import ru.risdeveau.pixeldragon.client
|
||||||
import ru.risdeveau.pixeldragon.initCheck
|
import ru.risdeveau.pixeldragon.util.getMediaStore
|
||||||
import ru.risdeveau.pixeldragon.token
|
import ru.risdeveau.pixeldragon.util.getRoomStore
|
||||||
import splitties.experimental.ExperimentalSplittiesApi
|
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)
|
//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("$baseUrl/account/whoami") { bearerAuth(token) }
|
// val r = webClient.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
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
val json = JSONObject(r.bodyAsText())
|
// val json = JSONObject(r.bodyAsText())
|
||||||
return Me(json.getString("user_id"), json.getString("device_id"))
|
// return Me(json.getString("user_id"), json.getString("device_id"))
|
||||||
}
|
//}
|
||||||
|
|
||||||
@OptIn(ExperimentalSplittiesApi::class)
|
@OptIn(ExperimentalSplittiesApi::class)
|
||||||
suspend fun login(server: String, login: String, pass: String): Boolean {
|
suspend fun login(server: String, login: String, pass: String): Boolean {
|
||||||
val hs = getHomeserver(server)!!
|
val hs = Url(getHomeserver(server)!!)
|
||||||
|
|
||||||
val pinfo = appCtx.packageManager.getPackageInfo(appCtx.packageName, 0)
|
val pinfo = appCtx.packageManager.getPackageInfo(appCtx.packageName, 0)
|
||||||
|
|
||||||
val pattern = """
|
try {
|
||||||
{
|
client = MatrixClient.login(
|
||||||
"type": "m.login.password",
|
baseUrl = hs,
|
||||||
"identifier": {
|
identifier = IdentifierType.User(login),
|
||||||
"type": "m.id.user"
|
password = pass,
|
||||||
},
|
initialDeviceDisplayName = "PixelDragon Android v${pinfo.versionName}",
|
||||||
"initial_device_display_name": "PixelDragon Android v${pinfo.versionName}"
|
repositoriesModule = getRoomStore(appCtx),
|
||||||
}
|
mediaStore = getMediaStore()
|
||||||
""".trimIndent()
|
).getOrThrow()
|
||||||
val json = JSONObject(pattern)
|
|
||||||
json.getJSONObject("identifier").put("user", login)
|
|
||||||
json.put("password", pass)
|
|
||||||
|
|
||||||
val r = try {
|
return true
|
||||||
client.post("$hs/_matrix/client/v3/login") {
|
|
||||||
setBody(json.toString())
|
|
||||||
contentType(ContentType.Application.Json)
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e("login", e.toString())
|
Log.i("Login", "Failed to login", e)
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (r.status != HttpStatusCode.OK) {
|
return false
|
||||||
Log.e("login", r.bodyAsText())
|
|
||||||
return false // TODO: Inform a user of error code
|
|
||||||
}
|
|
||||||
|
|
||||||
val res = JSONObject(r.bodyAsText())
|
|
||||||
AccountData.edit {
|
|
||||||
token = res.getString("access_token")
|
|
||||||
homeserver = hs
|
|
||||||
}
|
|
||||||
|
|
||||||
return initCheck()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun getAccountData(user: String, state: String): JSONObject? {
|
//suspend fun getAccountData(user: String, state: String): JSONObject? {
|
||||||
val r = client.get("$baseUrl/user/$user/account_data/$state") { bearerAuth(token) }
|
// val r = webClient.get("$baseUrl/user/$user/account_data/$state") { bearerAuth(token) }
|
||||||
if (r.status != HttpStatusCode.OK) return null
|
// if (r.status != HttpStatusCode.OK) return null
|
||||||
return JSONObject(r.bodyAsText())
|
// return JSONObject(r.bodyAsText())
|
||||||
}
|
//}
|
||||||
|
//
|
||||||
suspend fun getUserProfile(userId: String): UserProfile? {
|
//suspend fun getUserProfile(userId: String): UserProfile? {
|
||||||
val r = client.get("$baseUrl/profile/$userId") { bearerAuth(token) }
|
// val r = webClient.get("$baseUrl/profile/$userId") { bearerAuth(token) }
|
||||||
if (r.status != HttpStatusCode.OK) return null
|
// if (r.status != HttpStatusCode.OK) return null
|
||||||
val json = JSONObject(r.bodyAsText())
|
// val json = JSONObject(r.bodyAsText())
|
||||||
val name = json.optString("displayname", ""); json.remove("displayname")
|
// val name = json.optString("displayname", ""); json.remove("displayname")
|
||||||
val avatar = json.optString("avatar_url", ""); json.remove("avatar_url")
|
// val avatar = json.optString("avatar_url", ""); json.remove("avatar_url")
|
||||||
return UserProfile(name, avatar, json)
|
// return UserProfile(name, avatar, json)
|
||||||
}
|
//}
|
||||||
|
|||||||
@@ -1,242 +1,4 @@
|
|||||||
/*
|
/*
|
||||||
* Created by sweetbread
|
* Created by sweetbread
|
||||||
* Copyright (c) 2025. All rights reserved.
|
* Copyright (c) 2026. 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.contentLength
|
|
||||||
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
|
|
||||||
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 (AccountData.token == null)
|
|
||||||
Log.wtf("MatrixSyncService", "Token is null")
|
|
||||||
if (AccountData.userId == null)
|
|
||||||
Log.wtf("MatrixSyncService", "User ID is null")
|
|
||||||
if (AccountData.homeserver == null)
|
|
||||||
Log.wtf("MatrixSyncService", "Homeserver is null")
|
|
||||||
|
|
||||||
Log.i("MatrixSyncService", "Start syncing")
|
|
||||||
// if (!isInitialized)
|
|
||||||
// throw IllegalStateException("Sync service not initialized")
|
|
||||||
|
|
||||||
if (syncJob?.isActive == true) return
|
|
||||||
|
|
||||||
syncJob = CoroutineScope(Dispatchers.IO).launch {
|
|
||||||
if (syncLastBatch == null) {
|
|
||||||
Log.i("MatrixSyncService", "Init syncing")
|
|
||||||
_syncState.value = SyncState.Syncing
|
|
||||||
try {
|
|
||||||
initialSync()
|
|
||||||
_syncState.value = SyncState.Idle
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e("MatrixSyncService", "Initial sync error", e)
|
|
||||||
_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 parseSyncResponse(response: JSONObject) {
|
|
||||||
Log.v("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 {
|
|
||||||
fetchRoomMeta()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fetch rooms metadata during initial sync
|
|
||||||
*/
|
|
||||||
suspend fun fetchRoomMeta() {
|
|
||||||
val filterId = getFilterId("""
|
|
||||||
{
|
|
||||||
"room": {
|
|
||||||
"state": {
|
|
||||||
"types": [
|
|
||||||
"m.room.name",
|
|
||||||
"m.room.avatar",
|
|
||||||
"m.room.canonical_alias",
|
|
||||||
"m.room.member"
|
|
||||||
],
|
|
||||||
"lazy_load_members": true
|
|
||||||
},
|
|
||||||
"timeline": {
|
|
||||||
"limit": 0,
|
|
||||||
"types": []
|
|
||||||
},
|
|
||||||
"ephemeral": {
|
|
||||||
"types": []
|
|
||||||
},
|
|
||||||
"include_leave": false
|
|
||||||
},
|
|
||||||
"presence": {
|
|
||||||
"types": []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
""".trimIndent())
|
|
||||||
// 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.v("initialSync", "Response size: ${r.contentLength()}")
|
|
||||||
|
|
||||||
r.contentLength()?.let {
|
|
||||||
if (it >= 50*1024*1024)
|
|
||||||
Log.w("initialSync", "Response size is too large")
|
|
||||||
}
|
|
||||||
val json = JSONObject(r.bodyAsText())
|
|
||||||
syncLastBatch = json.getString("next_batch")
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Send a filter to the server and get its id
|
|
||||||
* @param filter JSON Filter
|
|
||||||
* @return Filter ID
|
|
||||||
*/
|
|
||||||
suspend fun getFilterId(filter: String): String {
|
|
||||||
val userId = AccountData.userId
|
|
||||||
if (userId == null)
|
|
||||||
Log.wtf("getFilter", "user_id is not defined")
|
|
||||||
|
|
||||||
val r = client.post("$baseUrl/user/$userId/filter") {
|
|
||||||
setBody(filter)
|
|
||||||
contentType(ContentType.Application.Json)
|
|
||||||
bearerAuth(token)
|
|
||||||
}
|
|
||||||
|
|
||||||
return JSONObject(r.bodyAsText()).getString("filter_id")
|
|
||||||
}
|
|
||||||
@@ -1,41 +1,4 @@
|
|||||||
/*
|
/*
|
||||||
* Created by sweetbread
|
* Created by sweetbread
|
||||||
* Copyright (c) 2025. All rights reserved.
|
* Copyright (c) 2026. 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()
|
|
||||||
@@ -1,101 +1,4 @@
|
|||||||
/*
|
/*
|
||||||
* Created by sweetbread
|
* Created by sweetbread
|
||||||
* Copyright (c) 2025. All rights reserved.
|
* Copyright (c) 2026. 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>
|
|
||||||
)
|
|
||||||
@@ -1,60 +1,4 @@
|
|||||||
/*
|
/*
|
||||||
* Created by sweetbread
|
* Created by sweetbread
|
||||||
* Copyright (c) 2025. All rights reserved.
|
* Copyright (c) 2026. 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)
|
|
||||||
}
|
|
||||||
@@ -1,62 +1,4 @@
|
|||||||
/*
|
/*
|
||||||
* Created by sweetbread
|
* Created by sweetbread
|
||||||
* Copyright (c) 2025. All rights reserved.
|
* Copyright (c) 2026. 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() }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,51 +1,4 @@
|
|||||||
/*
|
/*
|
||||||
* Created by sweetbread
|
* Created by sweetbread
|
||||||
* Copyright (c) 2025. All rights reserved.
|
* Copyright (c) 2026. 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,16 +1,18 @@
|
|||||||
/*
|
/*
|
||||||
* Created by sweetbread
|
* Created by sweetbread
|
||||||
* Copyright (c) 2025. All rights reserved.
|
* Copyright (c) 2026. All rights reserved.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package ru.risdeveau.pixeldragon.ui.activity
|
package ru.risdeveau.pixeldragon.ui.activity
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.util.Log
|
||||||
import androidx.activity.ComponentActivity
|
import androidx.activity.ComponentActivity
|
||||||
import androidx.activity.compose.setContent
|
import androidx.activity.compose.setContent
|
||||||
import androidx.activity.enableEdgeToEdge
|
import androidx.activity.enableEdgeToEdge
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
@@ -18,23 +20,24 @@ import androidx.compose.material3.Text
|
|||||||
import androidx.compose.material3.TopAppBar
|
import androidx.compose.material3.TopAppBar
|
||||||
import androidx.compose.material3.TopAppBarDefaults.topAppBarColors
|
import androidx.compose.material3.TopAppBarDefaults.topAppBarColors
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.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.Modifier
|
||||||
import androidx.navigation.NavType
|
import androidx.navigation.NavType
|
||||||
import androidx.navigation.compose.NavHost
|
import androidx.navigation.compose.NavHost
|
||||||
import androidx.navigation.compose.composable
|
import androidx.navigation.compose.composable
|
||||||
import androidx.navigation.compose.rememberNavController
|
import androidx.navigation.compose.rememberNavController
|
||||||
import androidx.navigation.navArgument
|
import androidx.navigation.navArgument
|
||||||
import kotlinx.coroutines.Dispatchers
|
import net.folivo.trixnity.client.MatrixClient
|
||||||
import kotlinx.coroutines.withContext
|
import net.folivo.trixnity.client.fromStore
|
||||||
import ru.risdeveau.pixeldragon.AccountData
|
import ru.risdeveau.pixeldragon.client
|
||||||
import ru.risdeveau.pixeldragon.api.Me
|
|
||||||
import ru.risdeveau.pixeldragon.api.getMe
|
|
||||||
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 ru.risdeveau.pixeldragon.util.getMediaStore
|
||||||
|
import ru.risdeveau.pixeldragon.util.getRoomStore
|
||||||
import splitties.activities.start
|
import splitties.activities.start
|
||||||
|
import splitties.init.appCtx
|
||||||
|
|
||||||
|
|
||||||
class MainActivity : ComponentActivity() {
|
class MainActivity : ComponentActivity() {
|
||||||
@@ -42,50 +45,72 @@ class MainActivity : ComponentActivity() {
|
|||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
if (AccountData.token == null) {
|
|
||||||
start<Login>()
|
|
||||||
finish()
|
|
||||||
} else {
|
|
||||||
syncService.startSync()
|
|
||||||
}
|
|
||||||
|
|
||||||
enableEdgeToEdge()
|
enableEdgeToEdge()
|
||||||
setContent {
|
setContent {
|
||||||
PixelDragonTheme {
|
PixelDragonTheme {
|
||||||
Scaffold(
|
var isClientReady by remember { mutableStateOf(false) }
|
||||||
modifier = Modifier.fillMaxSize(),
|
|
||||||
topBar = {
|
|
||||||
TopAppBar(
|
|
||||||
colors = topAppBarColors(
|
|
||||||
containerColor = MaterialTheme.colorScheme.primaryContainer,
|
|
||||||
titleContentColor = MaterialTheme.colorScheme.primary,
|
|
||||||
),
|
|
||||||
title = {
|
|
||||||
Text("Top app bar")
|
|
||||||
}
|
|
||||||
)
|
|
||||||
},
|
|
||||||
) { innerPadding ->
|
|
||||||
val navController = rememberNavController()
|
|
||||||
|
|
||||||
NavHost(navController = navController, startDestination = "none") {
|
if (!isClientReady) {
|
||||||
composable("none") { }
|
CircularProgressIndicator()
|
||||||
composable("rooms") { RoomList(Modifier.padding(innerPadding), navController) }
|
} else {
|
||||||
composable(
|
Scaffold(
|
||||||
"room/{rid}",
|
modifier = Modifier.fillMaxSize(),
|
||||||
arguments = listOf(navArgument("rid") { type = NavType.StringType })
|
topBar = {
|
||||||
) { navBackStackEntry ->
|
TopAppBar(
|
||||||
|
colors = topAppBarColors(
|
||||||
|
containerColor = MaterialTheme.colorScheme.primaryContainer,
|
||||||
|
titleContentColor = MaterialTheme.colorScheme.primary,
|
||||||
|
),
|
||||||
|
title = {
|
||||||
|
Text("Top app bar")
|
||||||
|
}
|
||||||
|
)
|
||||||
|
},
|
||||||
|
) { innerPadding ->
|
||||||
|
val navController = rememberNavController()
|
||||||
|
|
||||||
|
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
|
// Room(Modifier
|
||||||
// .padding(innerPadding)
|
// .padding(innerPadding)
|
||||||
// .fillMaxSize(), navBackStackEntry.arguments!!.getString("rid")!!)
|
// .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(
|
|
||||||
"space/{rid}",
|
|
||||||
arguments = listOf(navArgument("rid") { type = NavType.StringType })
|
|
||||||
) { navBackStackEntry ->
|
|
||||||
Text(modifier = Modifier.padding(innerPadding), text = "Not implemented") }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
if (client == null) {
|
||||||
|
client = MatrixClient.fromStore(
|
||||||
|
getRoomStore(appCtx),
|
||||||
|
getMediaStore()
|
||||||
|
).getOrNull()
|
||||||
|
|
||||||
|
if (client == null) {
|
||||||
|
start<Login>()
|
||||||
|
finish()
|
||||||
|
return@LaunchedEffect
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.i("MainActivity", "Log in as ${client!!.userId}")
|
||||||
|
isClientReady = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,81 +1,4 @@
|
|||||||
/*
|
/*
|
||||||
* Created by sweetbread
|
* Created by sweetbread
|
||||||
* Copyright (c) 2025. All rights reserved.
|
* 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 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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,118 +1,4 @@
|
|||||||
/*
|
/*
|
||||||
* Created by sweetbread
|
* Created by sweetbread
|
||||||
* Copyright (c) 2025. All rights reserved.
|
* Copyright (c) 2026. 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
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
/*
|
||||||
|
* Created by sweetbread
|
||||||
|
* Copyright (c) 2026. All rights reserved.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package ru.risdeveau.pixeldragon.util
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.room.Room
|
||||||
|
import net.folivo.trixnity.client.media.okio.OkioMediaStore
|
||||||
|
import net.folivo.trixnity.client.store.repository.room.TrixnityRoomDatabase
|
||||||
|
import net.folivo.trixnity.client.store.repository.room.createRoomRepositoriesModule
|
||||||
|
import okio.Path.Companion.toPath
|
||||||
|
import org.koin.dsl.module
|
||||||
|
import splitties.init.appCtx
|
||||||
|
|
||||||
|
fun getMediaStore() = OkioMediaStore(appCtx.filesDir.resolve("media").absolutePath.toPath())
|
||||||
|
fun getRoomStore(context: Context) = module {
|
||||||
|
includes(
|
||||||
|
createRoomRepositoriesModule(
|
||||||
|
Room.databaseBuilder<TrixnityRoomDatabase>(
|
||||||
|
context,
|
||||||
|
"trixnity.db"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
+3
-5
@@ -1,7 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
* Created by sweetbread on 22.02.2025, 15:45
|
* Created by sweetbread
|
||||||
* Copyright (c) 2025. All rights reserved.
|
* Copyright (c) 2026. All rights reserved.
|
||||||
* 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,5 +8,4 @@ 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
|
}
|
||||||
}
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
agp = "8.13.0"
|
agp = "8.13.0"
|
||||||
coil = "3.1.0"
|
coil = "3.1.0"
|
||||||
jsoup = "1.20.1"
|
jsoup = "1.20.1"
|
||||||
kotlin = "2.0.21"
|
kotlin = "2.2.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"
|
||||||
@@ -15,6 +15,9 @@ composeBom = "2025.02.00"
|
|||||||
navigationCompose = "2.8.8"
|
navigationCompose = "2.8.8"
|
||||||
room = "2.6.1"
|
room = "2.6.1"
|
||||||
splittiesFunPackAndroidBase = "3.0.0"
|
splittiesFunPackAndroidBase = "3.0.0"
|
||||||
|
trixnityMessenger = "3.8.11"
|
||||||
|
trixnityClient = "4.15.0"
|
||||||
|
ksp = "2.0.21-1.0.27"
|
||||||
|
|
||||||
[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" }
|
||||||
@@ -48,9 +51,12 @@ ktor-client-okhttp = { module = "io.ktor:ktor-client-okhttp", version.ref = "kto
|
|||||||
ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor" }
|
ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor" }
|
||||||
ktor-client-logging = { module = "io.ktor:ktor-client-logging", 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" }
|
||||||
|
trixnity-client = { module = "net.folivo:trixnity-client", version.ref = "trixnityClient" }
|
||||||
|
trixnity-client-media-okio = { module = "net.folivo:trixnity-client-media-okio", version.ref = "trixnityClient" }
|
||||||
|
trixnity-client-repository-room = { module = "net.folivo:trixnity-client-repository-room", version.ref = "trixnityClient" }
|
||||||
|
trixnity-messenger = { module = "de.connect2x:trixnity-messenger", version.ref = "trixnityMessenger" }
|
||||||
|
|
||||||
[plugins]
|
[plugins]
|
||||||
android-application = { id = "com.android.application", version.ref = "agp" }
|
android-application = { id = "com.android.application", version.ref = "agp" }
|
||||||
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
|
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
|
||||||
kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
|
kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
|
||||||
|
|
||||||
|
|||||||
+3
-3
@@ -1,7 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
* Created by sweetbread on 21.02.2025, 12:00
|
* Created by sweetbread
|
||||||
* Copyright (c) 2025. All rights reserved.
|
* Copyright (c) 2026. All rights reserved.
|
||||||
* Last modified 21.02.2025, 12:00
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
pluginManagement {
|
pluginManagement {
|
||||||
@@ -22,6 +21,7 @@ dependencyResolutionManagement {
|
|||||||
repositories {
|
repositories {
|
||||||
google()
|
google()
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
|
maven("https://gitlab.com/api/v4/projects/47538655/packages/maven")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user