feat: Login
This commit is contained in:
43
app/src/main/java/ru/risdeveau/pixeldragon/Common.kt
Executable file
43
app/src/main/java/ru/risdeveau/pixeldragon/Common.kt
Executable 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
|
||||
}
|
23
app/src/main/java/ru/risdeveau/pixeldragon/api/Server.kt
Executable file
23
app/src/main/java/ru/risdeveau/pixeldragon/api/Server.kt
Executable 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
|
||||
}
|
82
app/src/main/java/ru/risdeveau/pixeldragon/api/User.kt
Executable file
82
app/src/main/java/ru/risdeveau/pixeldragon/api/User.kt
Executable 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()
|
||||
}
|
136
app/src/main/java/ru/risdeveau/pixeldragon/ui/activity/Login.kt
Executable file
136
app/src/main/java/ru/risdeveau/pixeldragon/ui/activity/Login.kt
Executable 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) }
|
||||
}
|
||||
}
|
||||
}
|
81
app/src/main/java/ru/risdeveau/pixeldragon/ui/activity/MainActivity.kt
Executable file
81
app/src/main/java/ru/risdeveau/pixeldragon/ui/activity/MainActivity.kt
Executable 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")
|
||||
}
|
||||
}
|
17
app/src/main/java/ru/risdeveau/pixeldragon/ui/theme/Color.kt
Executable file
17
app/src/main/java/ru/risdeveau/pixeldragon/ui/theme/Color.kt
Executable 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)
|
63
app/src/main/java/ru/risdeveau/pixeldragon/ui/theme/Theme.kt
Executable file
63
app/src/main/java/ru/risdeveau/pixeldragon/ui/theme/Theme.kt
Executable 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
|
||||
)
|
||||
}
|
40
app/src/main/java/ru/risdeveau/pixeldragon/ui/theme/Type.kt
Executable file
40
app/src/main/java/ru/risdeveau/pixeldragon/ui/theme/Type.kt
Executable 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
|
||||
)
|
||||
*/
|
||||
)
|
Reference in New Issue
Block a user