feat: Login

This commit is contained in:
2025-02-21 14:44:48 +03:00
commit dc588a045d
56 changed files with 1677 additions and 0 deletions

View File

@ -0,0 +1,43 @@
/*
* Created by sweetbread on 21.02.2025, 12:01
* Copyright (c) 2025. All rights reserved.
* Last modified 21.02.2025, 12:01
*/
package ru.risdeveau.pixeldragon
import android.content.Context
import io.ktor.client.HttpClient
import io.ktor.client.engine.cio.CIO
import io.ktor.client.engine.cio.endpoint
import io.ktor.client.plugins.cache.HttpCache
import ru.risdeveau.pixeldragon.api.getMe
import splitties.init.appCtx
val client = HttpClient(CIO) {
engine {
endpoint {
maxConnectionsPerRoute = 100
pipelineMaxSize = 20
keepAliveTime = 30000
connectTimeout = 15000
connectAttempts = 5
}
}
install(HttpCache)
}
val accountData = appCtx.getSharedPreferences("settings", Context.MODE_PRIVATE)
lateinit var urlBase: String
lateinit var token: String
suspend fun initCheck(): Boolean {
if (!accountData.contains("token")) return false
if (!accountData.contains("homeserver")) return false
token = accountData.getString("token", "").toString()
urlBase = "https://${accountData.getString("homeserver", "")}/_matrix/client/v3"
return getMe() != null
}

View File

@ -0,0 +1,23 @@
/*
* Created by sweetbread on 21.02.2025, 12:01
* Copyright (c) 2025. All rights reserved.
* Last modified 21.02.2025, 12:01
*/
package ru.risdeveau.pixeldragon.api
import io.ktor.client.request.get
import io.ktor.client.statement.bodyAsText
import org.json.JSONException
import org.json.JSONObject
import ru.risdeveau.pixeldragon.client
suspend fun isMatrixServer(url: String): Boolean {
val r = try { client.get("https://$url/.well-known/matrix/client") }
catch (_: Exception) { return false }
try { JSONObject(r.bodyAsText()) }
catch (_: JSONException) { return false }
return true
}

View File

@ -0,0 +1,82 @@
/*
* Created by sweetbread on 21.02.2025, 12:09
* Copyright (c) 2025. All rights reserved.
* Last modified 21.02.2025, 12:01
*/
package ru.risdeveau.pixeldragon.api
import android.annotation.SuppressLint
import android.util.Log
import io.ktor.client.request.bearerAuth
import io.ktor.client.request.get
import io.ktor.client.request.post
import io.ktor.client.request.setBody
import io.ktor.client.statement.bodyAsText
import io.ktor.http.ContentType
import io.ktor.http.HttpStatusCode
import io.ktor.http.contentType
import org.json.JSONObject
import ru.risdeveau.pixeldragon.accountData
import 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)
/**
* This func is to validate the token
*/
suspend fun getMe(): Me? {
val r = client.get("$urlBase/account/whoami") { bearerAuth(token) }
if (r.status != HttpStatusCode.OK) {
Log.e("getMe", r.bodyAsText())
return null
}
val json = JSONObject(r.bodyAsText())
return Me(json.getString("user_id"), json.getString("device_id"))
}
@SuppressLint("ApplySharedPref")
suspend fun login(homeserver: String, login: String, pass: String): Boolean {
val pinfo = appCtx.packageManager.getPackageInfo(appCtx.packageName, 0);
val pattern = """
{
"type": "m.login.password",
"identifier": {
"type": "m.id.user"
},
"initial_device_display_name": "PixelDragon Android v${pinfo.versionName}"
}
""".trimIndent()
val json = JSONObject(pattern)
json.getJSONObject("identifier").put("user", login)
json.put("password", pass)
val r = try {
client.post("https://$homeserver/_matrix/client/v3/login") {
setBody(json.toString())
contentType(ContentType.Application.Json)
}
} catch (e: Exception) {
Log.e("login", e.toString())
return false
}
if (r.status != HttpStatusCode.OK) {
Log.e("login", r.bodyAsText())
return false // TODO: Inform a user of error code
}
val res = JSONObject(r.bodyAsText())
val editor = accountData.edit()
editor.putString("token", res.getString("access_token"))
editor.putString("homeserver", res.getString("home_server"))
editor.commit()
return initCheck()
}

View File

