diff --git a/.idea/deploymentTargetSelector.xml b/.idea/deploymentTargetSelector.xml index b268ef3..c1939da 100755 --- a/.idea/deploymentTargetSelector.xml +++ b/.idea/deploymentTargetSelector.xml @@ -4,6 +4,14 @@ diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml index 6d0ee1c..c224ad5 100755 --- a/.idea/kotlinc.xml +++ b/.idea/kotlinc.xml @@ -1,6 +1,6 @@ - \ No newline at end of file diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 1b0289b..b1ea9a9 100755 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -1,13 +1,14 @@ /* - * Created by sweetbread on 21.02.2025, 12:01 + * Created by sweetbread on 22.02.2025, 15:45 * Copyright (c) 2025. All rights reserved. - * Last modified 21.02.2025, 12:01 + * Last modified 22.02.2025, 14:56 */ plugins { alias(libs.plugins.android.application) alias(libs.plugins.kotlin.android) alias(libs.plugins.kotlin.compose) + id("com.google.devtools.ksp") } android { @@ -34,11 +35,11 @@ android { } } compileOptions { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 } kotlinOptions { - jvmTarget = "1.8" + jvmTarget = "17" } buildFeatures { compose = true @@ -64,11 +65,18 @@ dependencies { // Ktor - web client implementation(libs.ktor.client.core) - implementation(libs.ktor.client.cio) + implementation(libs.ktor.client.okhttp) + implementation(libs.ktor.client.logging) // Coil - image loader implementation(libs.coil.compose) implementation(libs.coil.network.okhttp) + + // Room - database + implementation(libs.androidx.room.runtime) + implementation(libs.androidx.lifecycle.viewmodel.ktx) + implementation(libs.androidx.room.ktx) + ksp(libs.androidx.room.compiler) // Others implementation(libs.splitties.base) // Syntax sugar diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 3c4028d..ea817a3 100755 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,8 +1,8 @@ - @@ -36,6 +30,11 @@ + + \ No newline at end of file diff --git a/app/src/main/java/ru/risdeveau/pixeldragon/Common.kt b/app/src/main/java/ru/risdeveau/pixeldragon/Common.kt index 699a23f..4221258 100755 --- a/app/src/main/java/ru/risdeveau/pixeldragon/Common.kt +++ b/app/src/main/java/ru/risdeveau/pixeldragon/Common.kt @@ -1,43 +1,55 @@ /* - * Created by sweetbread on 21.02.2025, 12:01 + * Created by sweetbread on 22.02.2025, 15:45 * Copyright (c) 2025. All rights reserved. - * Last modified 21.02.2025, 12:01 + * Last modified 22.02.2025, 15:45 */ package ru.risdeveau.pixeldragon import android.content.Context +import android.util.Log +import androidx.room.Room 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.logging.LogLevel +import io.ktor.client.plugins.logging.Logger +import io.ktor.client.plugins.logging.Logging import ru.risdeveau.pixeldragon.api.getMe +import ru.risdeveau.pixeldragon.db.AppDatabase import splitties.init.appCtx -val client = HttpClient(CIO) { - engine { - endpoint { - maxConnectionsPerRoute = 100 - pipelineMaxSize = 20 - keepAliveTime = 30000 - connectTimeout = 15000 - connectAttempts = 5 +val client = HttpClient { + install(Logging) { + logger = object : Logger { + override fun log(message: String) { + Log.i("Ktor", message) + } } + level = LogLevel.ALL } install(HttpCache) } val accountData = appCtx.getSharedPreferences("settings", Context.MODE_PRIVATE) -lateinit var urlBase: String +lateinit var homeserver: String +lateinit var baseUrl: String lateinit var token: String suspend fun initCheck(): Boolean { + Log.d("initCheck", "checking...") + if (!accountData.contains("token")) return false if (!accountData.contains("homeserver")) return false token = accountData.getString("token", "").toString() - urlBase = "https://${accountData.getString("homeserver", "")}/_matrix/client/v3" + homeserver = accountData.getString("homeserver", "").toString() + baseUrl = "https://$homeserver/_matrix/client/v3" return getMe() != null -} \ No newline at end of file +} + +val db = Room.databaseBuilder( + appCtx, + AppDatabase::class.java, "database" +).build() \ No newline at end of file diff --git a/app/src/main/java/ru/risdeveau/pixeldragon/api/Room.kt b/app/src/main/java/ru/risdeveau/pixeldragon/api/Room.kt new file mode 100755 index 0000000..7453d24 --- /dev/null +++ b/app/src/main/java/ru/risdeveau/pixeldragon/api/Room.kt @@ -0,0 +1,56 @@ +/* + * Created by sweetbread on 22.02.2025, 15:45 + * Copyright (c) 2025. All rights reserved. + * Last modified 22.02.2025, 15:45 + */ + +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 org.json.JSONObject +import ru.risdeveau.pixeldragon.baseUrl +import ru.risdeveau.pixeldragon.client +import ru.risdeveau.pixeldragon.db +import ru.risdeveau.pixeldragon.db.Room +import ru.risdeveau.pixeldragon.token + +//fun getRooms(): List { +// return db.roomDoa().getAllJoined() +//} +// +//fun updateRooms(): List { +// +//} + +suspend fun getRooms(): List { + 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)) } +} + +suspend fun getRoom(rid: String): Room { + var room = db.roomDoa().getById(rid) + if (room == null) { + val name = getState(rid, "m.room.name", "name") + val creator = getState(rid, "m.room.create", "creator") + val avatar = getState(rid, "m.room.avatar", "url") + room = Room(rid, name, creator, null, avatar, null, true) + db.roomDoa().insert(room) + } + + return room +} + +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) +} \ No newline at end of file diff --git a/app/src/main/java/ru/risdeveau/pixeldragon/api/Server.kt b/app/src/main/java/ru/risdeveau/pixeldragon/api/Server.kt index 7a854de..a0bdd33 100755 --- a/app/src/main/java/ru/risdeveau/pixeldragon/api/Server.kt +++ b/app/src/main/java/ru/risdeveau/pixeldragon/api/Server.kt @@ -1,7 +1,7 @@ /* - * Created by sweetbread on 21.02.2025, 12:01 + * Created by sweetbread on 22.02.2025, 17:28 * Copyright (c) 2025. All rights reserved. - * Last modified 21.02.2025, 12:01 + * Last modified 22.02.2025, 17:28 */ package ru.risdeveau.pixeldragon.api @@ -11,6 +11,7 @@ import io.ktor.client.statement.bodyAsText import org.json.JSONException import org.json.JSONObject import ru.risdeveau.pixeldragon.client +import ru.risdeveau.pixeldragon.homeserver suspend fun isMatrixServer(url: String): Boolean { val r = try { client.get("https://$url/.well-known/matrix/client") } @@ -20,4 +21,11 @@ suspend fun isMatrixServer(url: String): Boolean { catch (_: JSONException) { return false } return true +} + +fun mxcToUrl(mxc: String): String { + val pattern = Regex("mxc://([-a-zA-Z0-9@:%._+~#=]{1,256}\\.[a-zA-Z0-9()]{1,6})/([a-zA-Z]+)") + val match = pattern.find(mxc) + + return "https://$homeserver/_matrix/client/v1/media/download/${match?.groupValues[1]}/${match?.groupValues[2]}" } \ No newline at end of file diff --git a/app/src/main/java/ru/risdeveau/pixeldragon/api/User.kt b/app/src/main/java/ru/risdeveau/pixeldragon/api/User.kt index 742f8a6..a8758df 100755 --- a/app/src/main/java/ru/risdeveau/pixeldragon/api/User.kt +++ b/app/src/main/java/ru/risdeveau/pixeldragon/api/User.kt @@ -1,7 +1,7 @@ /* - * Created by sweetbread on 21.02.2025, 12:09 + * Created by sweetbread on 22.02.2025, 15:45 * Copyright (c) 2025. All rights reserved. - * Last modified 21.02.2025, 12:01 + * Last modified 22.02.2025, 15:45 */ package ru.risdeveau.pixeldragon.api @@ -18,10 +18,10 @@ 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.initCheck import ru.risdeveau.pixeldragon.token -import ru.risdeveau.pixeldragon.urlBase import splitties.init.appCtx data class Me (val userId: String, val deviceId: String) @@ -30,7 +30,7 @@ data class Me (val userId: String, val deviceId: String) * This func is to validate the token */ suspend fun getMe(): Me? { - val r = client.get("$urlBase/account/whoami") { bearerAuth(token) } + val r = client.get("$baseUrl/account/whoami") { bearerAuth(token) } if (r.status != HttpStatusCode.OK) { Log.e("getMe", r.bodyAsText()) return null diff --git a/app/src/main/java/ru/risdeveau/pixeldragon/db/Room.kt b/app/src/main/java/ru/risdeveau/pixeldragon/db/Room.kt new file mode 100755 index 0000000..4c7828c --- /dev/null +++ b/app/src/main/java/ru/risdeveau/pixeldragon/db/Room.kt @@ -0,0 +1,80 @@ +/* + * Created by sweetbread on 22.02.2025, 15:45 + * Copyright (c) 2025. All rights reserved. + * Last modified 22.02.2025, 15:45 + */ + +package ru.risdeveau.pixeldragon.db + +import androidx.room.Dao +import androidx.room.Database +import androidx.room.Delete +import androidx.room.Embedded +import androidx.room.Entity +import androidx.room.Insert +import androidx.room.Junction +import androidx.room.PrimaryKey +import androidx.room.Query +import androidx.room.Relation +import androidx.room.RoomDatabase + +@Entity +data class Room( + @PrimaryKey val roomId: String, + val name: String?, + val creatorId: String?, + val createTime: Long?, + val avatarUrl: String?, + val members: Int?, + val joined: Boolean +) + +@Dao +interface RoomDao { +// @Query("SELECT * FROM room") +// fun getAll(): List + +// @Query("SELECT * FROM user WHERE uid IN (:userIds)") +// fun loadAllByIds(userIds: IntArray): List + +// @Query("SELECT * FROM user WHERE first_name LIKE :first AND " + +// "last_name LIKE :last LIMIT 1") +// fun findByName(first: String, last: String): User + + @Query("SELECT * FROM room WHERE roomId LIKE :rid LIMIT 1") + fun getById(rid: String): Room? + +// @Transaction +// @Query("SELECT * FROM room WHERE ") +// fun getSpace(rid: String): Space + + @Query("SELECT * FROM room WHERE joined = 1 AND roomId NOT IN (SELECT roomId FROM SpaceToRoom)") + fun getAllJoined(): List + + @Insert + fun insert(vararg rooms: Room) + + @Delete + fun delete(room: Room) +} + +@Entity(primaryKeys = ["spaceId", "roomId"]) +data class SpaceToRoom( + val spaceId: String, + val roomId: String +) + +data class Space( + @Embedded val space: Room, + @Relation( + parentColumn = "spaceId", + entityColumn = "roomId", + associateBy = Junction(SpaceToRoom::class) + ) + val children: List +) + +@Database(entities = [Room::class, SpaceToRoom::class, User::class], version = 1) +abstract class AppDatabase : RoomDatabase() { + abstract fun roomDoa(): RoomDao +} \ No newline at end of file diff --git a/app/src/main/java/ru/risdeveau/pixeldragon/db/User.kt b/app/src/main/java/ru/risdeveau/pixeldragon/db/User.kt new file mode 100755 index 0000000..2e0e102 --- /dev/null +++ b/app/src/main/java/ru/risdeveau/pixeldragon/db/User.kt @@ -0,0 +1,18 @@ +/* + * Created by sweetbread on 22.02.2025, 15:45 + * Copyright (c) 2025. All rights reserved. + * Last modified 21.02.2025, 13:38 + */ + +package ru.risdeveau.pixeldragon.db + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.PrimaryKey + +@Entity +data class User( + @PrimaryKey val uid: String, + @ColumnInfo(name = "name") val name: String?, + @ColumnInfo(name = "avatar") val avatar: String? +) \ No newline at end of file diff --git a/app/src/main/java/ru/risdeveau/pixeldragon/ui/activity/Login.kt b/app/src/main/java/ru/risdeveau/pixeldragon/ui/activity/Login.kt index 7268283..9ef27d4 100755 --- a/app/src/main/java/ru/risdeveau/pixeldragon/ui/activity/Login.kt +++ b/app/src/main/java/ru/risdeveau/pixeldragon/ui/activity/Login.kt @@ -1,7 +1,7 @@ /* - * Created by sweetbread on 21.02.2025, 12:08 + * Created by sweetbread on 22.02.2025, 15:45 * Copyright (c) 2025. All rights reserved. - * Last modified 21.02.2025, 12:08 + * Last modified 22.02.2025, 15:45 */ package ru.risdeveau.pixeldragon.ui.activity @@ -35,15 +35,25 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.PasswordVisualTransformation import androidx.compose.ui.unit.dp +import kotlinx.coroutines.DelicateCoroutinesApi +import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch import ru.risdeveau.pixeldragon.api.isMatrixServer import ru.risdeveau.pixeldragon.api.login +import ru.risdeveau.pixeldragon.initCheck import ru.risdeveau.pixeldragon.ui.theme.PixelDragonTheme import splitties.activities.start class Login : ComponentActivity() { + @OptIn(DelicateCoroutinesApi::class) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + + + GlobalScope.launch { + if (initCheck()) start() + } + enableEdgeToEdge() setContent { PixelDragonTheme { @@ -56,7 +66,10 @@ class Login : ComponentActivity() { SnackbarHost(hostState = snackbarHostState) } ) { innerPadding -> - Box(Modifier.fillMaxSize().padding(innerPadding)) { + Box( + Modifier + .fillMaxSize() + .padding(innerPadding)) { LoginField( Modifier.align(Alignment.Center), { start() }, @@ -119,11 +132,10 @@ fun LoginField(modifier: Modifier = Modifier, ok: () -> Unit, err: () -> Unit) { Button( enabled = hmsValid && !login.isEmpty() && !pass.isEmpty(), onClick = { - var loginSuccess = false - scope.launch { loginSuccess = login(homeserver, login, pass) } - - if (loginSuccess) ok() - else err() + scope.launch { + if (login(homeserver, login, pass)) ok() + else err() + } } ) { Text("Login") diff --git a/app/src/main/java/ru/risdeveau/pixeldragon/ui/activity/MainActivity.kt b/app/src/main/java/ru/risdeveau/pixeldragon/ui/activity/MainActivity.kt index 0460440..fa8f8dd 100755 --- a/app/src/main/java/ru/risdeveau/pixeldragon/ui/activity/MainActivity.kt +++ b/app/src/main/java/ru/risdeveau/pixeldragon/ui/activity/MainActivity.kt @@ -1,7 +1,7 @@ /* - * Created by sweetbread on 21.02.2025, 12:08 + * Created by sweetbread on 22.02.2025, 15:45 * Copyright (c) 2025. All rights reserved. - * Last modified 21.02.2025, 12:07 + * Last modified 22.02.2025, 15:45 */ package ru.risdeveau.pixeldragon.ui.activity @@ -10,8 +10,15 @@ import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height 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.material3.CircularProgressIndicator import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold @@ -19,22 +26,31 @@ import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBarDefaults.topAppBarColors import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier -import androidx.compose.ui.tooling.preview.Preview -import kotlinx.coroutines.DelicateCoroutinesApi -import kotlinx.coroutines.GlobalScope +import androidx.compose.ui.unit.dp +import coil3.compose.SubcomposeAsyncImage +import coil3.network.NetworkHeaders +import coil3.network.httpHeaders +import coil3.request.ImageRequest +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch -import ru.risdeveau.pixeldragon.initCheck +import kotlinx.coroutines.withContext +import ru.risdeveau.pixeldragon.api.getRooms +import ru.risdeveau.pixeldragon.api.mxcToUrl +import ru.risdeveau.pixeldragon.db.Room +import ru.risdeveau.pixeldragon.token import ru.risdeveau.pixeldragon.ui.theme.PixelDragonTheme -import splitties.activities.start +import splitties.init.appCtx class MainActivity : ComponentActivity() { - @OptIn(ExperimentalMaterial3Api::class, DelicateCoroutinesApi::class) + @OptIn(ExperimentalMaterial3Api::class) override fun onCreate(savedInstanceState: Bundle?) { - GlobalScope.launch { - if (!initCheck()) start() - } - super.onCreate(savedInstanceState) enableEdgeToEdge() @@ -54,10 +70,7 @@ class MainActivity : ComponentActivity() { ) }, ) { innerPadding -> - Greeting( - name = "Android", - modifier = Modifier.padding(innerPadding) - ) + RoomList(Modifier.padding(innerPadding)) } } } @@ -65,17 +78,67 @@ class MainActivity : ComponentActivity() { } @Composable -fun Greeting(name: String, modifier: Modifier = Modifier) { - Text( - text = "Hello $name!", - modifier = modifier - ) +fun RoomList(modifier: Modifier = Modifier) { + var list by remember { mutableStateOf(listOf()) } + val coroutineScope = rememberCoroutineScope() + val listState = rememberLazyListState() + +// if (itemState.scrollToTop) { +// LaunchedEffect(coroutineScope) { +// Log.e("TAG", "TopCoinsScreen: scrollToTop" ) +// listState.scrollToItem(0) +// } +// } + + LaunchedEffect(true) { + coroutineScope.launch { + withContext(Dispatchers.IO) { + list = getRooms() + } + } + } + + LazyColumn(modifier = modifier, state = listState) { + items(list) { room -> + RoomItem(room = room) + } + + item { + if (list.isEmpty()) { + Text("You have no rooms") + } + } + } } -@Preview(showBackground = true) @Composable -fun GreetingPreview() { - PixelDragonTheme { - Greeting("Android") +fun RoomItem(modifier: Modifier = Modifier, room: Room) { + Row ( + modifier.height(64.dp).fillMaxWidth().padding(8.dp) + ) { + SubcomposeAsyncImage( + model = ImageRequest.Builder(appCtx) + .data(mxcToUrl(room.avatarUrl ?: "")) + .httpHeaders(NetworkHeaders.Builder() + .set("Authorization", "Bearer $token") + .build()) + .build(), + contentDescription = room.roomId, + loading = { + CircularProgressIndicator() + } + ) +// Column { + Text(room.name ?: "Unknown") +// } } -} \ No newline at end of file +} + +//@Preview(showBackground = true) +//@Composable +//fun GreetingPreview() { +// PixelDragonTheme { +// RoomItem() +// } +//} + diff --git a/build.gradle.kts b/build.gradle.kts index c57324f..b55b582 100755 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -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. - * 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. @@ -9,4 +9,5 @@ plugins { alias(libs.plugins.android.application) apply false alias(libs.plugins.kotlin.android) apply false alias(libs.plugins.kotlin.compose) apply false + id("com.google.devtools.ksp") version "2.0.21-1.0.27" apply false } \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 28fbf1c..4d46867 100755 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,7 +1,7 @@ [versions] agp = "8.7.3" coil = "3.1.0" -kotlin = "2.0.0" +kotlin = "2.0.21" coreKtx = "1.15.0" junit = "4.13.2" junitVersion = "1.2.1" @@ -10,10 +10,15 @@ ktor = "3.1.0" lifecycleRuntimeKtx = "2.8.7" activityCompose = "1.10.0" composeBom = "2025.02.00" +room = "2.6.1" splittiesFunPackAndroidBase = "3.0.0" [libraries] androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } +androidx-lifecycle-viewmodel-ktx = { module = "androidx.lifecycle:lifecycle-viewmodel-ktx", version.ref = "room" } +androidx-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-network-okhttp = { module = "io.coil-kt.coil3:coil-network-okhttp", version.ref = "coil" } junit = { group = "junit", name = "junit", version.ref = "junit" } @@ -29,8 +34,9 @@ androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-toolin androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" } androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" } androidx-material3 = { group = "androidx.compose.material3", name = "material3" } -ktor-client-cio = { module = "io.ktor:ktor-client-cio", version.ref = "ktor" } +ktor-client-okhttp = { module = "io.ktor:ktor-client-okhttp", version.ref = "ktor" } ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor" } +ktor-client-logging = { module = "io.ktor:ktor-client-logging", version.ref = "ktor" } splitties-base = { module = "com.louiscad.splitties:splitties-fun-pack-android-base", version.ref = "splittiesFunPackAndroidBase" } [plugins]