@ -0,0 +1,136 @@
/*
* Created by sweetbread on 21.02.2025, 12:08
* Copyright (c) 2025. All rights reserved.
* Last modified 21.02.2025, 12:08
*/
package ru.risdeveau.pixeldragon.ui.activity
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.Button
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Scaffold
import androidx.compose.material3.SnackbarDuration
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Surface
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.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
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.launch
import ru.risdeveau.pixeldragon.api.isMatrixServer
import ru.risdeveau.pixeldragon.api.login
import ru.risdeveau.pixeldragon.ui.theme.PixelDragonTheme
import splitties.activities.start
class Login : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
PixelDragonTheme {
Surface {
val snackbarHostState = remember { SnackbarHostState() }
val scope = rememberCoroutineScope()
Scaffold (
snackbarHost = {
SnackbarHost(hostState = snackbarHostState)
}
) { innerPadding ->
Box(Modifier.fillMaxSize().padding(innerPadding)) {
LoginField(
Modifier.align(Alignment.Center),
{ start<MainActivity>() },
{
scope.launch {
snackbarHostState
.showSnackbar(
message = "Login failed",
duration = SnackbarDuration.Long
)
}
}
)
}
}
}
}
}
}
}
@Composable
fun LoginField(modifier: Modifier = Modifier, ok: () -> Unit, err: () -> Unit) {
Column (
modifier = modifier
) {
val scope = rememberCoroutineScope()
var homeserver by remember { mutableStateOf("") }
var login by remember { mutableStateOf("") }
var pass by remember { mutableStateOf("") }
var hmsValid by remember { mutableStateOf(false) }
OutlinedTextField(
modifier = Modifier.padding(4.dp),
value = homeserver,
onValueChange = { homeserver = it.trim() },
label = { Text("Homeserver") },
placeholder = { Text("matrix.org") },
singleLine = true,
isError = !hmsValid
)
OutlinedTextField(
modifier = Modifier.padding(4.dp),
value = login,
onValueChange = { login = it.trim() },
label = { Text("Login") },
singleLine = true
)
OutlinedTextField(
modifier = Modifier.padding(4.dp),
value = pass,
onValueChange = { pass = it.trim() },
label = { Text("Password") },
visualTransformation = PasswordVisualTransformation(),
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password),
singleLine = true,
)
Button(
enabled = hmsValid && !login.isEmpty() && !pass.isEmpty(),
onClick = {
var loginSuccess = false
scope.launch { loginSuccess = login(homeserver, login, pass) }
if (loginSuccess) ok()
else err()
}
) {
Text("Login")
}
LaunchedEffect(homeserver) {
scope.launch { hmsValid = isMatrixServer(homeserver) }
}
}
}

View File

@ -0,0 +1,81 @@
/*
* Created by sweetbread on 21.02.2025, 12:08
* Copyright (c) 2025. All rights reserved.
* Last modified 21.02.2025, 12:07
*/
package ru.risdeveau.pixeldragon.ui.activity
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults.topAppBarColors
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import ru.risdeveau.pixeldragon.initCheck
import ru.risdeveau.pixeldragon.ui.theme.PixelDragonTheme
import splitties.activities.start
class MainActivity : ComponentActivity() {
@OptIn(ExperimentalMaterial3Api::class, DelicateCoroutinesApi::class)
override fun onCreate(savedInstanceState: Bundle?) {
GlobalScope.launch {
if (!initCheck()) start<Login>()
}
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
PixelDragonTheme {
Scaffold(
modifier = Modifier.fillMaxSize(),
topBar = {
TopAppBar(
colors = topAppBarColors(
containerColor = MaterialTheme.colorScheme.primaryContainer,
titleContentColor = MaterialTheme.colorScheme.primary,
),
title = {
Text("Top app bar")
}
)
},
) { innerPadding ->
Greeting(
name = "Android",
modifier = Modifier.padding(innerPadding)
)
}
}
}
}
}
@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
Text(
text = "Hello $name!",
modifier = modifier
)
}
@Preview(showBackground = true)
@Composable
fun GreetingPreview() {
PixelDragonTheme {
Greeting("Android")
}
}

View File

@ -0,0 +1,17 @@
/*
* Created by sweetbread on 21.02.2025, 12:01
* Copyright (c) 2025. All rights reserved.
* Last modified 21.02.2025, 12:01
*/
package ru.risdeveau.pixeldragon.ui.theme
import androidx.compose.ui.graphics.Color
val Purple80 = Color(0xFFD0BCFF)
val PurpleGrey80 = Color(0xFFCCC2DC)
val Pink80 = Color(0xFFEFB8C8)
val Purple40 = Color(0xFF6650a4)
val PurpleGrey40 = Color(0xFF625b71)
val Pink40 = Color(0xFF7D5260)

View File

@ -0,0 +1,63 @@
/*
* Created by sweetbread on 21.02.2025, 12:01
* Copyright (c) 2025. All rights reserved.
* Last modified 21.02.2025, 12:01
*/
package ru.risdeveau.pixeldragon.ui.theme
import android.os.Build
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.dynamicDarkColorScheme
import androidx.compose.material3.dynamicLightColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalContext
private val DarkColorScheme = darkColorScheme(
primary = Purple80,
secondary = PurpleGrey80,
tertiary = Pink80
)
private val LightColorScheme = lightColorScheme(
primary = Purple40,
secondary = PurpleGrey40,
tertiary = Pink40
/* Other default colors to override
background = Color(0xFFFFFBFE),
surface = Color(0xFFFFFBFE),
onPrimary = Color.White,
onSecondary = Color.White,
onTertiary = Color.White,
onBackground = Color(0xFF1C1B1F),
onSurface = Color(0xFF1C1B1F),
*/
)
@Composable
fun PixelDragonTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
// Dynamic color is available on Android 12+
dynamicColor: Boolean = true,
content: @Composable () -> Unit
) {
val colorScheme = when {
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
val context = LocalContext.current
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
}
darkTheme -> DarkColorScheme
else -> LightColorScheme
}
MaterialTheme(
colorScheme = colorScheme,
typography = Typography,
content = content
)
}

View File

@ -0,0 +1,40 @@
/*
* Created by sweetbread on 21.02.2025, 12:01
* Copyright (c) 2025. All rights reserved.
* Last modified 21.02.2025, 12:01
*/
package ru.risdeveau.pixeldragon.ui.theme
import androidx.compose.material3.Typography
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.sp
// Set of Material typography styles to start with
val Typography = Typography(
bodyLarge = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 16.sp,
lineHeight = 24.sp,
letterSpacing = 0.5.sp
)
/* Other default text styles to override
titleLarge = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 22.sp,
lineHeight = 28.sp,
letterSpacing = 0.sp
),
labelSmall = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Medium,
fontSize = 11.sp,
lineHeight = 16.sp,
letterSpacing = 0.5.sp
)
*/
)