Set up multiplatform structure
@@ -0,0 +1,8 @@
|
||||
<component name="ArtifactManager">
|
||||
<artifact type="jar" name="utils">
|
||||
<output-path>$PROJECT_DIR$/utils/build/libs</output-path>
|
||||
<root id="archive" name="utils.jar">
|
||||
<element id="module-output" name="FromChat.utils.androidMain" />
|
||||
</root>
|
||||
</artifact>
|
||||
</component>
|
||||
@@ -10,7 +10,8 @@
|
||||
<option name="modules">
|
||||
<set>
|
||||
<option value="$PROJECT_DIR$" />
|
||||
<option value="$PROJECT_DIR$/app" />
|
||||
<option value="$PROJECT_DIR$/composeApp" />
|
||||
<option value="$PROJECT_DIR$/utils" />
|
||||
</set>
|
||||
</option>
|
||||
</GradleProjectSettings>
|
||||
|
||||
@@ -1,91 +0,0 @@
|
||||
|
||||
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
||||
import java.io.FileInputStream
|
||||
import java.util.Properties
|
||||
|
||||
plugins {
|
||||
alias(libs.plugins.android.application)
|
||||
alias(libs.plugins.kotlin.android)
|
||||
alias(libs.plugins.kotlin.compose)
|
||||
alias(libs.plugins.kotlin.serialization)
|
||||
}
|
||||
|
||||
kotlin {
|
||||
compilerOptions {
|
||||
jvmTarget.set(JvmTarget.JVM_11)
|
||||
freeCompilerArgs.add("-Xwarning-level=NOTHING_TO_INLINE:disabled")
|
||||
}
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "ru.fromchat"
|
||||
compileSdk = 36
|
||||
|
||||
defaultConfig {
|
||||
applicationId = "ru.fromchat"
|
||||
minSdk = 24
|
||||
targetSdk = 36
|
||||
versionCode = 1
|
||||
versionName = "1.0"
|
||||
}
|
||||
|
||||
signingConfigs {
|
||||
create("release") {
|
||||
val keystoreProperties = Properties().apply {
|
||||
load(FileInputStream(rootProject.file("keys/keystore.properties")))
|
||||
}
|
||||
storeFile = rootProject.file("keys/release.jks")
|
||||
keyAlias = "release"
|
||||
storePassword = keystoreProperties["storePassword"].toString()
|
||||
keyPassword = keystoreProperties["keyPassword"].toString()
|
||||
enableV3Signing = true
|
||||
}
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
isMinifyEnabled = true
|
||||
isShrinkResources = true
|
||||
signingConfig = signingConfigs.getByName("release")
|
||||
|
||||
proguardFiles(
|
||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||
"proguard-rules.pro"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_11
|
||||
targetCompatibility = JavaVersion.VERSION_11
|
||||
}
|
||||
|
||||
buildFeatures {
|
||||
compose = true
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(libs.androidx.core.ktx)
|
||||
implementation(libs.androidx.lifecycle.runtime.ktx)
|
||||
implementation(libs.androidx.activity.compose)
|
||||
implementation(platform(libs.androidx.compose.bom))
|
||||
implementation(libs.androidx.ui)
|
||||
implementation(libs.androidx.ui.graphics)
|
||||
implementation(libs.androidx.ui.tooling.preview)
|
||||
implementation(libs.androidx.material3)
|
||||
implementation(libs.androidx.navigation.compose)
|
||||
implementation(libs.androidx.material.icons.extended)
|
||||
implementation(libs.kotlinx.datetime)
|
||||
|
||||
implementation(libs.ktor.client.core)
|
||||
implementation(libs.ktor.client.okhttp)
|
||||
implementation(libs.ktor.client.websockets)
|
||||
implementation(libs.ktor.client.logging)
|
||||
implementation(libs.ktor.client.content.negotiation)
|
||||
implementation(libs.ktor.serialization.json)
|
||||
implementation(libs.kotlinx.serialization.json)
|
||||
implementation(libs.haze)
|
||||
|
||||
debugImplementation(libs.androidx.ui.tooling)
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
package ru.fromchat
|
||||
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
import org.junit.Assert.*
|
||||
|
||||
/**
|
||||
* Instrumented test, which will execute on an Android device.
|
||||
*
|
||||
* See [testing documentation](http://d.android.com/tools/testing).
|
||||
*/
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class ExampleInstrumentedTest {
|
||||
@Test
|
||||
fun useAppContext() {
|
||||
// Context of the app under test.
|
||||
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
|
||||
assertEquals("ru.fromchat", appContext.packageName)
|
||||
}
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
package ru.fromchat.utils
|
||||
|
||||
import android.util.Base64
|
||||
import javax.crypto.Mac
|
||||
import javax.crypto.spec.SecretKeySpec
|
||||
|
||||
object HashUtils {
|
||||
/**
|
||||
* Derive authentication secret using HKDF (HMAC-based Key Derivation Function)
|
||||
* Matches the frontend implementation in deriveAuthSecret
|
||||
*
|
||||
* @param username The username
|
||||
* @param password The plain password
|
||||
* @return Base64-encoded 32-byte derived secret
|
||||
*/
|
||||
fun deriveAuthSecret(username: String, password: String): String {
|
||||
val salt = "fromchat.user:$username".toByteArray(Charsets.UTF_8)
|
||||
val ikm = password.toByteArray(Charsets.UTF_8)
|
||||
val info = "auth-secret".toByteArray(Charsets.UTF_8)
|
||||
val length = 32
|
||||
|
||||
val prk = hkdfExtract(salt, ikm)
|
||||
val okm = hkdfExpand(prk, info, length)
|
||||
|
||||
return Base64.encodeToString(okm, Base64.NO_WRAP)
|
||||
}
|
||||
|
||||
/**
|
||||
* HKDF Extract step: PRK = HMAC-Hash(salt, IKM)
|
||||
*/
|
||||
private fun hkdfExtract(salt: ByteArray, ikm: ByteArray): ByteArray {
|
||||
val mac = Mac.getInstance("HmacSHA256")
|
||||
val keySpec = SecretKeySpec(salt, "HmacSHA256")
|
||||
mac.init(keySpec)
|
||||
return mac.doFinal(ikm)
|
||||
}
|
||||
|
||||
/**
|
||||
* HKDF Expand step: OKM = HKDF-Expand(PRK, info, L)
|
||||
*/
|
||||
private fun hkdfExpand(prk: ByteArray, info: ByteArray, length: Int): ByteArray {
|
||||
val blocks = mutableListOf<ByteArray>()
|
||||
var previous = ByteArray(0)
|
||||
var counter = 1
|
||||
|
||||
while (blocks.sumOf { it.size } < length) {
|
||||
val mac = Mac.getInstance("HmacSHA256")
|
||||
val keySpec = SecretKeySpec(prk, "HmacSHA256")
|
||||
mac.init(keySpec)
|
||||
|
||||
val input = previous + info + byteArrayOf(counter.toByte())
|
||||
previous = mac.doFinal(input)
|
||||
blocks.add(previous)
|
||||
counter++
|
||||
}
|
||||
|
||||
val result = ByteArray(length)
|
||||
var offset = 0
|
||||
for (block in blocks) {
|
||||
val toCopy = minOf(block.size, length - offset)
|
||||
System.arraycopy(block, 0, result, offset, toCopy)
|
||||
offset += toCopy
|
||||
if (offset >= length) break
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 982 B |
|
Before Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 5.8 KiB |
|
Before Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 7.6 KiB |
@@ -1,7 +1,11 @@
|
||||
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||
plugins {
|
||||
alias(libs.plugins.android.application) apply false
|
||||
alias(libs.plugins.kotlin.android) apply false
|
||||
alias(libs.plugins.kotlin.compose) apply false
|
||||
alias(libs.plugins.kotlin.serialization) apply false
|
||||
// this is necessary to avoid the plugins to be loaded multiple times
|
||||
// in each subproject's classloader
|
||||
alias(libs.plugins.androidApplication) apply false
|
||||
alias(libs.plugins.androidLibrary) apply false
|
||||
alias(libs.plugins.composeMultiplatform) apply false
|
||||
alias(libs.plugins.composeCompiler) apply false
|
||||
alias(libs.plugins.kotlinMultiplatform) apply false
|
||||
alias(libs.plugins.androidKotlinMultiplatformLibrary) apply false
|
||||
alias(libs.plugins.jetbrains.kotlin.android) apply false
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
import java.io.FileInputStream
|
||||
import java.util.Properties
|
||||
|
||||
plugins {
|
||||
alias(libs.plugins.kotlinMultiplatform)
|
||||
alias(libs.plugins.androidApplication)
|
||||
alias(libs.plugins.composeMultiplatform)
|
||||
alias(libs.plugins.composeCompiler)
|
||||
kotlin("plugin.serialization") version "1.9.22"
|
||||
}
|
||||
|
||||
kotlin {
|
||||
androidTarget()
|
||||
|
||||
compilerOptions {
|
||||
freeCompilerArgs.addAll("-Xexpect-actual-classes")
|
||||
}
|
||||
|
||||
listOf(
|
||||
iosX64(),
|
||||
iosArm64(),
|
||||
iosSimulatorArm64()
|
||||
).forEach { iosTarget ->
|
||||
iosTarget.binaries.framework {
|
||||
baseName = "ComposeApp"
|
||||
isStatic = true
|
||||
}
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
androidMain.dependencies {
|
||||
implementation(compose.preview)
|
||||
implementation(libs.androidx.activity.compose)
|
||||
implementation(libs.androidx.core.splashscreen)
|
||||
implementation(libs.androidx.adaptive.android)
|
||||
implementation(libs.ktor.client.okhttp)
|
||||
implementation(libs.slf4j.android)
|
||||
}
|
||||
|
||||
commonMain.dependencies {
|
||||
implementation(compose.runtime)
|
||||
implementation(compose.foundation)
|
||||
implementation(compose.material3)
|
||||
implementation(compose.ui)
|
||||
implementation(compose.components.resources)
|
||||
implementation(compose.components.uiToolingPreview)
|
||||
implementation(libs.constraintlayout)
|
||||
implementation(libs.navigation.compose)
|
||||
implementation(compose.materialIconsExtended)
|
||||
|
||||
// Serialization
|
||||
implementation(libs.kotlinx.serialization.json)
|
||||
|
||||
implementation(libs.kotlinx.io.core)
|
||||
|
||||
// Ktor
|
||||
implementation(libs.ktor.client.core)
|
||||
implementation(libs.ktor.client.content.negotiation)
|
||||
implementation(libs.ktor.client.serialization.kotlinx.json)
|
||||
implementation(libs.ktor.client.websockets)
|
||||
implementation(libs.ktor.client.logging)
|
||||
|
||||
// Datetime
|
||||
implementation(libs.kotlinx.datetime)
|
||||
|
||||
implementation(project(":utils"))
|
||||
}
|
||||
|
||||
iosMain.dependencies {
|
||||
implementation(libs.jetbrains.kotlinx.io.bytestring)
|
||||
implementation(libs.jetbrains.kotlinx.coroutines.core)
|
||||
implementation(libs.ktor.client.darwin)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "ru.fromchat"
|
||||
compileSdk = 36
|
||||
|
||||
defaultConfig {
|
||||
applicationId = "ru.fromchat"
|
||||
minSdk = 24
|
||||
targetSdk = 36
|
||||
versionCode = 1
|
||||
versionName = "1.0"
|
||||
}
|
||||
|
||||
signingConfigs {
|
||||
create("release") {
|
||||
val keystoreProperties = Properties().apply {
|
||||
load(FileInputStream(rootProject.file("keys/keystore.properties")))
|
||||
}
|
||||
storeFile = rootProject.file("keys/release.jks")
|
||||
keyAlias = "release"
|
||||
storePassword = keystoreProperties["storePassword"].toString()
|
||||
keyPassword = keystoreProperties["keyPassword"].toString()
|
||||
enableV3Signing = true
|
||||
}
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
isMinifyEnabled = true
|
||||
isShrinkResources = true
|
||||
signingConfig = signingConfigs.getByName("release")
|
||||
|
||||
proguardFiles(
|
||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||
"proguard-rules.pro"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
packaging {
|
||||
resources {
|
||||
excludes += "/META-INF/{AL2.0,LGPL2.1}"
|
||||
}
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_11
|
||||
targetCompatibility = JavaVersion.VERSION_11
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
debugImplementation(compose.uiTooling)
|
||||
}
|
||||
|
||||
compose.resources {
|
||||
publicResClass = false
|
||||
packageOfResClass = "ru.fromchat"
|
||||
generateResClass = auto
|
||||
}
|
||||
|
||||
tasks.register("generateResourceAccessors") {
|
||||
dependsOn(
|
||||
*(
|
||||
tasks.filter {
|
||||
it.name.startsWith("generateResourceAccessors") &&
|
||||
!it.name.matches("^(:${project.name})?generateResourceAccessors$".toRegex())
|
||||
}.toTypedArray()
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -14,4 +14,4 @@ class MainActivity : ComponentActivity() {
|
||||
App()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package ru.fromchat.api
|
||||
|
||||
import android.util.Log
|
||||
import io.ktor.client.plugins.logging.Logger
|
||||
|
||||
/**
|
||||
* Custom Android logger for Ktor that uses Android's Log system
|
||||
*/
|
||||
object AndroidKtorLogger : Logger {
|
||||
private const val TAG = "Ktor"
|
||||
|
||||
override fun log(message: String) {
|
||||
Log.d(TAG, message)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
package ru.fromchat.api
|
||||
|
||||
import io.ktor.client.HttpClient
|
||||
import io.ktor.client.HttpClientConfig
|
||||
import io.ktor.client.engine.okhttp.OkHttp
|
||||
import io.ktor.client.engine.okhttp.OkHttpConfig
|
||||
|
||||
actual fun createPlatformHttpClient(
|
||||
block: HttpClientConfig<*>.() -> Unit
|
||||
): HttpClient {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
return HttpClient(OkHttp, block as HttpClientConfig<OkHttpConfig>.() -> Unit)
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package ru.fromchat.core
|
||||
|
||||
import android.util.Log
|
||||
|
||||
actual object Logger {
|
||||
actual fun d(tag: String, message: String, throwable: Throwable?) {
|
||||
Log.d(tag, message, throwable)
|
||||
}
|
||||
|
||||
actual fun i(tag: String, message: String, throwable: Throwable?) {
|
||||
Log.i(tag, message, throwable)
|
||||
}
|
||||
|
||||
actual fun w(tag: String, message: String, throwable: Throwable?) {
|
||||
Log.w(tag, message, throwable)
|
||||
}
|
||||
|
||||
actual fun e(tag: String, message: String, throwable: Throwable?) {
|
||||
Log.e(tag, message, throwable)
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,12 @@
|
||||
package ru.fromchat.api
|
||||
|
||||
import android.util.Log
|
||||
import io.ktor.client.HttpClient
|
||||
import io.ktor.client.call.body
|
||||
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
|
||||
import io.ktor.client.plugins.logging.LogLevel
|
||||
import io.ktor.client.plugins.logging.Logger
|
||||
import io.ktor.client.plugins.logging.Logging
|
||||
import io.ktor.client.plugins.logging.SIMPLE
|
||||
import io.ktor.client.plugins.websocket.WebSockets
|
||||
import io.ktor.client.request.bearerAuth
|
||||
import io.ktor.client.request.delete
|
||||
@@ -20,6 +20,15 @@ import io.ktor.serialization.kotlinx.json.json
|
||||
import kotlinx.serialization.json.Json
|
||||
import ru.fromchat.API_HOST
|
||||
import ru.fromchat.utils.failOnError
|
||||
import kotlin.concurrent.Volatile
|
||||
|
||||
/**
|
||||
* Creates a platform-specific HTTP client that supports WebSockets
|
||||
* The config block is applied to configure plugins like WebSockets, JSON, etc.
|
||||
*/
|
||||
expect fun createPlatformHttpClient(
|
||||
block: io.ktor.client.HttpClientConfig<*>.() -> Unit = {}
|
||||
): HttpClient
|
||||
|
||||
object ApiClient {
|
||||
val json = Json {
|
||||
@@ -28,19 +37,14 @@ object ApiClient {
|
||||
encodeDefaults = true
|
||||
}
|
||||
|
||||
val http = HttpClient {
|
||||
val http = createPlatformHttpClient {
|
||||
install(ContentNegotiation) {
|
||||
json(json)
|
||||
}
|
||||
|
||||
install(Logging) {
|
||||
logger = Logger.SIMPLE
|
||||
level = LogLevel.INFO
|
||||
logger = object : Logger {
|
||||
override fun log(message: String) {
|
||||
// Replace with Logcat if needed
|
||||
println(message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
install(WebSockets)
|
||||
@@ -58,7 +62,7 @@ object ApiClient {
|
||||
http
|
||||
.post("$API_HOST/api/login") {
|
||||
contentType(ContentType.Application.Json)
|
||||
setBody(request.also { Log.d("ApiClient", "Login request: $it") })
|
||||
setBody(request.also { ru.fromchat.core.Logger.d("ApiClient", "Login request: $it") })
|
||||
}
|
||||
.failOnError()
|
||||
.body<LoginResponse>()
|
||||
@@ -1,6 +1,6 @@
|
||||
package ru.fromchat.api
|
||||
|
||||
import android.util.Log
|
||||
import ru.fromchat.core.Logger
|
||||
import io.ktor.client.call.body
|
||||
import io.ktor.client.plugins.ClientRequestException
|
||||
|
||||
@@ -20,12 +20,12 @@ suspend inline fun <Response> apiRequest(
|
||||
"Unexpected error"
|
||||
}
|
||||
|
||||
Log.e("API", "API request failed: $message, exception:", e)
|
||||
Logger.e("API", "API request failed: $message", e)
|
||||
|
||||
onError(message, e)
|
||||
return Result.failure(e)
|
||||
} catch (e: Exception) {
|
||||
Log.e("API", "API request failed:", e)
|
||||
Logger.e("API", "API request failed: ${e.message}", e)
|
||||
onError("Unexpected error", e)
|
||||
return Result.failure(e)
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
package ru.fromchat.api
|
||||
|
||||
import android.util.Log
|
||||
import ru.fromchat.core.Logger
|
||||
import io.ktor.client.plugins.websocket.DefaultClientWebSocketSession
|
||||
import io.ktor.client.plugins.websocket.webSocket
|
||||
import io.ktor.client.request.url
|
||||
@@ -45,7 +45,7 @@ object WebSocketManager {
|
||||
@Volatile private var session: DefaultClientWebSocketSession? = null
|
||||
|
||||
fun connect() {
|
||||
Log.d("WebSocketManager", "Connecting to WebSocket")
|
||||
Logger.d("WebSocketManager", "Connecting to WebSocket")
|
||||
if (connecting) return
|
||||
connecting = true
|
||||
|
||||
@@ -61,27 +61,27 @@ object WebSocketManager {
|
||||
session = this
|
||||
connecting = false
|
||||
|
||||
Log.d("WebSocketManager", "WebSocket connected")
|
||||
Logger.d("WebSocketManager", "WebSocket connected")
|
||||
for (frame in incoming) {
|
||||
val text = (frame as? Frame.Text)?.readText() ?: continue
|
||||
Log.d("WebSocketManager", "Received payload: $text")
|
||||
Logger.d("WebSocketManager", "Received payload: $text")
|
||||
try {
|
||||
val msg = json.decodeFromString<WebSocketMessage>(text)
|
||||
globalHandlers.forEach { it(msg) }
|
||||
_messages.emit(msg)
|
||||
} catch (e: Throwable) {
|
||||
Log.w("WebSocketManager", "Received malformed payload:", e)
|
||||
Logger.w("WebSocketManager", "Received malformed payload: ${e.message}", e)
|
||||
// ignore malformed
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
Log.w("WebSocketManager", "An error occurred:", e)
|
||||
Logger.w("WebSocketManager", "An error occurred: ${e.message}", e)
|
||||
connecting = false
|
||||
session = null
|
||||
delay(3000)
|
||||
} finally {
|
||||
Log.w("WebSocketManager", "WebSocket disconnected")
|
||||
Logger.w("WebSocketManager", "WebSocket disconnected")
|
||||
session = null
|
||||
connecting = false
|
||||
}
|
||||
@@ -95,24 +95,24 @@ object WebSocketManager {
|
||||
try {
|
||||
session.send(Frame.Text(json.encodeToString(message)))
|
||||
} catch (e: Exception) {
|
||||
Log.e("WebSocketManager", "Failed to send message", e)
|
||||
Logger.e("WebSocketManager", "Failed to send message: ${e.message}", e)
|
||||
throw e
|
||||
}
|
||||
} else {
|
||||
Log.w("WebSocketManager", "Cannot send message: no active session")
|
||||
Logger.w("WebSocketManager", "Cannot send message: no active session")
|
||||
throw IllegalStateException("No active WebSocket session")
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
suspend fun request(message: WebSocketMessage, timeoutMs: Long = 10_000): WebSocketMessage? {
|
||||
Log.d("WebSocketManager", "WebSocket request: $message")
|
||||
Logger.d("WebSocketManager", "WebSocket request: $message")
|
||||
var handler: ((WebSocketMessage) -> Unit)? = null
|
||||
|
||||
return try {
|
||||
// Check if we have a valid session before sending
|
||||
if (session == null) {
|
||||
Log.w("WebSocketManager", "No active WebSocket session")
|
||||
Logger.w("WebSocketManager", "No active WebSocket session")
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -130,10 +130,10 @@ object WebSocketManager {
|
||||
}
|
||||
}
|
||||
} catch (_: TimeoutCancellationException) {
|
||||
Log.w("WebSocketManager", "Request timed out")
|
||||
Logger.w("WebSocketManager", "Request timed out")
|
||||
null
|
||||
} catch (e: Exception) {
|
||||
Log.e("WebSocketManager", "Request failed", e)
|
||||
Logger.e("WebSocketManager", "Request failed: ${e.message}", e)
|
||||
null
|
||||
} finally {
|
||||
handler?.let { removeGlobalMessageHandler(it) }
|
||||
@@ -0,0 +1,8 @@
|
||||
package ru.fromchat.core
|
||||
|
||||
expect object Logger {
|
||||
fun d(tag: String, message: String, throwable: Throwable? = null)
|
||||
fun i(tag: String, message: String, throwable: Throwable? = null)
|
||||
fun w(tag: String, message: String, throwable: Throwable? = null)
|
||||
fun e(tag: String, message: String, throwable: Throwable? = null)
|
||||
}
|
||||
@@ -1,15 +1,11 @@
|
||||
package ru.fromchat.ui
|
||||
|
||||
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.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
|
||||
private val DarkColorScheme = darkColorScheme(
|
||||
primary = Color(0xFFDBB9F9),
|
||||
@@ -90,18 +86,10 @@ private val LightColorScheme = lightColorScheme(
|
||||
@Composable
|
||||
fun FromChatTheme(
|
||||
darkTheme: Boolean = isSystemInDarkTheme(),
|
||||
dynamicColor: Boolean = true,
|
||||
dynamicColor: Boolean = false, // Disabled for multiplatform compatibility
|
||||
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
|
||||
}
|
||||
val colorScheme = if (darkTheme) DarkColorScheme else LightColorScheme
|
||||
|
||||
MaterialTheme(
|
||||
colorScheme = colorScheme,
|
||||
@@ -33,7 +33,7 @@ import ru.fromchat.api.ApiClient
|
||||
import ru.fromchat.api.LoginRequest
|
||||
import ru.fromchat.api.apiRequest
|
||||
import ru.fromchat.ui.RowHeader
|
||||
import ru.fromchat.utils.HashUtils
|
||||
import com.pr0gramm3r101.utils.crypto.deriveAuthSecret
|
||||
|
||||
@Composable
|
||||
fun LoginScreen(
|
||||
@@ -98,7 +98,7 @@ fun LoginScreen(
|
||||
|
||||
// Derive auth secret before sending (matches frontend implementation)
|
||||
scope.launch {
|
||||
val derived = HashUtils.deriveAuthSecret(username.trim(), password.trim())
|
||||
val derived = deriveAuthSecret(username.trim(), password.trim())
|
||||
|
||||
apiRequest(
|
||||
onError = { message, _ ->
|
||||
@@ -37,7 +37,7 @@ import ru.fromchat.api.RegisterRequest
|
||||
import ru.fromchat.api.apiRequest
|
||||
import ru.fromchat.ui.LocalNavController
|
||||
import ru.fromchat.ui.RowHeader
|
||||
import ru.fromchat.utils.HashUtils
|
||||
import com.pr0gramm3r101.utils.crypto.deriveAuthSecret
|
||||
|
||||
@Composable
|
||||
fun RegisterScreen(
|
||||
@@ -148,7 +148,7 @@ fun RegisterScreen(
|
||||
|
||||
// Derive auth secret before sending (matches frontend implementation)
|
||||
scope.launch {
|
||||
val derived = HashUtils.deriveAuthSecret(username.trim(), password)
|
||||
val derived = deriveAuthSecret(username.trim(), password)
|
||||
|
||||
apiRequest(
|
||||
onError = { message, _ ->
|
||||
@@ -7,11 +7,10 @@ import io.ktor.client.plugins.DefaultRequest
|
||||
import io.ktor.client.plugins.ResponseException
|
||||
import io.ktor.client.plugins.ServerResponseException
|
||||
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
|
||||
import io.ktor.client.plugins.logging.DEFAULT
|
||||
import io.ktor.client.plugins.logging.LogLevel
|
||||
import io.ktor.client.plugins.logging.Logger
|
||||
import io.ktor.client.plugins.logging.Logging
|
||||
import io.ktor.client.plugins.logging.LoggingConfig
|
||||
import io.ktor.client.plugins.logging.SIMPLE
|
||||
import io.ktor.client.statement.HttpResponse
|
||||
import io.ktor.client.statement.bodyAsText
|
||||
import io.ktor.serialization.Configuration
|
||||
@@ -56,10 +55,10 @@ inline fun HttpClientConfig<*>.defaultRequest(
|
||||
|
||||
/**
|
||||
* Installs [Logging] in a Ktor client config.
|
||||
* @param settings Lambda to configure the [LoggingConfig].
|
||||
* @param settings Lambda to configure the [Logging.Config].
|
||||
*/
|
||||
inline fun HttpClientConfig<*>.logging(
|
||||
crossinline settings: LoggingConfig.() -> Unit
|
||||
crossinline settings: Logging.Config.() -> Unit
|
||||
) = install(Logging) {
|
||||
settings()
|
||||
}
|
||||
@@ -68,7 +67,7 @@ inline fun HttpClientConfig<*>.logging(
|
||||
* Installs [Logging] in a Ktor client config with default settings (all logs).
|
||||
*/
|
||||
inline fun HttpClientConfig<*>.logging() = logging {
|
||||
logger = Logger.DEFAULT
|
||||
logger = Logger.SIMPLE
|
||||
level = LogLevel.ALL
|
||||
}
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
package ru.fromchat
|
||||
|
||||
import ru.fromchat.ui.App
|
||||
import androidx.compose.ui.window.ComposeUIViewController
|
||||
|
||||
fun MainViewController() = ComposeUIViewController { App() }
|
||||
@@ -0,0 +1,13 @@
|
||||
package ru.fromchat.api
|
||||
|
||||
import io.ktor.client.HttpClient
|
||||
import io.ktor.client.HttpClientConfig
|
||||
import io.ktor.client.engine.darwin.Darwin
|
||||
import io.ktor.client.engine.darwin.DarwinClientEngineConfig
|
||||
|
||||
actual fun createPlatformHttpClient(
|
||||
block: HttpClientConfig<*>.() -> Unit
|
||||
): HttpClient {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
return HttpClient(Darwin, block as HttpClientConfig<DarwinClientEngineConfig>.() -> Unit)
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package ru.fromchat.core
|
||||
|
||||
import platform.Foundation.NSLog
|
||||
|
||||
actual object Logger {
|
||||
actual fun d(tag: String, message: String, throwable: Throwable?) {
|
||||
NSLog("DEBUG: [%s] %s %s", tag, message, throwable?.message ?: "")
|
||||
}
|
||||
|
||||
actual fun i(tag: String, message: String, throwable: Throwable?) {
|
||||
NSLog("INFO: [%s] %s %s", tag, message, throwable?.message ?: "")
|
||||
}
|
||||
|
||||
actual fun w(tag: String, message: String, throwable: Throwable?) {
|
||||
NSLog("WARN: [%s] %s %s", tag, message, throwable?.message ?: "")
|
||||
}
|
||||
|
||||
actual fun e(tag: String, message: String, throwable: Throwable?) {
|
||||
NSLog("ERROR: [%s] %s %s", tag, message, throwable?.message ?: "")
|
||||
}
|
||||
}
|
||||
@@ -1,43 +1,66 @@
|
||||
[versions]
|
||||
agp = "8.13.1"
|
||||
androidx-activityCompose = "1.12.0"
|
||||
androidx-appcompat = "1.7.1"
|
||||
androidx-core-ktx = "1.17.0"
|
||||
androidx-lifecycle = "2.9.6"
|
||||
compose-multiplatform = "1.9.3"
|
||||
constraintlayout = "0.6.1-shaded"
|
||||
coreSplashscreen = "1.2.0"
|
||||
kotlin = "2.2.21"
|
||||
coreKtx = "1.17.0"
|
||||
kotlinxDatetime = "0.7.1"
|
||||
lifecycleRuntimeKtx = "2.10.0"
|
||||
activityCompose = "1.12.0"
|
||||
composeBom = "2025.11.01"
|
||||
materialIconsExtended = "1.7.8"
|
||||
navigationCompose = "2.9.6"
|
||||
ktor = "3.3.3"
|
||||
adaptiveAndroid = "1.2.0"
|
||||
kotlinStdlib = "2.2.21"
|
||||
biometric = "1.4.0-alpha04"
|
||||
gson = "2.13.2"
|
||||
kotlinxIoBytestring = "0.8.2"
|
||||
kotlinxCoroutinesCore = "1.10.2"
|
||||
kotlinxIoCore = "0.8.2"
|
||||
kotlinxSerializationJson = "1.9.0"
|
||||
compose = "1.9.5"
|
||||
material3 = "1.5.0-alpha09"
|
||||
haze = "1.7.1"
|
||||
material = "1.13.0"
|
||||
activityKtx = "1.12.0"
|
||||
navigationCompose = "2.9.1"
|
||||
datastore = "1.2.0"
|
||||
ktor = "2.3.12"
|
||||
slf4j = "1.7.36"
|
||||
kotlinxDatetime = "0.7.1"
|
||||
|
||||
[libraries]
|
||||
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
|
||||
androidx-material-icons-extended = { module = "androidx.compose.material:material-icons-extended", version.ref = "materialIconsExtended" }
|
||||
androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" }
|
||||
androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" }
|
||||
androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" }
|
||||
androidx-ui = { group = "androidx.compose.ui", name = "ui", version.ref = "compose" }
|
||||
androidx-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics", version.ref = "compose" }
|
||||
androidx-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling", version.ref = "compose" }
|
||||
androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview", version.ref = "compose" }
|
||||
androidx-material3 = { group = "androidx.compose.material3", name = "material3", version.ref = "material3" }
|
||||
androidx-navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version.ref = "navigationCompose" }
|
||||
kotlinx-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "kotlinxDatetime" }
|
||||
ktor-client-core = { group = "io.ktor", name = "ktor-client-core", version.ref = "ktor" }
|
||||
ktor-client-okhttp = { group = "io.ktor", name = "ktor-client-okhttp", version.ref = "ktor" }
|
||||
ktor-client-websockets = { group = "io.ktor", name = "ktor-client-websockets", version.ref = "ktor" }
|
||||
ktor-client-logging = { module = "io.ktor:ktor-client-logging", version.ref = "ktor" }
|
||||
ktor-serialization-json = { module = "io.ktor:ktor-serialization-kotlinx-json", version.ref = "ktor" }
|
||||
androidx-core-splashscreen = { module = "androidx.core:core-splashscreen", version.ref = "coreSplashscreen" }
|
||||
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "androidx-core-ktx" }
|
||||
androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "androidx-appcompat" }
|
||||
androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "androidx-activityCompose" }
|
||||
androidx-lifecycle-viewmodel = { group = "org.jetbrains.androidx.lifecycle", name = "lifecycle-viewmodel", version.ref = "androidx-lifecycle" }
|
||||
androidx-lifecycle-runtime-compose = { group = "org.jetbrains.androidx.lifecycle", name = "lifecycle-runtime-compose", version.ref = "androidx-lifecycle" }
|
||||
constraintlayout = { module = "tech.annexflow.compose:constraintlayout-compose-multiplatform", version.ref = "constraintlayout" }
|
||||
jetbrains-kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinxCoroutinesCore" }
|
||||
jetbrains-kotlinx-io-bytestring = { module = "org.jetbrains.kotlinx:kotlinx-io-bytestring", version.ref = "kotlinxIoBytestring" }
|
||||
androidx-adaptive-android = { group = "androidx.compose.material3.adaptive", name = "adaptive-android", version.ref = "adaptiveAndroid" }
|
||||
kotlin-stdlib = { group = "org.jetbrains.kotlin", name = "kotlin-stdlib", version.ref = "kotlinStdlib" }
|
||||
biometric = { module = "androidx.biometric:biometric", version.ref = "biometric" }
|
||||
gson = { module = "com.google.code.gson:gson", version.ref = "gson" }
|
||||
kotlinx-io-core = { module = "org.jetbrains.kotlinx:kotlinx-io-core", version.ref = "kotlinxIoCore" }
|
||||
kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinxSerializationJson" }
|
||||
material = { group = "com.google.android.material", name = "material", version.ref = "material" }
|
||||
androidx-activity-ktx = { group = "androidx.activity", name = "activity-ktx", version.ref = "activityKtx" }
|
||||
navigation-compose = { module = "org.jetbrains.androidx.navigation:navigation-compose", version.ref = "navigationCompose" }
|
||||
datastore-preferences = { group = "androidx.datastore", name = "datastore-preferences", version.ref = "datastore" }
|
||||
datastore-core = { group = "androidx.datastore", name = "datastore-core", version.ref = "datastore" }
|
||||
ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor" }
|
||||
ktor-client-content-negotiation = { module = "io.ktor:ktor-client-content-negotiation", version.ref = "ktor" }
|
||||
haze = { module = "dev.chrisbanes.haze:haze", version.ref = "haze" }
|
||||
ktor-client-serialization-kotlinx-json = { module = "io.ktor:ktor-serialization-kotlinx-json", version.ref = "ktor" }
|
||||
ktor-client-websockets = { module = "io.ktor:ktor-client-websockets", version.ref = "ktor" }
|
||||
ktor-client-logging = { module = "io.ktor:ktor-client-logging", version.ref = "ktor" }
|
||||
ktor-client-android = { module = "io.ktor:ktor-client-android", version.ref = "ktor" }
|
||||
ktor-client-okhttp = { module = "io.ktor:ktor-client-okhttp", version.ref = "ktor" }
|
||||
ktor-client-darwin = { module = "io.ktor:ktor-client-darwin", version.ref = "ktor" }
|
||||
slf4j-android = { module = "org.slf4j:slf4j-android", version.ref = "slf4j" }
|
||||
kotlinx-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "kotlinxDatetime" }
|
||||
|
||||
[plugins]
|
||||
android-application = { id = "com.android.application", version.ref = "agp" }
|
||||
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
|
||||
kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
|
||||
kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
|
||||
androidApplication = { id = "com.android.application", version.ref = "agp" }
|
||||
androidLibrary = { id = "com.android.library", version.ref = "agp" }
|
||||
composeMultiplatform = { id = "org.jetbrains.compose", version.ref = "compose-multiplatform" }
|
||||
composeCompiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
|
||||
kotlinMultiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" }
|
||||
androidKotlinMultiplatformLibrary = { id = "com.android.kotlin.multiplatform.library", version.ref = "agp" }
|
||||
jetbrains-kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
|
||||
@@ -1,24 +1,34 @@
|
||||
@file:Suppress("UnstableApiUsage")
|
||||
|
||||
rootProject.name = "FromChat"
|
||||
enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS")
|
||||
|
||||
pluginManagement {
|
||||
repositories {
|
||||
google {
|
||||
content {
|
||||
includeGroupByRegex("com\\.android.*")
|
||||
includeGroupByRegex("com\\.google.*")
|
||||
includeGroupByRegex("androidx.*")
|
||||
mavenContent {
|
||||
includeGroupAndSubgroups("androidx")
|
||||
includeGroupAndSubgroups("com.android")
|
||||
includeGroupAndSubgroups("com.google")
|
||||
}
|
||||
}
|
||||
mavenCentral()
|
||||
gradlePluginPortal()
|
||||
}
|
||||
}
|
||||
|
||||
dependencyResolutionManagement {
|
||||
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
|
||||
repositories {
|
||||
google()
|
||||
google {
|
||||
mavenContent {
|
||||
includeGroupAndSubgroups("androidx")
|
||||
includeGroupAndSubgroups("com.android")
|
||||
includeGroupAndSubgroups("com.google")
|
||||
}
|
||||
}
|
||||
mavenCentral()
|
||||
}
|
||||
}
|
||||
|
||||
rootProject.name = "FromChat"
|
||||
include(":app")
|
||||
include(":composeApp", ":utils")
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
/build
|
||||
@@ -0,0 +1,66 @@
|
||||
plugins {
|
||||
alias(libs.plugins.kotlinMultiplatform)
|
||||
alias(libs.plugins.androidKotlinMultiplatformLibrary)
|
||||
alias(libs.plugins.composeMultiplatform)
|
||||
alias(libs.plugins.composeCompiler)
|
||||
}
|
||||
|
||||
kotlin {
|
||||
compilerOptions {
|
||||
freeCompilerArgs.addAll("-Xexpect-actual-classes")
|
||||
}
|
||||
|
||||
androidLibrary {
|
||||
namespace = "com.pr0gramm3r101.utils"
|
||||
compileSdk = 36
|
||||
minSdk = 24
|
||||
}
|
||||
|
||||
listOf(
|
||||
iosX64(),
|
||||
iosArm64(),
|
||||
iosSimulatorArm64()
|
||||
).forEach {
|
||||
it.binaries.framework {
|
||||
baseName = "utils"
|
||||
}
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
commonMain.dependencies {
|
||||
implementation(libs.kotlin.stdlib)
|
||||
implementation(compose.runtime)
|
||||
implementation(compose.foundation)
|
||||
implementation(compose.material3)
|
||||
implementation(compose.ui)
|
||||
implementation(compose.components.resources)
|
||||
implementation(libs.constraintlayout)
|
||||
implementation(compose.materialIconsExtended)
|
||||
implementation(libs.jetbrains.kotlinx.coroutines.core)
|
||||
}
|
||||
|
||||
androidMain.dependencies {
|
||||
implementation(libs.androidx.adaptive.android)
|
||||
implementation(libs.androidx.activity.ktx)
|
||||
implementation(libs.androidx.core.ktx)
|
||||
implementation(libs.androidx.appcompat)
|
||||
implementation(libs.material)
|
||||
implementation(libs.biometric)
|
||||
implementation(libs.gson)
|
||||
|
||||
implementation(libs.androidx.adaptive.android)
|
||||
implementation(libs.datastore.preferences)
|
||||
implementation(libs.datastore.core)
|
||||
}
|
||||
|
||||
iosMain.dependencies {
|
||||
// nothing
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
compose.resources {
|
||||
publicResClass = true
|
||||
packageOfResClass = "com.pr0gramm3r101.utils"
|
||||
generateResClass = auto
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest/>
|
||||
@@ -0,0 +1,42 @@
|
||||
@file:Suppress("NOTHING_TO_INLINE")
|
||||
|
||||
package com.pr0gramm3r101.components
|
||||
|
||||
import android.widget.ImageView
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.ColorFilter
|
||||
import androidx.compose.ui.graphics.DefaultAlpha
|
||||
import androidx.compose.ui.graphics.asAndroidColorFilter
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.viewinterop.AndroidView
|
||||
import com.pr0gramm3r101.utils.asAndroidScaleType
|
||||
|
||||
@Composable
|
||||
inline fun Mipmap(
|
||||
@DrawableRes id: Int,
|
||||
contentDescription: String?,
|
||||
modifier: Modifier = Modifier,
|
||||
contentScale: ContentScale = ContentScale.Fit,
|
||||
alpha: Float = DefaultAlpha,
|
||||
colorFilter: ColorFilter? = null
|
||||
) {
|
||||
val update: (ImageView) -> Unit = {
|
||||
it.setImageResource(id)
|
||||
it.contentDescription = contentDescription
|
||||
it.alpha = alpha
|
||||
it.colorFilter = colorFilter?.asAndroidColorFilter()
|
||||
it.scaleType = contentScale.asAndroidScaleType()
|
||||
}
|
||||
|
||||
AndroidView(
|
||||
modifier = modifier,
|
||||
factory = {
|
||||
ImageView(it).apply {
|
||||
update(this)
|
||||
}
|
||||
},
|
||||
update = update
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
@file:Suppress("NOTHING_TO_INLINE")
|
||||
|
||||
package com.pr0gramm3r101.utils
|
||||
|
||||
import androidx.compose.material3.adaptive.currentWindowSize
|
||||
import androidx.compose.runtime.Composable
|
||||
|
||||
@Composable
|
||||
actual inline fun currentWindowSize() = currentWindowSize()
|
||||
@@ -0,0 +1,99 @@
|
||||
package com.pr0gramm3r101.utils;
|
||||
|
||||
import android.content.Intent;
|
||||
|
||||
import androidx.activity.result.ActivityResult;
|
||||
import androidx.activity.result.ActivityResultCaller;
|
||||
import androidx.activity.result.ActivityResultLauncher;
|
||||
import androidx.activity.result.contract.ActivityResultContract;
|
||||
import androidx.activity.result.contract.ActivityResultContracts;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.app.ActivityOptionsCompat;
|
||||
|
||||
public final class BetterActivityResult<I, R> {
|
||||
/**
|
||||
* Register activity result using a {@link ActivityResultContract} and an in-place activity result callback like
|
||||
* the default approach. You can still customise callback using {@link #launch(Object, OnActivityResult)}.
|
||||
*/
|
||||
@NonNull
|
||||
public static <I, R> BetterActivityResult<I, R> registerForActivityResult(
|
||||
@NonNull ActivityResultCaller caller,
|
||||
@NonNull ActivityResultContract<I, R> contract,
|
||||
@Nullable OnActivityResult<R> onActivityResult) {
|
||||
return new BetterActivityResult<>(caller, contract, onActivityResult);
|
||||
}
|
||||
|
||||
/**
|
||||
* Same as {@link #registerForActivityResult(ActivityResultCaller, ActivityResultContract, OnActivityResult)} except
|
||||
* the last argument is set to {@code null}.
|
||||
*/
|
||||
@NonNull
|
||||
public static <I, R> BetterActivityResult<I, R> registerForActivityResult(
|
||||
@NonNull ActivityResultCaller caller,
|
||||
@NonNull ActivityResultContract<I, R> contract) {
|
||||
return registerForActivityResult(caller, contract, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Specialised method for launching new activities.
|
||||
*/
|
||||
@NonNull
|
||||
public static BetterActivityResult<Intent, ActivityResult> registerActivityForResult(
|
||||
@NonNull ActivityResultCaller caller) {
|
||||
return registerForActivityResult(caller, new ActivityResultContracts.StartActivityForResult());
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback interface
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface OnActivityResult<O> {
|
||||
/**
|
||||
* Called after receiving a result from the target activity
|
||||
*/
|
||||
void onActivityResult(O result);
|
||||
}
|
||||
|
||||
private final ActivityResultLauncher<I> launcher;
|
||||
@Nullable
|
||||
private OnActivityResult<R> onActivityResult;
|
||||
|
||||
private BetterActivityResult(@NonNull ActivityResultCaller caller,
|
||||
@NonNull ActivityResultContract<I, R> contract,
|
||||
@Nullable OnActivityResult<R> onActivityResult) {
|
||||
this.onActivityResult = onActivityResult;
|
||||
this.launcher = caller.registerForActivityResult(contract, this::callOnActivityResult);
|
||||
}
|
||||
|
||||
/**
|
||||
* Launch activity, same as {@link ActivityResultLauncher#launch(Object)} except that it allows a callback
|
||||
* executed after receiving a result from the target activity.
|
||||
*/
|
||||
public void launch(I input, @Nullable OnActivityResult<R> onActivityResult) {
|
||||
this.onActivityResult = onActivityResult;
|
||||
launcher.launch(input);
|
||||
}
|
||||
|
||||
public void launch(I input, ActivityOptionsCompat options, @Nullable OnActivityResult<R> onActivityResult) {
|
||||
this.onActivityResult = onActivityResult;
|
||||
launcher.launch(input, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Same as {@link #launch(Object, OnActivityResult)} with last parameter set to {@code null}.
|
||||
* @noinspection unused
|
||||
*/
|
||||
public void launch(I input) {
|
||||
launch(input, (OnActivityResult<R>) null);
|
||||
}
|
||||
|
||||
/** @noinspection unused*/
|
||||
public void launch(I input, ActivityOptionsCompat options) {
|
||||
launch(input, options, null);
|
||||
}
|
||||
|
||||
private void callOnActivityResult(R result) {
|
||||
if (onActivityResult != null) onActivityResult.onActivityResult(result);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package com.pr0gramm3r101.utils
|
||||
|
||||
import androidx.annotation.IntDef
|
||||
import org.jetbrains.annotations.Contract
|
||||
|
||||
|
||||
object JobIdManager {
|
||||
const val JOB_TYPE_CHANNEL_PROGRAMS: Int = 1
|
||||
const val JOB_TYPE_CHANNEL_METADATA: Int = 2
|
||||
const val JOB_TYPE_CHANNEL_DELETION: Int = 3
|
||||
const val JOB_TYPE_CHANNEL_LOGGER: Int = 4
|
||||
|
||||
const val JOB_TYPE_USER_PREFS: Int = 11
|
||||
const val JOB_TYPE_USER_BEHAVIOR: Int = 21
|
||||
|
||||
//16-1 for short. Adjust per your needs
|
||||
private const val JOB_TYPE_SHIFTS = 15
|
||||
|
||||
@Contract(pure = true)
|
||||
fun getJobId(@JobType jobType: Int, objectId: Int): Int {
|
||||
if (objectId > 0 && objectId < (1 shl JOB_TYPE_SHIFTS)) {
|
||||
return (jobType shl JOB_TYPE_SHIFTS) + objectId
|
||||
} else {
|
||||
throw IllegalArgumentException(String.format(
|
||||
"objectId %s must be between %s and %s",
|
||||
objectId, 0, (1 shl JOB_TYPE_SHIFTS)
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
@IntDef(
|
||||
value = [
|
||||
JOB_TYPE_CHANNEL_PROGRAMS,
|
||||
JOB_TYPE_CHANNEL_METADATA,
|
||||
JOB_TYPE_CHANNEL_DELETION,
|
||||
JOB_TYPE_CHANNEL_LOGGER,
|
||||
JOB_TYPE_USER_PREFS,
|
||||
JOB_TYPE_USER_BEHAVIOR
|
||||
]
|
||||
)
|
||||
@Retention(AnnotationRetention.SOURCE)
|
||||
private annotation class JobType
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.pr0gramm3r101.utils
|
||||
|
||||
import java.util.concurrent.Executor
|
||||
|
||||
@Suppress("MemberVisibilityCanBePrivate")
|
||||
class NoParallelExecutor: Executor {
|
||||
val isRunning get() = thread != null
|
||||
var thread: Thread? = null
|
||||
|
||||
override fun execute(command: Runnable) {
|
||||
if (!isRunning) {
|
||||
thread = async {
|
||||
command.run()
|
||||
thread = null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,489 @@
|
||||
@file:Suppress("MemberVisibilityCanBePrivate")
|
||||
|
||||
package com.pr0gramm3r101.utils
|
||||
|
||||
import android.content.ClipData
|
||||
import android.content.ComponentName
|
||||
import android.content.Intent
|
||||
import android.graphics.Rect
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.Parcelable
|
||||
import android.os.PersistableBundle
|
||||
import java.io.InputStream
|
||||
import java.io.ObjectInputStream
|
||||
import java.io.ObjectOutputStream
|
||||
import java.io.OutputStream
|
||||
import java.io.Serial
|
||||
import java.io.Serializable
|
||||
|
||||
fun Intent.serialize(outputStream: OutputStream) = SerializableIntent.serialize(this, outputStream)
|
||||
fun Intent.toSerializableIntent() = SerializableIntent(this)
|
||||
|
||||
class SerializableIntent(): Intent(), Serializable {
|
||||
companion object {
|
||||
@Serial
|
||||
const val serialVersionUID = 1241895592857245245L
|
||||
|
||||
fun deserialize(inputStream: InputStream): Intent {
|
||||
ObjectInputStream(inputStream).use {
|
||||
val obj = it.readObject() as SerializableIntent
|
||||
return obj.toIntent()
|
||||
}
|
||||
}
|
||||
|
||||
fun serialize(intent: Intent, outputStream: OutputStream) {
|
||||
SerializableIntent(intent).serialize(outputStream)
|
||||
}
|
||||
}
|
||||
|
||||
constructor(intent: Intent): this() {
|
||||
_data = intent.data
|
||||
_package = intent.`package`
|
||||
_flags = intent.flags
|
||||
_type = intent.type
|
||||
_action = intent.action
|
||||
_categories.addAll(intent.categories)
|
||||
_component = intent.component
|
||||
_extras = intent.extras
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
_identifier = intent.identifier
|
||||
}
|
||||
_sourceBounds = intent.sourceBounds
|
||||
}
|
||||
|
||||
@Transient
|
||||
var _data: Uri? = null
|
||||
private var __data: String? = null
|
||||
var _package: String? = null
|
||||
var _flags: Int = 0
|
||||
var _type: String? = null
|
||||
val _scheme get() = _data?.scheme
|
||||
var _action: String? = null
|
||||
val _categories = mutableSetOf<String>()
|
||||
@Transient
|
||||
var _component: ComponentName? = null
|
||||
private var __component_packageName: String? = null
|
||||
private var __component_className: String? = null
|
||||
@Transient
|
||||
var _extras: Bundle? = null
|
||||
private var __extras_persistableBundle: PersistableBundle? = null
|
||||
var _identifier: String? = null
|
||||
@Transient
|
||||
var _sourceBounds: Rect? = null
|
||||
private var __sourceBounds_top: Int? = null
|
||||
private var __sourceBounds_bottom: Int? = null
|
||||
private var __sourceBounds_left: Int? = null
|
||||
private var __sourceBounds_right: Int? = null
|
||||
|
||||
fun prepareSerialize() {
|
||||
__data = _data?.toString()
|
||||
__extras_persistableBundle = _extras?.toPersistableBundle()
|
||||
__component_packageName = _component?.packageName
|
||||
__component_className = _component?.className
|
||||
__sourceBounds_top = _sourceBounds?.top
|
||||
__sourceBounds_bottom = _sourceBounds?.bottom
|
||||
__sourceBounds_left = _sourceBounds?.left
|
||||
__sourceBounds_right = _sourceBounds?.right
|
||||
}
|
||||
|
||||
fun serialize(outputStream: OutputStream) {
|
||||
prepareSerialize()
|
||||
ObjectOutputStream(outputStream).use {
|
||||
it.writeObject(this@SerializableIntent)
|
||||
}
|
||||
}
|
||||
|
||||
fun toIntent(): Intent {
|
||||
val intent = Intent()
|
||||
intent.setDataAndType(_data, _type)
|
||||
intent.`package` = _package
|
||||
intent.flags = _flags
|
||||
intent.action = _action
|
||||
_categories.forEach {
|
||||
intent.addCategory(it)
|
||||
}
|
||||
intent.component = _component
|
||||
_extras?.let { intent.putExtras(it) }
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
intent.identifier = _identifier
|
||||
}
|
||||
_sourceBounds = Rect(
|
||||
__sourceBounds_left?: 0,
|
||||
__sourceBounds_top?: 0,
|
||||
__sourceBounds_right?: 0,
|
||||
__sourceBounds_bottom?: 0
|
||||
)
|
||||
return intent
|
||||
}
|
||||
|
||||
// OVERRIDES
|
||||
override fun getData() = _data
|
||||
override fun setData(uri: Uri?): Intent {
|
||||
this._data = uri
|
||||
return this
|
||||
}
|
||||
|
||||
override fun getPackage() = _package
|
||||
override fun setPackage(pkg: String?): Intent {
|
||||
this._package = pkg
|
||||
return this
|
||||
}
|
||||
override fun getFlags() = _flags
|
||||
override fun setFlags(flags: Int): Intent {
|
||||
this._flags = flags
|
||||
return this
|
||||
}
|
||||
|
||||
override fun addFlags(flags: Int): Intent {
|
||||
this._flags = this._flags or flags
|
||||
return this
|
||||
}
|
||||
|
||||
override fun getType() = _type
|
||||
override fun setType(type: String?): Intent {
|
||||
this._type = type
|
||||
return this
|
||||
}
|
||||
|
||||
override fun getScheme() = _scheme
|
||||
|
||||
override fun getAction() = _action
|
||||
override fun setAction(action: String?): Intent {
|
||||
this._action = action
|
||||
return this
|
||||
}
|
||||
|
||||
override fun addCategory(category: String): Intent {
|
||||
_categories.add(category)
|
||||
return this
|
||||
}
|
||||
override fun getCategories() = _categories
|
||||
override fun hasCategory(category: String) = _categories.contains(category)
|
||||
override fun removeCategory(category: String) {
|
||||
_categories.remove(category)
|
||||
}
|
||||
|
||||
override fun getClipData() = throw UnsupportedOperationException("Not supported in SerializableIntent")
|
||||
override fun setClipData(clipData: ClipData?) = throw UnsupportedOperationException("Not supported in SerializableIntent")
|
||||
|
||||
override fun getComponent(): ComponentName? = _component
|
||||
override fun setComponent(component: ComponentName?): Intent {
|
||||
this._component = component
|
||||
return this
|
||||
}
|
||||
|
||||
override fun getDataString() = _data?.toString()
|
||||
|
||||
override fun getExtras() = _extras
|
||||
override fun putExtras(extras: Bundle): Intent {
|
||||
if (this._extras == null) {
|
||||
this._extras = Bundle()
|
||||
}
|
||||
this._extras!!.putAll(extras)
|
||||
return this
|
||||
}
|
||||
override fun putExtras(src: Intent): Intent {
|
||||
if (src.extras != null) {
|
||||
putExtras(src.extras!!)
|
||||
}
|
||||
return this
|
||||
}
|
||||
override fun putExtra(name: String, value: Array<out CharSequence>?): Intent {
|
||||
if (this._extras == null) {
|
||||
this._extras = Bundle()
|
||||
}
|
||||
this._extras!!.putCharSequenceArray(name, value)
|
||||
return this
|
||||
}
|
||||
override fun putExtra(name: String, value: Array<out Parcelable>?): Intent {
|
||||
return this // Stub!
|
||||
}
|
||||
override fun putExtra(name: String, value: Array<out String>?): Intent {
|
||||
if (this._extras == null) {
|
||||
this._extras = Bundle()
|
||||
}
|
||||
this._extras!!.putStringArray(name, value)
|
||||
return this
|
||||
}
|
||||
override fun putExtra(name: String, value: Boolean): Intent {
|
||||
if (this._extras == null) {
|
||||
this._extras = Bundle()
|
||||
}
|
||||
this._extras!!.putBoolean(name, value)
|
||||
return this
|
||||
}
|
||||
override fun putExtra(name: String, value: BooleanArray?): Intent {
|
||||
if (this._extras == null) {
|
||||
this._extras = Bundle()
|
||||
}
|
||||
this._extras!!.putBooleanArray(name, value)
|
||||
return this
|
||||
}
|
||||
override fun putExtra(name: String, value: Bundle?): Intent {
|
||||
return this // Stub!
|
||||
}
|
||||
override fun putExtra(name: String, value: Byte): Intent {
|
||||
if (this._extras == null) {
|
||||
this._extras = Bundle()
|
||||
}
|
||||
this._extras!!.putByte(name, value)
|
||||
return this
|
||||
}
|
||||
override fun putExtra(name: String, value: ByteArray?): Intent {
|
||||
if (this._extras == null) {
|
||||
this._extras = Bundle()
|
||||
}
|
||||
this._extras!!.putByteArray(name, value)
|
||||
return this
|
||||
}
|
||||
override fun putExtra(name: String?, value: Char): Intent {
|
||||
if (this._extras == null) {
|
||||
this._extras = Bundle()
|
||||
}
|
||||
this._extras!!.putChar(name, value)
|
||||
return this
|
||||
}
|
||||
override fun putExtra(name: String, value: CharArray?): Intent {
|
||||
if (this._extras == null) {
|
||||
this._extras = Bundle()
|
||||
}
|
||||
this._extras!!.putCharArray(name, value)
|
||||
return this
|
||||
}
|
||||
override fun putExtra(name: String, value: CharSequence?): Intent {
|
||||
if (this._extras == null) {
|
||||
this._extras = Bundle()
|
||||
}
|
||||
this._extras!!.putCharSequence(name, value)
|
||||
return this
|
||||
}
|
||||
override fun putExtra(name: String, value: Double): Intent {
|
||||
if (this._extras == null) {
|
||||
this._extras = Bundle()
|
||||
}
|
||||
this._extras!!.putDouble(name, value)
|
||||
return this
|
||||
}
|
||||
override fun putExtra(name: String, value: DoubleArray?): Intent {
|
||||
if (this._extras == null) {
|
||||
this._extras = Bundle()
|
||||
}
|
||||
this._extras!!.putDoubleArray(name, value)
|
||||
return this
|
||||
}
|
||||
override fun putExtra(name: String?, value: Float): Intent {
|
||||
if (this._extras == null) {
|
||||
this._extras = Bundle()
|
||||
}
|
||||
this._extras!!.putFloat(name, value)
|
||||
return this
|
||||
}
|
||||
override fun putExtra(name: String, value: FloatArray?): Intent {
|
||||
if (this._extras == null) {
|
||||
this._extras = Bundle()
|
||||
}
|
||||
this._extras!!.putFloatArray(name, value)
|
||||
return this
|
||||
}
|
||||
override fun putExtra(name: String, value: Int): Intent {
|
||||
if (this._extras == null) {
|
||||
this._extras = Bundle()
|
||||
}
|
||||
this._extras!!.putInt(name, value)
|
||||
return this
|
||||
}
|
||||
override fun putExtra(name: String, value: IntArray?): Intent {
|
||||
if (this._extras == null) {
|
||||
this._extras = Bundle()
|
||||
}
|
||||
this._extras!!.putIntArray(name, value)
|
||||
return this
|
||||
}
|
||||
override fun putExtra(name: String, value: Long): Intent {
|
||||
if (this._extras == null) {
|
||||
this._extras = Bundle()
|
||||
}
|
||||
this._extras!!.putLong(name, value)
|
||||
return this
|
||||
}
|
||||
override fun putExtra(name: String, value: LongArray?): Intent {
|
||||
if (this._extras == null) {
|
||||
this._extras = Bundle()
|
||||
}
|
||||
this._extras!!.putLongArray(name, value)
|
||||
return this
|
||||
}
|
||||
override fun putExtra(name: String, value: Parcelable?): Intent {
|
||||
return this // Stub!
|
||||
}
|
||||
override fun putExtra(name: String, value: Short): Intent {
|
||||
if (this._extras == null) {
|
||||
this._extras = Bundle()
|
||||
}
|
||||
this._extras!!.putShort(name, value)
|
||||
return this
|
||||
}
|
||||
override fun putExtra(name: String, value: ShortArray?): Intent {
|
||||
if (this._extras == null) {
|
||||
this._extras = Bundle()
|
||||
}
|
||||
this._extras!!.putShortArray(name, value)
|
||||
return this
|
||||
}
|
||||
override fun putExtra(name: String, value: String?): Intent {
|
||||
if (this._extras == null) {
|
||||
this._extras = Bundle()
|
||||
}
|
||||
this._extras!!.putString(name, value)
|
||||
return this
|
||||
}
|
||||
override fun putExtra(name: String, value: Serializable?): Intent {
|
||||
return this // Stub!
|
||||
}
|
||||
override fun putStringArrayListExtra(name: String, value: ArrayList<String>?): Intent {
|
||||
if (this._extras == null) {
|
||||
this._extras = Bundle()
|
||||
}
|
||||
this._extras!!.putStringArrayList(name, value)
|
||||
return this
|
||||
}
|
||||
override fun putIntegerArrayListExtra(name: String, value: ArrayList<Int>?): Intent {
|
||||
if (this._extras == null) {
|
||||
this._extras = Bundle()
|
||||
}
|
||||
this._extras!!.putIntegerArrayList(name, value)
|
||||
return this
|
||||
}
|
||||
override fun putParcelableArrayListExtra(name: String?, value: java.util.ArrayList<out Parcelable>?): Intent {
|
||||
return this // Stub!
|
||||
}
|
||||
override fun putCharSequenceArrayListExtra(name: String?, value: java.util.ArrayList<CharSequence>?): Intent {
|
||||
if (this._extras == null) {
|
||||
this._extras = Bundle()
|
||||
}
|
||||
this._extras!!.putCharSequenceArrayList(name, value)
|
||||
return this
|
||||
}
|
||||
override fun getBooleanExtra(name: String, defaultValue: Boolean): Boolean {
|
||||
return _extras?.getBoolean(name, defaultValue) ?: defaultValue
|
||||
}
|
||||
override fun getBooleanArrayExtra(name: String): BooleanArray? {
|
||||
return _extras?.getBooleanArray(name)
|
||||
}
|
||||
override fun getByteExtra(name: String, defaultValue: Byte): Byte {
|
||||
return _extras?.getByte(name, defaultValue) ?: defaultValue
|
||||
}
|
||||
override fun getByteArrayExtra(name: String?): ByteArray? {
|
||||
return _extras?.getByteArray(name)
|
||||
}
|
||||
override fun getCharExtra(name: String, defaultValue: Char): Char {
|
||||
return _extras?.getChar(name, defaultValue) ?: defaultValue
|
||||
}
|
||||
override fun getCharArrayExtra(name: String?): CharArray? {
|
||||
return _extras?.getCharArray(name)
|
||||
}
|
||||
override fun getDoubleExtra(name: String, defaultValue: Double): Double {
|
||||
return _extras?.getDouble(name, defaultValue) ?: defaultValue
|
||||
}
|
||||
override fun getDoubleArrayExtra(name: String?): DoubleArray? {
|
||||
return _extras?.getDoubleArray(name)
|
||||
}
|
||||
override fun getFloatExtra(name: String, defaultValue: Float): Float {
|
||||
return _extras?.getFloat(name, defaultValue) ?: defaultValue
|
||||
}
|
||||
override fun getFloatArrayExtra(name: String?): FloatArray? {
|
||||
return _extras?.getFloatArray(name)
|
||||
}
|
||||
override fun getIntExtra(name: String, defaultValue: Int): Int {
|
||||
return _extras?.getInt(name, defaultValue) ?: defaultValue
|
||||
}
|
||||
override fun getIntArrayExtra(name: String?): IntArray? {
|
||||
return _extras?.getIntArray(name)
|
||||
}
|
||||
override fun getLongExtra(name: String, defaultValue: Long): Long {
|
||||
return _extras?.getLong(name, defaultValue) ?: defaultValue
|
||||
}
|
||||
override fun getLongArrayExtra(name: String): LongArray? {
|
||||
return _extras?.getLongArray(name)
|
||||
}
|
||||
@Deprecated("Deprecated in Java")
|
||||
override fun getSerializableExtra(name: String): Serializable? {
|
||||
return null // Stub!
|
||||
}
|
||||
override fun getStringArrayListExtra(name: String): ArrayList<String>? {
|
||||
return _extras?.getStringArrayList(name)
|
||||
}
|
||||
override fun getIntegerArrayListExtra(name: String): ArrayList<Int>? {
|
||||
return _extras?.getIntegerArrayList(name)
|
||||
}
|
||||
override fun getCharSequenceArrayListExtra(name: String): ArrayList<CharSequence>? {
|
||||
return _extras?.getCharSequenceArrayList(name)
|
||||
}
|
||||
override fun <T : Serializable?> getSerializableExtra(name: String?, clazz: Class<T>): T? {
|
||||
return null // Stub!
|
||||
}
|
||||
override fun getBundleExtra(name: String): Bundle? {
|
||||
return null // Stub!
|
||||
}
|
||||
override fun getStringExtra(name: String): String? {
|
||||
return _extras?.getString(name)
|
||||
}
|
||||
override fun getShortExtra(name: String, defaultValue: Short): Short {
|
||||
return _extras?.getShort(name, defaultValue) ?: defaultValue
|
||||
}
|
||||
override fun getShortArrayExtra(name: String): ShortArray? {
|
||||
return _extras?.getShortArray(name)
|
||||
}
|
||||
override fun getStringArrayExtra(name: String): Array<String>? {
|
||||
return _extras?.getStringArray(name)
|
||||
}
|
||||
override fun getCharSequenceExtra(name: String): CharSequence? {
|
||||
return _extras?.getCharSequence(name)
|
||||
}
|
||||
override fun <T : Any?> getParcelableExtra(name: String?, clazz: Class<T>): T? {
|
||||
return null // Stub!
|
||||
}
|
||||
@Deprecated("Deprecated in Java")
|
||||
override fun <T : Parcelable?> getParcelableExtra(name: String?): T? {
|
||||
return null // Stub!
|
||||
}
|
||||
override fun getCharSequenceArrayExtra(name: String): Array<CharSequence>? {
|
||||
return _extras?.getCharSequenceArray(name)
|
||||
}
|
||||
override fun <T : Any?> getParcelableArrayExtra(name: String?, clazz: Class<T>): Array<T>? {
|
||||
return null // Stub!
|
||||
}
|
||||
@Deprecated("Deprecated in Java")
|
||||
override fun getParcelableArrayExtra(name: String): Array<Parcelable>? {
|
||||
return null // Stub!
|
||||
}
|
||||
override fun <T : Any?> getParcelableArrayListExtra(name: String?, clazz: Class<out T>): java.util.ArrayList<T>? {
|
||||
return null // Stub!
|
||||
}
|
||||
@Deprecated("Deprecated in Java")
|
||||
override fun <T : Parcelable?> getParcelableArrayListExtra(name: String?): java.util.ArrayList<T>? {
|
||||
return null // Stub!
|
||||
}
|
||||
|
||||
override fun getIdentifier() = _identifier
|
||||
override fun setIdentifier(identifier: String?): Intent {
|
||||
this._identifier = identifier
|
||||
return this
|
||||
}
|
||||
|
||||
override fun getSelector(): Intent? {
|
||||
return null
|
||||
}
|
||||
override fun setSelector(selector: Intent?) {
|
||||
// Stub!
|
||||
}
|
||||
|
||||
override fun getSourceBounds() = _sourceBounds
|
||||
override fun setSourceBounds(sourceBounds: Rect?) {
|
||||
this._sourceBounds = sourceBounds
|
||||
}
|
||||
|
||||
override fun isMismatchingFilter() = false
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
@file:Suppress("DEPRECATION", "OVERRIDE_DEPRECATION")
|
||||
|
||||
package com.pr0gramm3r101.utils
|
||||
|
||||
import android.os.AsyncTask
|
||||
|
||||
abstract class SimpleAsyncTask: AsyncTask<Unit, Unit, Unit>() {
|
||||
final override fun doInBackground(vararg params: Unit) = run()
|
||||
abstract fun run()
|
||||
abstract fun postRun()
|
||||
final override fun onProgressUpdate(vararg values: Unit) {}
|
||||
final override fun onPostExecute(result: Unit?) = postRun()
|
||||
}
|
||||
@@ -0,0 +1,832 @@
|
||||
@file:Suppress("NOTHING_TO_INLINE", "unused")
|
||||
|
||||
package com.pr0gramm3r101.utils
|
||||
|
||||
import android.Manifest
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
import android.app.KeyguardManager
|
||||
import android.app.Notification
|
||||
import android.content.ComponentName
|
||||
import android.content.ContentResolver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.Intent.ACTION_MAIN
|
||||
import android.content.Intent.CATEGORY_LAUNCHER
|
||||
import android.content.Intent.FLAG_ACTIVITY_NEW_TASK
|
||||
import android.content.pm.ActivityInfo
|
||||
import android.content.pm.PackageManager
|
||||
import android.content.res.Resources
|
||||
import android.hardware.Sensor
|
||||
import android.hardware.SensorEvent
|
||||
import android.hardware.SensorEventListener
|
||||
import android.hardware.SensorManager
|
||||
import android.hardware.usb.UsbEndpoint
|
||||
import android.hardware.usb.UsbInterface
|
||||
import android.net.Uri
|
||||
import android.os.BaseBundle
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.Parcelable
|
||||
import android.os.PersistableBundle
|
||||
import android.provider.OpenableColumns
|
||||
import android.service.quicksettings.Tile
|
||||
import android.util.Base64
|
||||
import android.util.Log
|
||||
import android.util.TypedValue
|
||||
import android.view.View
|
||||
import android.view.ViewTreeObserver.OnPreDrawListener
|
||||
import android.widget.ImageView.ScaleType
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.OnBackPressedCallback
|
||||
import androidx.activity.result.ActivityResult
|
||||
import androidx.activity.result.ActivityResultCaller
|
||||
import androidx.annotation.AttrRes
|
||||
import androidx.annotation.ChecksSdkIntAtLeast
|
||||
import androidx.annotation.RequiresPermission
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.biometric.BiometricManager
|
||||
import androidx.biometric.BiometricPrompt
|
||||
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.calculateEndPadding
|
||||
import androidx.compose.foundation.layout.calculateStartPadding
|
||||
import androidx.compose.foundation.layout.isImeVisible
|
||||
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.composed
|
||||
import androidx.compose.ui.focus.onFocusEvent
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.ComposeView
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalFocusManager
|
||||
import androidx.compose.ui.platform.LocalLayoutDirection
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.net.toUri
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import androidx.window.core.layout.WindowSizeClass
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import java.io.Serializable
|
||||
import java.lang.ref.WeakReference
|
||||
import java.security.SecureRandom
|
||||
import java.util.regex.Pattern
|
||||
import javax.crypto.Cipher
|
||||
import javax.crypto.SecretKeyFactory
|
||||
import javax.crypto.spec.IvParameterSpec
|
||||
import javax.crypto.spec.PBEKeySpec
|
||||
import kotlin.collections.iterator
|
||||
import kotlin.random.Random
|
||||
import kotlin.random.nextInt
|
||||
import kotlin.reflect.KClass
|
||||
import kotlin.reflect.KProperty
|
||||
|
||||
@OptIn(ExperimentalLayoutApi::class)
|
||||
actual fun Modifier.clearFocusOnKeyboardDismiss(): Modifier = composed {
|
||||
var isFocused by remember { mutableStateOf(false) }
|
||||
var keyboardAppearedSinceLastFocused by remember { mutableStateOf(false) }
|
||||
if (isFocused) {
|
||||
val imeIsVisible = WindowInsets.isImeVisible
|
||||
val focusManager = LocalFocusManager.current
|
||||
LaunchedEffect(imeIsVisible) {
|
||||
if (imeIsVisible) {
|
||||
@Suppress("AssignedValueIsNeverRead")
|
||||
keyboardAppearedSinceLastFocused = true
|
||||
} else if (keyboardAppearedSinceLastFocused) {
|
||||
focusManager.clearFocus()
|
||||
}
|
||||
}
|
||||
}
|
||||
onFocusEvent {
|
||||
if (isFocused != it.isFocused) {
|
||||
isFocused = it.isFocused
|
||||
if (isFocused) {
|
||||
keyboardAppearedSinceLastFocused = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("ComposableNaming")
|
||||
@Composable
|
||||
actual fun ToggleNavScrimEffect(enabled: Boolean) {
|
||||
val context = (LocalContext() as Activity)
|
||||
LaunchedEffect(enabled) {
|
||||
runCatching {
|
||||
val window = context.window
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
window.isNavigationBarContrastEnforced = enabled
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val screenWidth: Int inline get() = Resources.getSystem().displayMetrics.widthPixels
|
||||
@Suppress("unused")
|
||||
val screenHeight: Int inline get() = Resources.getSystem().displayMetrics.heightPixels
|
||||
|
||||
fun appName(context: Context, packageName: String): String? {
|
||||
try {
|
||||
val packageManager = context.packageManager
|
||||
val applicationInfo = packageManager.getApplicationInfo(
|
||||
packageName, PackageManager.GET_META_DATA
|
||||
)
|
||||
val appName = packageManager.getApplicationLabel(applicationInfo) as String
|
||||
return appName
|
||||
} catch (_: PackageManager.NameNotFoundException) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
fun Context.homeScreen() {
|
||||
val intentHome = Intent(ACTION_MAIN)
|
||||
intentHome.addCategory(Intent.CATEGORY_HOME)
|
||||
intentHome.flags = FLAG_ACTIVITY_NEW_TASK
|
||||
startActivity(intentHome)
|
||||
}
|
||||
|
||||
|
||||
inline fun async(noinline exec: () -> Unit) = Thread(exec).apply { start() }
|
||||
|
||||
inline fun Tile.configure(apply: Tile.() -> Unit) {
|
||||
apply.invoke(this)
|
||||
updateTile()
|
||||
}
|
||||
|
||||
inline fun View.addOneTimeOnPreDrawListener(crossinline listener: () -> Boolean) {
|
||||
viewTreeObserver.addOnPreDrawListener(object: OnPreDrawListener {
|
||||
override fun onPreDraw(): Boolean {
|
||||
viewTreeObserver.removeOnPreDrawListener(this)
|
||||
return listener()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fun Context.launchFiles(): Boolean {
|
||||
val primaryStorageUri = "content://com.android.externalstorage.documents/root/primary".toUri()
|
||||
|
||||
fun launchIntentWithComponent(action: String, componentName: ComponentName? = null): Boolean {
|
||||
val intent = Intent(action, primaryStorageUri)
|
||||
if (componentName != null) {
|
||||
intent.setComponent(componentName)
|
||||
}
|
||||
try {
|
||||
startActivity(intent)
|
||||
return true
|
||||
} catch (_: Throwable) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
fun intent1() = launchIntentWithComponent(
|
||||
Intent.ACTION_VIEW,
|
||||
ComponentName(
|
||||
"com.google.android.documentsui",
|
||||
"com.android.documentsui.files.FilesActivity"
|
||||
)
|
||||
)
|
||||
fun intent2() = launchIntentWithComponent(
|
||||
Intent.ACTION_VIEW,
|
||||
ComponentName(
|
||||
"com.android.documentsui",
|
||||
"com.android.documentsui.files.FilesActivity"
|
||||
)
|
||||
)
|
||||
fun intent3() = launchIntentWithComponent(
|
||||
Intent.ACTION_VIEW,
|
||||
ComponentName(
|
||||
"com.android.documentsui",
|
||||
"com.android.documentsui.FilesActivity"
|
||||
)
|
||||
)
|
||||
fun intent4() = launchIntentWithComponent(Intent.ACTION_VIEW)
|
||||
fun intent5() = launchIntentWithComponent("android.provider.action.BROWSE")
|
||||
fun intent6() = launchIntentWithComponent("android.provider.action.BROWSE_DOCUMENT_ROOT")
|
||||
|
||||
return intent1() || intent2() || intent3() || intent4() || intent5() || intent6()
|
||||
}
|
||||
|
||||
inline fun Fragment.getSystemService(name: String): Any? = requireActivity().getSystemService(name)
|
||||
inline fun <T> Fragment.getSystemService(cls: Class<T>): T? = requireActivity().getSystemService(cls)
|
||||
|
||||
inline fun <T: Any> Context.getSystemService(cls: KClass<T>): T? = getSystemService(cls.java)
|
||||
inline fun <T: Any> Fragment.getSystemService(cls: KClass<T>): T? = requireActivity().getSystemService(cls)
|
||||
|
||||
typealias ActivityLauncher = BetterActivityResult<Intent, ActivityResult>
|
||||
|
||||
val ActivityResultCaller.activityResultLauncher get() = BetterActivityResult.registerActivityForResult(this)
|
||||
|
||||
@Suppress("unused")
|
||||
fun getOpenDocumentIntent(vararg fileTypes: String) =
|
||||
Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
|
||||
addCategory(Intent.CATEGORY_OPENABLE)
|
||||
if (fileTypes.size == 1) {
|
||||
type = fileTypes[0]
|
||||
} else {
|
||||
type = "*/*"
|
||||
putExtra(Intent.EXTRA_MIME_TYPES, fileTypes)
|
||||
}
|
||||
}
|
||||
|
||||
fun Context.getFileName(uri: Uri): String {
|
||||
if (uri.scheme == "content") {
|
||||
contentResolver.query(
|
||||
uri,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null
|
||||
)!!.use { cursor ->
|
||||
if (cursor.moveToFirst()) {
|
||||
return cursor.getString(cursor.getColumnIndexOrThrow(OpenableColumns.DISPLAY_NAME))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
throw UnsupportedOperationException("Cannot process non-\"content://\" URIs")
|
||||
}
|
||||
return uri.lastPathSegment.let { name ->
|
||||
name!!
|
||||
val matcher = Pattern.compile("\\w+:(.*/)*(.*)").matcher(name)
|
||||
if (matcher.find()) {
|
||||
matcher.group(2)
|
||||
} else {
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
inline fun Fragment.getFileName(uri: Uri) = requireActivity().getFileName(uri)
|
||||
|
||||
@Suppress("unused")
|
||||
val UsbInterface.endpointList: List<UsbEndpoint> get() {
|
||||
val list = mutableListOf<UsbEndpoint>()
|
||||
for (i in 0 until endpointCount) {
|
||||
list.add(getEndpoint(i))
|
||||
}
|
||||
return list.toList()
|
||||
}
|
||||
|
||||
fun Activity.resolveAttr(@AttrRes id: Int): Int? {
|
||||
val typedValue = TypedValue()
|
||||
if (theme.resolveAttribute(
|
||||
id,
|
||||
typedValue,
|
||||
true
|
||||
)) {
|
||||
val resId = typedValue.resourceId
|
||||
return if (resId != 0) resId else null
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("UNUSED_PARAMETER", "unused", "MemberVisibilityCanBePrivate")
|
||||
class QuickAlertDialogBuilder(context: Context): MaterialAlertDialogBuilder(context) {
|
||||
fun title(title: CharSequence): QuickAlertDialogBuilder {
|
||||
setTitle(title)
|
||||
return this
|
||||
}
|
||||
fun title(@StringRes titleRes: Int): QuickAlertDialogBuilder {
|
||||
setTitle(titleRes)
|
||||
return this
|
||||
}
|
||||
|
||||
fun message(message: CharSequence): QuickAlertDialogBuilder {
|
||||
setMessage(message)
|
||||
return this
|
||||
}
|
||||
fun message(@StringRes messageRes: Int): QuickAlertDialogBuilder {
|
||||
setMessage(messageRes)
|
||||
return this
|
||||
}
|
||||
|
||||
fun body(body: CharSequence) = message(body)
|
||||
fun body(@StringRes bodyRes: Int) = message(bodyRes)
|
||||
|
||||
inline fun positiveButton(
|
||||
text: CharSequence,
|
||||
crossinline listener: () -> Unit
|
||||
) : QuickAlertDialogBuilder {
|
||||
setPositiveButton(text) { _, _ -> listener() }
|
||||
return this
|
||||
}
|
||||
inline fun positiveButton(@StringRes textRes: Int, crossinline listener: () -> Unit): QuickAlertDialogBuilder {
|
||||
setPositiveButton(textRes) { _, _ -> listener() }
|
||||
return this
|
||||
}
|
||||
inline fun positiveButton(text: CharSequence, listener: Nothing? = null): QuickAlertDialogBuilder {
|
||||
setPositiveButton(text, null)
|
||||
return this
|
||||
}
|
||||
inline fun positiveButton(@StringRes text: Int, listener: Nothing? = null): QuickAlertDialogBuilder {
|
||||
setPositiveButton(text, null)
|
||||
return this
|
||||
}
|
||||
|
||||
inline fun negativeButton(text: CharSequence, crossinline listener: () -> Unit): QuickAlertDialogBuilder {
|
||||
setNegativeButton(text) { _, _ -> listener() }
|
||||
return this
|
||||
}
|
||||
inline fun negativeButton(@StringRes textRes: Int, crossinline listener: () -> Unit): QuickAlertDialogBuilder {
|
||||
setNegativeButton(textRes) { _, _ -> listener() }
|
||||
return this
|
||||
}
|
||||
inline fun negativeButton(text: CharSequence, listener: Nothing? = null): QuickAlertDialogBuilder {
|
||||
setNegativeButton(text, null)
|
||||
return this
|
||||
}
|
||||
inline fun negativeButton(@StringRes text: Int, listener: Nothing? = null): QuickAlertDialogBuilder {
|
||||
setNegativeButton(text, null)
|
||||
return this
|
||||
}
|
||||
|
||||
inline fun neutralButton(text: CharSequence, crossinline listener: () -> Unit): QuickAlertDialogBuilder {
|
||||
setNeutralButton(text) { _, _ -> listener() }
|
||||
return this
|
||||
}
|
||||
inline fun neutralButton(@StringRes textRes: Int, crossinline listener: () -> Unit): QuickAlertDialogBuilder {
|
||||
setNeutralButton(textRes) { _, _ -> listener() }
|
||||
return this
|
||||
}
|
||||
inline fun neutralButton(text: CharSequence, listener: Nothing? = null): QuickAlertDialogBuilder {
|
||||
setNeutralButton(text, null)
|
||||
return this
|
||||
}
|
||||
inline fun neutralButton(@StringRes text: Int, listener: Nothing? = null): QuickAlertDialogBuilder {
|
||||
setNeutralButton(text, null)
|
||||
return this
|
||||
}
|
||||
|
||||
inline fun onCancel(crossinline listener: () -> Unit): QuickAlertDialogBuilder {
|
||||
setOnCancelListener { listener() }
|
||||
return this
|
||||
}
|
||||
|
||||
inline fun cancelable(value: Boolean): QuickAlertDialogBuilder {
|
||||
setCancelable(value)
|
||||
return this
|
||||
}
|
||||
}
|
||||
|
||||
inline fun Activity.alertDialog(crossinline config: QuickAlertDialogBuilder.() -> Unit): AlertDialog {
|
||||
val builder = QuickAlertDialogBuilder(this)
|
||||
config(builder)
|
||||
return builder.show()
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
inline fun Fragment.alertDialog(crossinline config: QuickAlertDialogBuilder.() -> Unit)
|
||||
= requireActivity().alertDialog(config)
|
||||
|
||||
@Suppress("unused")
|
||||
class AuthenticationConfig {
|
||||
var success: ((BiometricPrompt.AuthenticationResult) -> Unit)? = null
|
||||
var fail: (() -> Unit)? = null
|
||||
var error: ((Int, String) -> Unit)? = null
|
||||
var always: (() -> Unit)? = null
|
||||
|
||||
fun success(block: (BiometricPrompt.AuthenticationResult) -> Unit) {
|
||||
success = block
|
||||
}
|
||||
|
||||
inline fun ail(noinline block: () -> Unit) {
|
||||
fail = block
|
||||
}
|
||||
|
||||
inline fun error(noinline block: (Int, String) -> Unit) {
|
||||
error = block
|
||||
}
|
||||
|
||||
inline fun always(noinline block: () -> Unit) {
|
||||
always = block
|
||||
}
|
||||
|
||||
lateinit var title: String
|
||||
var subtitle: String? = null
|
||||
lateinit var negativeButtonText: String
|
||||
}
|
||||
|
||||
inline fun FragmentActivity.requestAuthentication(crossinline callback: AuthenticationConfig.() -> Unit): BiometricPrompt {
|
||||
val config = AuthenticationConfig()
|
||||
callback(config)
|
||||
try {
|
||||
config.title
|
||||
config.negativeButtonText
|
||||
} catch (_: UninitializedPropertyAccessException) {
|
||||
throw IllegalStateException("Required fields haven't been set")
|
||||
}
|
||||
val executor = ContextCompat.getMainExecutor(this)
|
||||
val biometricPrompt = BiometricPrompt(
|
||||
this,
|
||||
executor,
|
||||
object: BiometricPrompt.AuthenticationCallback() {
|
||||
override fun onAuthenticationFailed() {
|
||||
super.onAuthenticationFailed()
|
||||
config.fail?.invoke()
|
||||
}
|
||||
|
||||
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
|
||||
super.onAuthenticationSucceeded(result)
|
||||
config.success?.invoke(result)
|
||||
config.always?.invoke()
|
||||
}
|
||||
|
||||
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
|
||||
super.onAuthenticationError(errorCode, errString)
|
||||
config.error?.invoke(errorCode, "$errString")
|
||||
config.always?.invoke()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
val promptInfo = BiometricPrompt.PromptInfo.Builder().apply {
|
||||
setTitle(config.title)
|
||||
setSubtitle(config.subtitle)
|
||||
setNegativeButtonText(config.negativeButtonText)
|
||||
setAllowedAuthenticators(BiometricManager.Authenticators.BIOMETRIC_STRONG)
|
||||
setConfirmationRequired(false)
|
||||
}.build()
|
||||
biometricPrompt.authenticate(promptInfo)
|
||||
return biometricPrompt
|
||||
}
|
||||
|
||||
fun ContentScale.asAndroidScaleType(): ScaleType? {
|
||||
return when (this) {
|
||||
ContentScale.Fit -> ScaleType.FIT_CENTER
|
||||
ContentScale.Crop -> ScaleType.CENTER_CROP
|
||||
ContentScale.FillWidth,
|
||||
ContentScale.FillHeight,
|
||||
ContentScale.FillBounds -> ScaleType.FIT_XY
|
||||
ContentScale.Inside -> ScaleType.CENTER_INSIDE
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
inline fun Context.openUrl(url: Uri) {
|
||||
startActivity(Intent(Intent.ACTION_VIEW, url))
|
||||
}
|
||||
|
||||
inline fun Context.openUrl(url: String) {
|
||||
openUrl(
|
||||
url.let {
|
||||
val regex = "^[\\w-_]://".toRegex()
|
||||
var th = it
|
||||
if (!regex.containsMatchIn(th)) {
|
||||
th = "https://$th"
|
||||
}
|
||||
th
|
||||
}.toUri()
|
||||
)
|
||||
}
|
||||
|
||||
inline fun ComponentActivity.ComposeView(crossinline init: @Composable () -> Unit) =
|
||||
ComposeView(this).apply {
|
||||
setContent {
|
||||
init()
|
||||
}
|
||||
}
|
||||
|
||||
inline fun backCallback(enabled: Boolean = true, crossinline callback: OnBackPressedCallback.() -> Unit)
|
||||
= object: OnBackPressedCallback(enabled) {
|
||||
override fun handleOnBackPressed() {
|
||||
callback(this)
|
||||
}
|
||||
}
|
||||
|
||||
fun String.encrypt(password: String): String {
|
||||
val salt = ByteArray(16).also { SecureRandom().nextBytes(it) }
|
||||
val spec = PBEKeySpec(password.toCharArray(), salt, 65536, 256)
|
||||
val factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1")
|
||||
val secretKey = factory.generateSecret(spec)
|
||||
|
||||
val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding")
|
||||
val iv = ByteArray(cipher.blockSize).also { SecureRandom().nextBytes(it) }
|
||||
val params = IvParameterSpec(iv)
|
||||
cipher.init(Cipher.ENCRYPT_MODE, secretKey, params)
|
||||
|
||||
val encryptedBytes = cipher.doFinal(this.toByteArray())
|
||||
return "${Base64.encodeToString(salt, Base64.DEFAULT)}:${Base64.encodeToString(iv, Base64.DEFAULT)}:${Base64.encodeToString(encryptedBytes, Base64.DEFAULT)}"
|
||||
}
|
||||
|
||||
fun String.decrypt(password: String): String {
|
||||
val parts = this.split(":")
|
||||
val salt = Base64.decode(parts[0], Base64.DEFAULT)
|
||||
val iv = Base64.decode(parts[1], Base64.DEFAULT)
|
||||
val encryptedBytes = Base64.decode(parts[2], Base64.DEFAULT)
|
||||
|
||||
val spec = PBEKeySpec(password.toCharArray(), salt, 65536, 256)
|
||||
val factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1")
|
||||
val secretKey = factory.generateSecret(spec)
|
||||
|
||||
val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding")
|
||||
val params = IvParameterSpec(iv)
|
||||
cipher.init(Cipher.DECRYPT_MODE, secretKey, params)
|
||||
|
||||
val decryptedBytes = cipher.doFinal(encryptedBytes)
|
||||
return String(decryptedBytes)
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
inline fun waitUntil(timeout: Long = 0, condition: () -> Boolean): Boolean {
|
||||
val time = System.currentTimeMillis()
|
||||
while (!condition()) {
|
||||
if (timeout > 0 && System.currentTimeMillis() - time > timeout) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
inline fun waitWhile(timeout: Long = 0, condition: () -> Boolean): Boolean {
|
||||
val time = System.currentTimeMillis()
|
||||
while (condition()) {
|
||||
if (timeout > 0 && System.currentTimeMillis() - time > timeout) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
inline fun orientationSensorEventListener(
|
||||
crossinline callback: (Float, Float, Float) -> Unit
|
||||
) = object: SensorEventListener {
|
||||
private var accelerometerData: FloatArray? = null
|
||||
private var geomagneticData: FloatArray? = null
|
||||
|
||||
override fun onSensorChanged(event: SensorEvent) {
|
||||
if (event.sensor.type == Sensor.TYPE_ACCELEROMETER) accelerometerData = event.values
|
||||
if (event.sensor.type == Sensor.TYPE_MAGNETIC_FIELD) geomagneticData = event.values
|
||||
if (accelerometerData != null && geomagneticData != null) {
|
||||
val R = FloatArray(9)
|
||||
val success = SensorManager.getRotationMatrix(
|
||||
R,
|
||||
FloatArray(9),
|
||||
accelerometerData,
|
||||
geomagneticData
|
||||
)
|
||||
if (success) {
|
||||
val orientationData = FloatArray(3)
|
||||
SensorManager.getOrientation(R, orientationData)
|
||||
callback(
|
||||
orientationData[0],
|
||||
orientationData[1],
|
||||
orientationData[2]
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onAccuracyChanged(sensor: Sensor, accuracy: Int) {}
|
||||
}
|
||||
|
||||
inline fun SensorEventListener(crossinline callback: (SensorEvent) -> Unit) = object: SensorEventListener {
|
||||
override fun onSensorChanged(event: SensorEvent) {
|
||||
callback(event)
|
||||
}
|
||||
override fun onAccuracyChanged(sensor: Sensor, accuracy: Int) {}
|
||||
}
|
||||
|
||||
inline val Context.isScreenLocked get() = (getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager).isKeyguardLocked
|
||||
|
||||
@Suppress("MemberVisibilityCanBePrivate", "unused")
|
||||
class NotificationIdManager(vararg reservedIds: Int) {
|
||||
private val reserved = reservedIds.toMutableList()
|
||||
|
||||
fun reserve(id: Int) {
|
||||
reserved.add(id)
|
||||
}
|
||||
|
||||
fun release(id: Int) {
|
||||
reserved.remove(id)
|
||||
}
|
||||
|
||||
fun get(): Int {
|
||||
while (true) {
|
||||
val random = Random.nextInt(0..Int.MAX_VALUE)
|
||||
if (!reserved.contains(random)) {
|
||||
return random
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun getAndReserve(): Int {
|
||||
val id = get()
|
||||
reserve(id)
|
||||
return id
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun PaddingValues.copy(
|
||||
start: Dp? = null,
|
||||
end: Dp? = null,
|
||||
top: Dp? = null,
|
||||
bottom: Dp? = null
|
||||
) = PaddingValues(
|
||||
start = start ?: calculateStartPadding(LocalLayoutDirection.current),
|
||||
end = end ?: calculateEndPadding(LocalLayoutDirection.current),
|
||||
top = top ?: calculateTopPadding(),
|
||||
bottom = bottom ?: calculateBottomPadding()
|
||||
)
|
||||
|
||||
@Suppress("unused")
|
||||
@Composable
|
||||
fun PaddingValues.copy(
|
||||
horizontal: Dp? = null,
|
||||
vertical: Dp? = null
|
||||
) = copy(
|
||||
start = horizontal,
|
||||
end = horizontal,
|
||||
top = vertical,
|
||||
bottom = vertical
|
||||
)
|
||||
|
||||
inline fun InputStream.test() =
|
||||
try {
|
||||
read()
|
||||
close()
|
||||
true
|
||||
} catch (_: IOException) {
|
||||
false
|
||||
}
|
||||
|
||||
inline fun ContentResolver.test(item: Uri) =
|
||||
try {
|
||||
openInputStream(item)!!.test()
|
||||
} catch (_: Exception) {
|
||||
false
|
||||
}
|
||||
|
||||
inline fun <reified T: Parcelable> Intent.getParcelableExtraAs(key: String): T {
|
||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
getParcelableExtra(key, T::class.java)
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
getParcelableExtra(key)
|
||||
}!!
|
||||
}
|
||||
|
||||
inline fun <reified T: Serializable> Intent.getSerializableExtraAs(key: String): T {
|
||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
getSerializableExtra(key, T::class.java)
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
getSerializableExtra(key) as T
|
||||
}!!
|
||||
}
|
||||
|
||||
fun ActivityInfo.contentEquals(other: ActivityInfo): Boolean {
|
||||
return (
|
||||
(
|
||||
if (Build.VERSION.SDK_INT >= 26)
|
||||
colorMode == other.colorMode
|
||||
else true
|
||||
) &&
|
||||
(
|
||||
if (Build.VERSION.SDK_INT >= 34)
|
||||
requiredDisplayCategory == other.requiredDisplayCategory
|
||||
else true
|
||||
) &&
|
||||
theme == other.theme &&
|
||||
launchMode == other.launchMode &&
|
||||
documentLaunchMode == other.documentLaunchMode &&
|
||||
permission == other.permission &&
|
||||
taskAffinity == other.taskAffinity &&
|
||||
targetActivity == other.targetActivity &&
|
||||
flags == other.flags &&
|
||||
screenOrientation == other.screenOrientation &&
|
||||
configChanges == other.configChanges &&
|
||||
softInputMode == other.softInputMode &&
|
||||
uiOptions == other.uiOptions &&
|
||||
parentActivityName == other.parentActivityName &&
|
||||
maxRecents == other.maxRecents &&
|
||||
windowLayout == other.windowLayout
|
||||
)
|
||||
}
|
||||
|
||||
val ActivityInfo.launchIntent get() = Intent().apply {
|
||||
component = ComponentName(packageName!!, name)
|
||||
flags = FLAG_ACTIVITY_NEW_TASK
|
||||
}
|
||||
|
||||
@RequiresPermission(Manifest.permission.QUERY_ALL_PACKAGES)
|
||||
fun ActivityInfo.isLauncher(context: Context): Boolean {
|
||||
with (context) {
|
||||
@Suppress("ExplicitThis")
|
||||
val intent = launchIntent.apply {
|
||||
action = ACTION_MAIN
|
||||
addCategory(CATEGORY_LAUNCHER)
|
||||
component = ComponentName(this@isLauncher.packageName!!, name)
|
||||
flags = FLAG_ACTIVITY_NEW_TASK
|
||||
}
|
||||
val infos = packageManager.queryIntentActivities(intent, PackageManager.GET_RESOLVED_FILTER)
|
||||
for (info in infos) {
|
||||
val filter = info.filter
|
||||
if (
|
||||
info.activityInfo.contentEquals(this@isLauncher) &&
|
||||
filter != null &&
|
||||
filter.hasAction(ACTION_MAIN) &&
|
||||
filter.hasCategory(CATEGORY_LAUNCHER)
|
||||
) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Suppress("unused", "RedundantSuppression")
|
||||
operator fun <T> WeakReference<T>.getValue(thisRef: Any?, property: KProperty<*>) = get()!!
|
||||
|
||||
@Suppress("unused")
|
||||
inline fun runOrLog(
|
||||
tag: String,
|
||||
message: String = "An error occurred:",
|
||||
crossinline block: () -> Unit
|
||||
) {
|
||||
try {
|
||||
block()
|
||||
} catch (e: Exception) {
|
||||
Log.e(tag, message, e)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
typealias WidthSizeClass = WindowWidthSizeClass
|
||||
@Suppress("unused")
|
||||
typealias HeightSizeClass = WindowHeightSizeClass
|
||||
@Suppress("unused")
|
||||
typealias SizeClass = WindowSizeClass
|
||||
|
||||
@RequiresPermission(Manifest.permission.POST_NOTIFICATIONS)
|
||||
inline fun Context.notifyIfAllowed(
|
||||
id: Int,
|
||||
notification: Notification
|
||||
) {
|
||||
with (NotificationManagerCompat.from(this)) {
|
||||
if (
|
||||
checkSelfPermission(
|
||||
Manifest.permission.POST_NOTIFICATIONS
|
||||
) != PackageManager.PERMISSION_GRANTED
|
||||
) {
|
||||
return@with
|
||||
}
|
||||
notify(id, notification)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
fun BaseBundle.toMap(): Map<String, Any> {
|
||||
val map = mutableMapOf<String, Any>()
|
||||
for (key in keySet()) {
|
||||
map[key] = get(key)!!
|
||||
}
|
||||
return map
|
||||
}
|
||||
|
||||
fun Bundle.toPersistableBundle(): PersistableBundle {
|
||||
val map = toMap()
|
||||
val bundle = PersistableBundle()
|
||||
for ((key, value) in map) {
|
||||
when (value) {
|
||||
is Int -> bundle.putInt(key, value)
|
||||
is Long -> bundle.putLong(key, value)
|
||||
is Double -> bundle.putDouble(key, value)
|
||||
is String -> bundle.putString(key, value)
|
||||
is IntArray -> bundle.putIntArray(key, value)
|
||||
is LongArray -> bundle.putLongArray(key, value)
|
||||
is DoubleArray -> bundle.putDoubleArray(key, value)
|
||||
is PersistableBundle -> bundle.putPersistableBundle(key, value)
|
||||
is Boolean -> bundle.putBoolean(key, value)
|
||||
is BooleanArray -> bundle.putBooleanArray(key, value)
|
||||
is Array<*> -> {
|
||||
if (value.isArrayOf<String>()) {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
bundle.putStringArray(key, value as Array<String>)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return bundle
|
||||
}
|
||||
|
||||
inline fun String.decodeWindows1251() = String(
|
||||
toByteArray(Charsets.ISO_8859_1),
|
||||
Charsets.UTF_8
|
||||
)
|
||||
|
||||
@get:ChecksSdkIntAtLeast(api = Build.VERSION_CODES.S)
|
||||
actual val materialYouAvailable get() = Build.VERSION.SDK_INT >= 31
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.pr0gramm3r101.utils
|
||||
|
||||
import android.content.Context
|
||||
import java.lang.ref.WeakReference
|
||||
|
||||
actual object UtilsLibrary {
|
||||
private var init = false
|
||||
private lateinit var _context: WeakReference<Context>
|
||||
internal val context get() = _context.get()!!
|
||||
|
||||
actual fun init(vararg args: Any) {
|
||||
_context = WeakReference(args[0] as Context)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.pr0gramm3r101.utils.crypto
|
||||
|
||||
import android.util.Base64 as AndroidBase64
|
||||
|
||||
actual object Base64 {
|
||||
actual fun encode(bytes: ByteArray): String {
|
||||
return AndroidBase64.encodeToString(bytes, AndroidBase64.NO_WRAP)
|
||||
}
|
||||
|
||||
actual fun decode(base64: String): ByteArray {
|
||||
return AndroidBase64.decode(base64, AndroidBase64.NO_WRAP)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.pr0gramm3r101.utils.crypto
|
||||
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import javax.crypto.Mac
|
||||
import javax.crypto.spec.SecretKeySpec
|
||||
|
||||
actual object Hmac {
|
||||
actual suspend fun hmacSha256(key: ByteArray, data: ByteArray): ByteArray = withContext(Dispatchers.Default) {
|
||||
val mac = Mac.getInstance("HmacSHA256")
|
||||
val secretKey = SecretKeySpec(key, "HmacSHA256")
|
||||
mac.init(secretKey)
|
||||
mac.doFinal(data)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
package com.pr0gramm3r101.utils.settings
|
||||
|
||||
import android.content.Context
|
||||
import androidx.datastore.core.DataStore
|
||||
import androidx.datastore.preferences.core.Preferences
|
||||
import androidx.datastore.preferences.core.booleanPreferencesKey
|
||||
import androidx.datastore.preferences.core.edit
|
||||
import androidx.datastore.preferences.core.floatPreferencesKey
|
||||
import androidx.datastore.preferences.core.intPreferencesKey
|
||||
import androidx.datastore.preferences.core.longPreferencesKey
|
||||
import androidx.datastore.preferences.core.stringPreferencesKey
|
||||
import androidx.datastore.preferences.core.stringSetPreferencesKey
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import com.pr0gramm3r101.utils.settings.DataStoreSingleton
|
||||
|
||||
class AndroidSettings(private val context: Context): Settings {
|
||||
private val dataStore: DataStore<Preferences>
|
||||
get() = DataStoreSingleton.dataStore
|
||||
override fun putString(key: String, value: String) {
|
||||
runBlocking {
|
||||
dataStore.edit { preferences ->
|
||||
preferences[stringPreferencesKey(key)] = value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getString(key: String, default: String) = runBlocking {
|
||||
dataStore.data.map { preferences ->
|
||||
preferences[stringPreferencesKey(key)] ?: default
|
||||
}.first()
|
||||
}
|
||||
|
||||
override fun putInt(key: String, value: Int) = runBlocking {
|
||||
dataStore.edit { preferences ->
|
||||
preferences[intPreferencesKey(key)] = value
|
||||
}
|
||||
Unit
|
||||
}
|
||||
|
||||
override fun getInt(key: String, default: Int) = runBlocking {
|
||||
dataStore.data.map { preferences ->
|
||||
preferences[intPreferencesKey(key)] ?: default
|
||||
}.first()
|
||||
}
|
||||
|
||||
override fun putLong(key: String, value: Long) = runBlocking {
|
||||
dataStore.edit { preferences ->
|
||||
preferences[longPreferencesKey(key)] = value
|
||||
}
|
||||
Unit
|
||||
}
|
||||
|
||||
override fun getLong(key: String, default: Long) = runBlocking {
|
||||
dataStore.data.map { preferences ->
|
||||
preferences[longPreferencesKey(key)] ?: default
|
||||
}.first()
|
||||
}
|
||||
|
||||
override fun putFloat(key: String, value: Float) = runBlocking {
|
||||
dataStore.edit { preferences ->
|
||||
preferences[floatPreferencesKey(key)] = value
|
||||
}
|
||||
Unit
|
||||
}
|
||||
|
||||
override fun getFloat(key: String, default: Float) = runBlocking {
|
||||
dataStore.data.map { preferences ->
|
||||
preferences[floatPreferencesKey(key)] ?: default
|
||||
}.first()
|
||||
}
|
||||
|
||||
override fun putBoolean(key: String, value: Boolean) = runBlocking {
|
||||
dataStore.edit { preferences ->
|
||||
preferences[booleanPreferencesKey(key)] = value
|
||||
}
|
||||
Unit
|
||||
}
|
||||
|
||||
override fun getBoolean(key: String, default: Boolean) = runBlocking {
|
||||
dataStore.data.map { preferences ->
|
||||
preferences[booleanPreferencesKey(key)] ?: default
|
||||
}.first()
|
||||
}
|
||||
|
||||
override fun putStringSet(key: String, value: Set<String>) = runBlocking {
|
||||
dataStore.edit { preferences ->
|
||||
preferences[stringSetPreferencesKey(key)] = value
|
||||
}
|
||||
Unit
|
||||
}
|
||||
|
||||
override fun getStringSet(key: String, default: Set<String>) = runBlocking {
|
||||
dataStore.data.map { preferences ->
|
||||
preferences[stringSetPreferencesKey(key)] ?: default
|
||||
}.first()
|
||||
}
|
||||
|
||||
override fun putStringList(key: String, value: List<String>) = putStringSet(key, value.toSet())
|
||||
|
||||
override fun getStringList(key: String, default: List<String>) = getStringSet(key, default.toSet()).toList()
|
||||
|
||||
override fun remove(key: String) = runBlocking {
|
||||
dataStore.edit { preferences ->
|
||||
preferences.remove(stringPreferencesKey(key))
|
||||
preferences.remove(intPreferencesKey(key))
|
||||
preferences.remove(longPreferencesKey(key))
|
||||
preferences.remove(floatPreferencesKey(key))
|
||||
preferences.remove(booleanPreferencesKey(key))
|
||||
preferences.remove(stringSetPreferencesKey(key))
|
||||
}
|
||||
Unit
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
package com.pr0gramm3r101.utils.settings
|
||||
|
||||
import androidx.datastore.core.DataStore
|
||||
import androidx.datastore.preferences.core.Preferences
|
||||
import androidx.datastore.preferences.core.booleanPreferencesKey
|
||||
import androidx.datastore.preferences.core.edit
|
||||
import androidx.datastore.preferences.core.floatPreferencesKey
|
||||
import androidx.datastore.preferences.core.intPreferencesKey
|
||||
import androidx.datastore.preferences.core.longPreferencesKey
|
||||
import androidx.datastore.preferences.core.stringPreferencesKey
|
||||
import androidx.datastore.preferences.core.stringSetPreferencesKey
|
||||
import com.pr0gramm3r101.utils.settings.DataStoreSingleton
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
||||
class AndroidAsyncSettings : AsyncSettings {
|
||||
private val dataStore: DataStore<Preferences>
|
||||
get() = DataStoreSingleton.dataStore
|
||||
|
||||
override suspend fun putString(key: String, value: String) {
|
||||
dataStore.edit { preferences ->
|
||||
preferences[stringPreferencesKey(key)] = value
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getString(key: String, default: String): String {
|
||||
return dataStore.data.map { preferences ->
|
||||
preferences[stringPreferencesKey(key)] ?: default
|
||||
}.first()
|
||||
}
|
||||
|
||||
override suspend fun putInt(key: String, value: Int) {
|
||||
dataStore.edit { preferences ->
|
||||
preferences[intPreferencesKey(key)] = value
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getInt(key: String, default: Int): Int {
|
||||
return dataStore.data.map { preferences ->
|
||||
preferences[intPreferencesKey(key)] ?: default
|
||||
}.first()
|
||||
}
|
||||
|
||||
override suspend fun putLong(key: String, value: Long) {
|
||||
dataStore.edit { preferences ->
|
||||
preferences[longPreferencesKey(key)] = value
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getLong(key: String, default: Long): Long {
|
||||
return dataStore.data.map { preferences ->
|
||||
preferences[longPreferencesKey(key)] ?: default
|
||||
}.first()
|
||||
}
|
||||
|
||||
override suspend fun putFloat(key: String, value: Float) {
|
||||
dataStore.edit { preferences ->
|
||||
preferences[floatPreferencesKey(key)] = value
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getFloat(key: String, default: Float): Float {
|
||||
return dataStore.data.map { preferences ->
|
||||
preferences[floatPreferencesKey(key)] ?: default
|
||||
}.first()
|
||||
}
|
||||
|
||||
override suspend fun putBoolean(key: String, value: Boolean) {
|
||||
dataStore.edit { preferences ->
|
||||
preferences[booleanPreferencesKey(key)] = value
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getBoolean(key: String, default: Boolean): Boolean {
|
||||
return dataStore.data.map { preferences ->
|
||||
preferences[booleanPreferencesKey(key)] ?: default
|
||||
}.first()
|
||||
}
|
||||
|
||||
override suspend fun putStringSet(key: String, value: Set<String>) {
|
||||
dataStore.edit { preferences ->
|
||||
preferences[stringSetPreferencesKey(key)] = value
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getStringSet(key: String, default: Set<String>): Set<String> {
|
||||
return dataStore.data.map { preferences ->
|
||||
preferences[stringSetPreferencesKey(key)] ?: default
|
||||
}.first()
|
||||
}
|
||||
|
||||
override suspend fun putStringList(key: String, value: List<String>) {
|
||||
putStringSet(key, value.toSet())
|
||||
}
|
||||
|
||||
override suspend fun getStringList(key: String, default: List<String>): List<String> {
|
||||
return getStringSet(key, default.toSet()).toList()
|
||||
}
|
||||
|
||||
override suspend fun remove(key: String) {
|
||||
dataStore.edit { preferences ->
|
||||
preferences.remove(stringPreferencesKey(key))
|
||||
preferences.remove(intPreferencesKey(key))
|
||||
preferences.remove(longPreferencesKey(key))
|
||||
preferences.remove(floatPreferencesKey(key))
|
||||
preferences.remove(booleanPreferencesKey(key))
|
||||
preferences.remove(stringSetPreferencesKey(key))
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun contains(key: String): Boolean {
|
||||
return dataStore.data.map { preferences ->
|
||||
preferences.contains(stringPreferencesKey(key)) ||
|
||||
preferences.contains(intPreferencesKey(key)) ||
|
||||
preferences.contains(longPreferencesKey(key)) ||
|
||||
preferences.contains(floatPreferencesKey(key)) ||
|
||||
preferences.contains(booleanPreferencesKey(key)) ||
|
||||
preferences.contains(stringSetPreferencesKey(key))
|
||||
}.first()
|
||||
}
|
||||
|
||||
override suspend fun clear() {
|
||||
dataStore.edit { preferences ->
|
||||
preferences.clear()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
actual val asyncSettings: AsyncSettings = AndroidAsyncSettings()
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.pr0gramm3r101.utils.settings
|
||||
|
||||
import android.content.Context
|
||||
import androidx.datastore.core.DataStore
|
||||
import androidx.datastore.preferences.core.Preferences
|
||||
import androidx.datastore.preferences.preferencesDataStore
|
||||
import com.pr0gramm3r101.utils.UtilsLibrary
|
||||
|
||||
/**
|
||||
* Singleton DataStore instance to avoid multiple DataStore instances for the same file
|
||||
*/
|
||||
object DataStoreSingleton {
|
||||
private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "settings")
|
||||
|
||||
val dataStore: DataStore<Preferences>
|
||||
get() = UtilsLibrary.context.dataStore
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package com.pr0gramm3r101.utils.settings
|
||||
|
||||
import com.pr0gramm3r101.utils.UtilsLibrary
|
||||
|
||||
actual val settings: Settings get() = AndroidSettings(UtilsLibrary.context)
|
||||
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="show_pw">Показать пароль</string>
|
||||
<string name="hide_pw">Скрыть пароль</string>
|
||||
<string name="smthwentwrong">Что-то пошло не так</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
<string name="show_pw" tools:keep="@string/show_pw">Show password</string>
|
||||
<string name="hide_pw" tools:keep="@string/hide_pw">Hide password</string>
|
||||
<string name="smthwentwrong" tools:keep="@string/smthwentwrong">Something went wrong</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
<style name="Theme.Dialog" parent="Theme.Material3.DayNight.NoActionBar" tools:keep="@style/Theme_Dialog">
|
||||
<item name="android:windowIsTranslucent">true</item>
|
||||
<item name="android:windowBackground">@android:color/transparent</item>
|
||||
<item name="android:windowContentOverlay">@null</item>
|
||||
<item name="android:windowNoTitle">true</item>
|
||||
<item name="android:windowIsFloating">true</item>
|
||||
<item name="android:backgroundDimEnabled">true</item>
|
||||
<item name="windowActionBar">false</item>
|
||||
<item name="android:windowAnimationStyle">@android:style/Animation.Dialog</item>
|
||||
<item name="android:windowFullscreen">true</item>
|
||||
</style>
|
||||
</resources>
|
||||
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="960"
|
||||
android:viewportHeight="960">
|
||||
<path
|
||||
android:pathData="M480,520q-50,0 -85,-35t-35,-85q0,-50 35,-85t85,-35q50,0 85,35t35,85q0,50 -35,85t-85,35ZM240,920v-309q-38,-42 -59,-96t-21,-115q0,-134 93,-227t227,-93q134,0 227,93t93,227q0,61 -21,115t-59,96v309l-240,-80 -240,80ZM480,640q100,0 170,-70t70,-170q0,-100 -70,-170t-170,-70q-100,0 -170,70t-70,170q0,100 70,170t170,70ZM320,801l160,-41 160,41v-124q-35,20 -75.5,31.5T480,720q-44,0 -84.5,-11.5T320,677v124ZM480,739Z"
|
||||
android:fillColor="#5f6368"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="960"
|
||||
android:viewportHeight="960">
|
||||
<path
|
||||
android:pathData="M320,520h80v-120q0,-33 23.5,-56.5T480,320v-80q-66,0 -113,47t-47,113v120ZM160,840q-33,0 -56.5,-23.5T80,760v-80q0,-33 23.5,-56.5T160,600h40v-200q0,-117 81.5,-198.5T480,120q117,0 198.5,81.5T760,400v200h40q33,0 56.5,23.5T880,680v80q0,33 -23.5,56.5T800,840L160,840Z"
|
||||
android:fillColor="#5f6368"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="960"
|
||||
android:viewportHeight="960">
|
||||
<path
|
||||
android:pathData="M160,760h640v-80L160,680v80ZM320,520h80v-120q0,-33 23.5,-56.5T480,320v-80q-66,0 -113,47t-47,113v120ZM480,680ZM280,600h400v-200q0,-83 -58.5,-141.5T480,200q-83,0 -141.5,58.5T280,400v200ZM160,840q-33,0 -56.5,-23.5T80,760v-80q0,-33 23.5,-56.5T160,600h40v-200q0,-117 81.5,-198.5T480,120q117,0 198.5,81.5T760,400v200h40q33,0 56.5,23.5T880,680v80q0,33 -23.5,56.5T800,840L160,840ZM480,600Z"
|
||||
android:fillColor="#5f6368"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,29 @@
|
||||
@file:Suppress("UnusedReceiverParameter", "unused")
|
||||
|
||||
package com.pr0gramm3r101.ui
|
||||
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Language
|
||||
import androidx.compose.material.icons.outlined.Language
|
||||
import androidx.compose.runtime.Composable
|
||||
import org.jetbrains.compose.resources.vectorResource
|
||||
import com.pr0gramm3r101.utils.Res
|
||||
import com.pr0gramm3r101.utils.license
|
||||
import com.pr0gramm3r101.utils.siren_filled
|
||||
import com.pr0gramm3r101.utils.siren_outlined
|
||||
|
||||
inline val Icons.Outlined.Internet get() = Icons.Outlined.Language
|
||||
inline val Icons.Filled.Internet get() = Icons.Filled.Language
|
||||
|
||||
inline val Icons.Outlined.Website get() = Icons.Outlined.Language
|
||||
inline val Icons.Filled.Website get() = Icons.Filled.Language
|
||||
|
||||
inline val Icons.Outlined.License
|
||||
@Composable get() = vectorResource(Res.drawable.license)
|
||||
inline val Icons.Filled.License
|
||||
@Composable get() = Icons.Outlined.License
|
||||
|
||||
inline val Icons.Outlined.Siren
|
||||
@Composable get() = vectorResource(Res.drawable.siren_outlined)
|
||||
inline val Icons.Filled.Siren
|
||||
@Composable get() = vectorResource(Res.drawable.siren_filled)
|
||||
@@ -0,0 +1,290 @@
|
||||
@file:JvmName("Adaptive")
|
||||
|
||||
package com.pr0gramm3r101.utils
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.unit.IntSize
|
||||
import androidx.compose.ui.unit.toSize
|
||||
import kotlin.jvm.JvmField
|
||||
import kotlin.jvm.JvmName
|
||||
import kotlin.jvm.JvmStatic
|
||||
|
||||
@Composable expect inline fun currentWindowSize(): IntSize
|
||||
|
||||
/**
|
||||
* Calculates and returns [WindowAdaptiveInfo] of the provided context. It's a convenient function
|
||||
* that uses the default [WindowSizeClass] constructor to retrieve [WindowSizeClass].
|
||||
*
|
||||
* @return [WindowAdaptiveInfo] of the provided context
|
||||
*/
|
||||
@Composable
|
||||
fun currentWindowAdaptiveInfo(): WindowAdaptiveInfo {
|
||||
val windowSize = with(LocalDensity.current) { currentWindowSize().toSize().toDpSize() }
|
||||
return WindowAdaptiveInfo(
|
||||
WindowSizeClass.compute(windowSize.width.value, windowSize.height.value)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* This class collects window info that affects adaptation decisions. An adaptive layout is supposed
|
||||
* to use the info from this class to decide how the layout is supposed to be adapted.
|
||||
*
|
||||
* @param windowSizeClass [WindowSizeClass] of the current window.
|
||||
* @constructor create an instance of [WindowAdaptiveInfo]
|
||||
*/
|
||||
@Immutable
|
||||
class WindowAdaptiveInfo(val windowSizeClass: WindowSizeClass) {
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (other !is WindowAdaptiveInfo) return false
|
||||
if (windowSizeClass != other.windowSizeClass) return false
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode() = windowSizeClass.hashCode()
|
||||
|
||||
override fun toString(): String {
|
||||
return "WindowAdaptiveInfo(windowSizeClass=$windowSizeClass)"
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* [WindowSizeClass] represents breakpoints for a viewport. The recommended width and height break
|
||||
* points are presented through [windowWidthSizeClass] and [windowHeightSizeClass]. Designers
|
||||
* should design around the different combinations of width and height buckets. Developers should
|
||||
* use the different buckets to specify the layouts. Ideally apps will work well in each bucket and
|
||||
* by extension work well across multiple devices. If two devices are in similar buckets they
|
||||
* should behave similarly.
|
||||
*
|
||||
* This class is meant to be a common definition that can be shared across different device types.
|
||||
* Application developers can use WindowSizeClass to have standard window buckets and design the UI
|
||||
* around those buckets. Library developers can use these buckets to create different UI with
|
||||
* respect to each bucket. This will help with consistency across multiple device types.
|
||||
*
|
||||
* A library developer use-case can be creating some navigation UI library. For a size
|
||||
* class with the [WindowWidthSizeClass.EXPANDED] width it might be more reasonable to have a side
|
||||
* navigation. For a [WindowWidthSizeClass.COMPACT] width, a bottom navigation might be a better
|
||||
* fit.
|
||||
*
|
||||
* An application use-case can be applied for apps that use a list-detail pattern. The app can use
|
||||
* the [WindowWidthSizeClass.MEDIUM] to determine if there is enough space to show the list and the
|
||||
* detail side by side. If all apps follow this guidance then it will present a very consistent user
|
||||
* experience.
|
||||
*
|
||||
* In some cases developers or UI systems may decide to create their own break points. A developer
|
||||
* might optimize for a window that is smaller than the supported break points or larger. A UI
|
||||
* system might find that some break points are better suited than the recommended break points.
|
||||
* In these cases developers may wish to specify their own custom break points and match using
|
||||
* a `when` statement.
|
||||
*
|
||||
* @see WindowWidthSizeClass
|
||||
* @see WindowHeightSizeClass
|
||||
*/
|
||||
class WindowSizeClass private constructor(
|
||||
/**
|
||||
* Returns the [WindowWidthSizeClass] that corresponds to the widthDp of the window.
|
||||
*/
|
||||
val windowWidthSizeClass: WindowWidthSizeClass,
|
||||
/**
|
||||
* Returns the [WindowHeightSizeClass] that corresponds to the heightDp of the window.
|
||||
*/
|
||||
val windowHeightSizeClass: WindowHeightSizeClass
|
||||
) {
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (other == null || this::class != other::class) return false
|
||||
|
||||
other as WindowSizeClass
|
||||
|
||||
if (windowWidthSizeClass != other.windowWidthSizeClass) return false
|
||||
if (windowHeightSizeClass != other.windowHeightSizeClass) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = windowWidthSizeClass.hashCode()
|
||||
result = 31 * result + windowHeightSizeClass.hashCode()
|
||||
return result
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return "WindowSizeClass {" +
|
||||
"windowWidthSizeClass=$windowWidthSizeClass, " +
|
||||
"windowHeightSizeClass=$windowHeightSizeClass }"
|
||||
}
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* Computes the recommended [WindowSizeClass] for the given width and height in DP.
|
||||
* @param dpWidth width of a window in DP.
|
||||
* @param dpHeight height of a window in DP.
|
||||
* @return [WindowSizeClass] that is recommended for the given dimensions.
|
||||
* @throws IllegalArgumentException if [dpWidth] or [dpHeight] is
|
||||
* negative.
|
||||
*/
|
||||
@JvmStatic
|
||||
fun compute(dpWidth: Float, dpHeight: Float): WindowSizeClass {
|
||||
return WindowSizeClass(
|
||||
WindowWidthSizeClass.compute(dpWidth),
|
||||
WindowHeightSizeClass.compute(dpHeight)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes the [WindowSizeClass] for the given width and height in pixels with density.
|
||||
* @param widthPx width of a window in PX.
|
||||
* @param heightPx height of a window in PX.
|
||||
* @param density density of the display where the window is shown.
|
||||
* @return [WindowSizeClass] that is recommended for the given dimensions.
|
||||
* @throws IllegalArgumentException if [widthPx], [heightPx], or [density] is
|
||||
* negative.
|
||||
*/
|
||||
@JvmStatic
|
||||
fun compute(widthPx: Int, heightPx: Int, density: Float): WindowSizeClass {
|
||||
val widthDp = widthPx / density
|
||||
val heightDp = heightPx / density
|
||||
return compute(widthDp, heightDp)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A class to represent the width size buckets for a viewport. The possible values are [COMPACT],
|
||||
* [MEDIUM], and [EXPANDED]. [WindowWidthSizeClass] should not be used as a proxy for the device
|
||||
* type. It is possible to have resizeable windows in different device types.
|
||||
* The viewport might change from a [COMPACT] all the way to an [EXPANDED] size class.
|
||||
*/
|
||||
class WindowWidthSizeClass private constructor(
|
||||
private val rawValue: Int
|
||||
) {
|
||||
override fun toString(): String {
|
||||
val name = when (this) {
|
||||
COMPACT -> "COMPACT"
|
||||
MEDIUM -> "MEDIUM"
|
||||
EXPANDED -> "EXPANDED"
|
||||
else -> "UNKNOWN"
|
||||
}
|
||||
return "WindowWidthSizeClass: $name"
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (other == null) return false
|
||||
if (this::class != other::class) return false
|
||||
|
||||
val that = other as WindowWidthSizeClass
|
||||
|
||||
return rawValue == that.rawValue
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return rawValue
|
||||
}
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* A bucket to represent a compact width window, typical for a phone in portrait.
|
||||
*/
|
||||
@JvmField
|
||||
val COMPACT: WindowWidthSizeClass = WindowWidthSizeClass(0)
|
||||
|
||||
/**
|
||||
* A bucket to represent a medium width window, typical for a phone in landscape or
|
||||
* a tablet.
|
||||
*/
|
||||
@JvmField
|
||||
val MEDIUM: WindowWidthSizeClass = WindowWidthSizeClass(1)
|
||||
|
||||
/**
|
||||
* A bucket to represent an expanded width window, typical for a large tablet or desktop
|
||||
* form-factor.
|
||||
*/
|
||||
@JvmField
|
||||
val EXPANDED: WindowWidthSizeClass = WindowWidthSizeClass(2)
|
||||
|
||||
/**
|
||||
* Returns a recommended [WindowWidthSizeClass] for the width of a window given the width
|
||||
* in DP.
|
||||
* @param dpWidth the width of the window in DP
|
||||
* @return A recommended size class for the width
|
||||
* @throws IllegalArgumentException if the width is negative
|
||||
*/
|
||||
internal fun compute(dpWidth: Float): WindowWidthSizeClass {
|
||||
require(dpWidth >= 0) { "Width must be positive, received $dpWidth" }
|
||||
return when {
|
||||
dpWidth < 600 -> COMPACT
|
||||
dpWidth < 840 -> MEDIUM
|
||||
else -> EXPANDED
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class WindowHeightSizeClass private constructor(
|
||||
private val rawValue: Int
|
||||
) {
|
||||
|
||||
override fun toString(): String {
|
||||
val name = when (this) {
|
||||
COMPACT -> "COMPACT"
|
||||
MEDIUM -> "MEDIUM"
|
||||
EXPANDED -> "EXPANDED"
|
||||
else -> "UNKNOWN"
|
||||
}
|
||||
return "WindowHeightSizeClass: $name"
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (other == null) return false
|
||||
if (this::class != other::class) return false
|
||||
|
||||
val that = other as WindowHeightSizeClass
|
||||
|
||||
return rawValue == that.rawValue
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return rawValue
|
||||
}
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* A bucket to represent a compact height, typical for a phone that is in landscape.
|
||||
*/
|
||||
@JvmField
|
||||
val COMPACT: WindowHeightSizeClass = WindowHeightSizeClass(0)
|
||||
|
||||
/**
|
||||
* A bucket to represent a medium height, typical for a phone in portrait or a tablet.
|
||||
*/
|
||||
@JvmField
|
||||
val MEDIUM: WindowHeightSizeClass = WindowHeightSizeClass(1)
|
||||
|
||||
/**
|
||||
* A bucket to represent an expanded height window, typical for a large tablet or a
|
||||
* desktop form-factor.
|
||||
*/
|
||||
@JvmField
|
||||
val EXPANDED: WindowHeightSizeClass = WindowHeightSizeClass(2)
|
||||
|
||||
/**
|
||||
* Returns a recommended [WindowHeightSizeClass] for the height of a window given the
|
||||
* height in DP.
|
||||
* @param dpHeight the height of the window in DP
|
||||
* @return A recommended size class for the height
|
||||
* @throws IllegalArgumentException if the height is negative
|
||||
*/
|
||||
internal fun compute(dpHeight: Float): WindowHeightSizeClass {
|
||||
require(dpHeight >= 0) { "Height must be positive, received $dpHeight" }
|
||||
return when {
|
||||
dpHeight < 480 -> COMPACT
|
||||
dpHeight < 900 -> MEDIUM
|
||||
else -> EXPANDED
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
package com.pr0gramm3r101.utils
|
||||
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.NavigationBar
|
||||
import androidx.compose.material3.NavigationBarItem
|
||||
import androidx.compose.material3.NavigationRail
|
||||
import androidx.compose.material3.NavigationRailItem
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
|
||||
data class NavigationItem(
|
||||
val name: String,
|
||||
val selectedIcon: ImageVector,
|
||||
val unselectedIcon: ImageVector = selectedIcon,
|
||||
val onClick: () -> Unit
|
||||
)
|
||||
|
||||
@Composable
|
||||
fun SimpleNavigationBar(
|
||||
selectedItem: Int,
|
||||
vararg items: NavigationItem,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
NavigationBar(modifier) {
|
||||
items.forEachIndexed { index, item ->
|
||||
NavigationBarItem(
|
||||
icon = {
|
||||
Icon(
|
||||
imageVector =
|
||||
if (selectedItem == index)
|
||||
items[index].selectedIcon
|
||||
else
|
||||
items[index].unselectedIcon,
|
||||
contentDescription = item.name
|
||||
)
|
||||
},
|
||||
label = { Text(item.name) },
|
||||
selected = selectedItem == index,
|
||||
onClick = items[index].onClick
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SimpleNavigationRail(
|
||||
selectedItem: Int,
|
||||
vararg items: NavigationItem,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
NavigationRail(
|
||||
modifier,
|
||||
containerColor = MaterialTheme.colorScheme.surfaceContainer
|
||||
) {
|
||||
items.forEachIndexed { index, item ->
|
||||
NavigationRailItem(
|
||||
icon = {
|
||||
Icon(
|
||||
imageVector =
|
||||
if (selectedItem == index)
|
||||
items[index].selectedIcon
|
||||
else
|
||||
items[index].unselectedIcon,
|
||||
contentDescription = item.name
|
||||
)
|
||||
},
|
||||
label = { Text(item.name) },
|
||||
selected = selectedItem == index,
|
||||
onClick = items[index].onClick
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
@file:Suppress("UnusedReceiverParameter", "unused", "NOTHING_TO_INLINE", "KotlinRedundantDiagnosticSuppress")
|
||||
|
||||
package com.pr0gramm3r101.utils
|
||||
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.material3.Typography
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocal
|
||||
import androidx.compose.runtime.ProvidableCompositionLocal
|
||||
import androidx.compose.runtime.compositionLocalOf
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.FocusManager
|
||||
import androidx.compose.ui.platform.ClipboardManager
|
||||
import androidx.compose.ui.platform.LocalClipboardManager
|
||||
import androidx.compose.ui.platform.LocalLayoutDirection
|
||||
import androidx.compose.ui.platform.SoftwareKeyboardController
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.unit.LayoutDirection
|
||||
import org.jetbrains.compose.resources.Font
|
||||
import org.jetbrains.compose.resources.FontResource
|
||||
import tech.annexflow.constraintlayout.compose.ConstrainScope
|
||||
import tech.annexflow.constraintlayout.compose.ConstrainedLayoutReference
|
||||
import tech.annexflow.constraintlayout.compose.ConstraintLayoutBaseScope
|
||||
import tech.annexflow.constraintlayout.compose.HorizontalAnchorable
|
||||
import tech.annexflow.constraintlayout.compose.VerticalAnchorable
|
||||
|
||||
@Composable
|
||||
fun Typography(fontRes: FontResource) = Typography().let {
|
||||
val font = FontFamily(Font(fontRes))
|
||||
it.copy(
|
||||
displayLarge = it.displayLarge.copy(fontFamily = font),
|
||||
displayMedium = it.displayMedium.copy(fontFamily = font),
|
||||
displaySmall = it.displaySmall.copy(fontFamily = font),
|
||||
headlineLarge = it.headlineLarge.copy(fontFamily = font),
|
||||
headlineMedium = it.headlineMedium.copy(fontFamily = font),
|
||||
headlineSmall = it.headlineSmall.copy(fontFamily = font),
|
||||
titleLarge = it.titleLarge.copy(fontFamily = font),
|
||||
titleMedium = it.titleMedium.copy(fontFamily = font),
|
||||
titleSmall = it.titleSmall.copy(fontFamily = font),
|
||||
bodyLarge = it.bodyLarge.copy(fontFamily = font),
|
||||
bodyMedium = it.bodyMedium.copy(fontFamily = font),
|
||||
bodySmall = it.bodySmall.copy(fontFamily = font),
|
||||
labelLarge = it.labelLarge.copy(fontFamily = font),
|
||||
labelMedium = it.labelMedium.copy(fontFamily = font),
|
||||
labelSmall = it.labelSmall.copy(fontFamily = font)
|
||||
)
|
||||
}
|
||||
|
||||
val ConstrainScope.left get() = absoluteLeft
|
||||
val ConstrainScope.right get() = absoluteRight
|
||||
|
||||
val ConstrainedLayoutReference.left get() = absoluteLeft
|
||||
val ConstrainedLayoutReference.right get() = absoluteRight
|
||||
|
||||
inline infix fun HorizontalAnchorable.link(
|
||||
anchor: ConstraintLayoutBaseScope.HorizontalAnchor
|
||||
) = linkTo(anchor)
|
||||
|
||||
inline infix fun VerticalAnchorable.link(
|
||||
anchor: ConstraintLayoutBaseScope.VerticalAnchor
|
||||
) = linkTo(anchor)
|
||||
|
||||
|
||||
expect fun Modifier.clearFocusOnKeyboardDismiss(): Modifier
|
||||
|
||||
operator fun Modifier.plus(other: Modifier) = then(other)
|
||||
|
||||
var ClipboardManager.text
|
||||
get() = getText()
|
||||
set(value) = setText(value!!)
|
||||
|
||||
@Composable
|
||||
expect fun ToggleNavScrimEffect(enabled: Boolean = false)
|
||||
|
||||
interface SupportClipboardManager {
|
||||
suspend fun setText(string: String)
|
||||
suspend fun getText(): String?
|
||||
suspend fun hasText(): Boolean = getText()?.isNotEmpty() == true
|
||||
|
||||
fun setTextListener(listener: (String) -> Unit)
|
||||
}
|
||||
|
||||
val LocalSupportClipboardManager: ProvidableCompositionLocal<SupportClipboardManager> = compositionLocalOf {
|
||||
throw IllegalStateException("This CompositionLocal hasn't been provided.")
|
||||
}
|
||||
|
||||
val supportClipboardManagerImpl
|
||||
@Composable get() = LocalClipboardManager().toSupport()
|
||||
|
||||
inline fun ClipboardManager.toSupport() = object : SupportClipboardManager {
|
||||
private var listener: ((String) -> Unit)? = null
|
||||
|
||||
override suspend fun setText(string: String) {
|
||||
setText(string.toAnnotatedString())
|
||||
listener?.invoke(string)
|
||||
}
|
||||
override suspend fun getText() = this@toSupport.getText()?.toString() ?: ""
|
||||
|
||||
override fun setTextListener(listener: (String) -> Unit) {
|
||||
this.listener = listener
|
||||
}
|
||||
}
|
||||
|
||||
fun String.toAnnotatedString() = AnnotatedString(this)
|
||||
|
||||
@Composable
|
||||
inline operator fun <T> CompositionLocal<T>.invoke() = current
|
||||
|
||||
val LocalSnackbar: ProvidableCompositionLocal<SnackbarHostState> = compositionLocalOf { throw NullPointerException() }
|
||||
|
||||
inline fun resetFocus(
|
||||
keyboardController: SoftwareKeyboardController?,
|
||||
focusManager: FocusManager
|
||||
) {
|
||||
keyboardController?.hide()
|
||||
focusManager.clearFocus(true)
|
||||
}
|
||||
|
||||
val unsupported: Nothing
|
||||
get() = throw UnsupportedOperationException()
|
||||
|
||||
val WindowWidthSizeClass.rawValue: Int get() =
|
||||
when (this) {
|
||||
WindowWidthSizeClass.COMPACT -> 0
|
||||
WindowWidthSizeClass.MEDIUM -> 1
|
||||
WindowWidthSizeClass.EXPANDED -> 2
|
||||
else -> throw IllegalArgumentException("Unsupported WindowWidthSizeClass: $this")
|
||||
}
|
||||
|
||||
val WindowHeightSizeClass.rawValue: Int get() =
|
||||
when (this) {
|
||||
WindowHeightSizeClass.COMPACT -> 0
|
||||
WindowHeightSizeClass.MEDIUM -> 1
|
||||
WindowHeightSizeClass.EXPANDED -> 2
|
||||
else -> throw IllegalArgumentException("Unsupported WindowHeightSizeClass: $this")
|
||||
}
|
||||
|
||||
operator fun WindowWidthSizeClass.compareTo(other: WindowWidthSizeClass) = this.rawValue.compareTo(other.rawValue)
|
||||
operator fun WindowHeightSizeClass.compareTo(other: WindowHeightSizeClass) = this.rawValue.compareTo(other.rawValue)
|
||||
|
||||
val WindowSizeClass.width get() = windowWidthSizeClass
|
||||
val WindowSizeClass.height get() = windowHeightSizeClass
|
||||
|
||||
val WindowAdaptiveInfo.widthSizeClass get() = windowSizeClass.width
|
||||
@Suppress("unused")
|
||||
val WindowAdaptiveInfo.heightSizeClass get() = windowSizeClass.height
|
||||
|
||||
fun String.encodeJSON(): String {
|
||||
val out = StringBuilder()
|
||||
for (i in indices) {
|
||||
when (val c: Char = get(i)) {
|
||||
'"', '\\', '/' -> out.append('\\').append(c)
|
||||
'\t' -> out.append("\\t")
|
||||
'\b' -> out.append("\\b")
|
||||
'\n' -> out.append("\\n")
|
||||
'\r' -> out.append("\\r")
|
||||
else -> if (c.code <= 0x1F) {
|
||||
out.append("\\u${c.code.toString(16).padStart(4, '0')}")
|
||||
} else {
|
||||
out.append(c)
|
||||
}
|
||||
}
|
||||
}
|
||||
return "$out"
|
||||
}
|
||||
|
||||
@Composable
|
||||
operator fun PaddingValues.plus(other: PaddingValues) = PaddingValues(
|
||||
top = calculateTopPadding() + other.calculateTopPadding(),
|
||||
bottom = calculateBottomPadding() + other.calculateTopPadding(),
|
||||
start = when (LocalLayoutDirection()) {
|
||||
LayoutDirection.Ltr -> calculateLeftPadding(LocalLayoutDirection())
|
||||
LayoutDirection.Rtl -> calculateRightPadding(LocalLayoutDirection())
|
||||
},
|
||||
end = when (LocalLayoutDirection()) {
|
||||
LayoutDirection.Ltr -> calculateRightPadding(LocalLayoutDirection())
|
||||
LayoutDirection.Rtl -> calculateLeftPadding(LocalLayoutDirection())
|
||||
}
|
||||
)
|
||||
|
||||
expect val materialYouAvailable: Boolean
|
||||
@@ -0,0 +1,5 @@
|
||||
package com.pr0gramm3r101.utils
|
||||
|
||||
expect object UtilsLibrary {
|
||||
fun init(vararg args: Any)
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package com.pr0gramm3r101.utils.crypto
|
||||
|
||||
/**
|
||||
* Platform-specific base64 encoding/decoding using native implementations
|
||||
*/
|
||||
expect object Base64 {
|
||||
fun encode(bytes: ByteArray): String
|
||||
fun decode(base64: String): ByteArray
|
||||
}
|
||||
|
||||
/**
|
||||
* Extension function to encode ByteArray to base64 string
|
||||
*/
|
||||
fun ByteArray.toBase64(): String = Base64.encode(this)
|
||||
|
||||
/**
|
||||
* Extension function to decode base64 string to ByteArray
|
||||
*/
|
||||
fun String.fromBase64(): ByteArray = Base64.decode(this)
|
||||
@@ -0,0 +1,8 @@
|
||||
package com.pr0gramm3r101.utils.crypto
|
||||
|
||||
/**
|
||||
* Platform-specific HMAC-SHA256 implementation
|
||||
*/
|
||||
expect object Hmac {
|
||||
suspend fun hmacSha256(key: ByteArray, data: ByteArray): ByteArray
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package com.pr0gramm3r101.utils.crypto
|
||||
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
/**
|
||||
* Derives authentication secret from password using HKDF
|
||||
* Matches web client implementation:
|
||||
* - Salt: fromchat.user:${username} encoded as bytes
|
||||
* - Info: "auth-secret" encoded as bytes
|
||||
* - Output: 32 bytes, then base64 encoded
|
||||
*/
|
||||
object PasswordHash {
|
||||
/**
|
||||
* Derives a 32-byte key using HKDF-SHA256
|
||||
* Implementation of RFC 5869 HKDF
|
||||
*/
|
||||
suspend fun hkdfExtractAndExpand(
|
||||
inputKeyMaterial: ByteArray,
|
||||
salt: ByteArray,
|
||||
info: ByteArray,
|
||||
length: Int = 32
|
||||
): ByteArray = withContext(Dispatchers.Default) {
|
||||
// HKDF-Extract: PRK = HMAC-Hash(salt, IKM)
|
||||
val prk = Hmac.hmacSha256(salt, inputKeyMaterial)
|
||||
|
||||
// HKDF-Expand: OKM = HKDF-Expand(PRK, info, L)
|
||||
val hashLen = 32 // SHA-256 output length
|
||||
val n = (length + hashLen - 1) / hashLen // number of blocks
|
||||
|
||||
val okm = ByteArray(length)
|
||||
var offset = 0
|
||||
|
||||
var t = ByteArray(0)
|
||||
for (i in 0 until n) {
|
||||
val tInput = t + info + byteArrayOf((i + 1).toByte())
|
||||
t = Hmac.hmacSha256(prk, tInput)
|
||||
|
||||
val copyLen = minOf(t.size, length - offset)
|
||||
t.copyInto(okm, offset, 0, copyLen)
|
||||
offset += copyLen
|
||||
}
|
||||
|
||||
okm
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Derives authentication secret so the raw password never leaves the client
|
||||
*/
|
||||
suspend fun deriveAuthSecret(username: String, password: String): String {
|
||||
val salt = "fromchat.user:$username".encodeToByteArray()
|
||||
val info = "auth-secret".encodeToByteArray()
|
||||
val passwordBytes = password.encodeToByteArray()
|
||||
|
||||
val derived = PasswordHash.hkdfExtractAndExpand(passwordBytes, salt, info, 32)
|
||||
|
||||
return derived.toBase64()
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package com.pr0gramm3r101.utils.settings
|
||||
|
||||
/**
|
||||
* Async version of Settings API using suspend functions
|
||||
*/
|
||||
interface AsyncSettings {
|
||||
suspend fun putString(key: String, value: String)
|
||||
suspend fun getString(key: String, default: String = ""): String
|
||||
|
||||
suspend fun putInt(key: String, value: Int)
|
||||
suspend fun getInt(key: String, default: Int = 0): Int
|
||||
|
||||
suspend fun putLong(key: String, value: Long)
|
||||
suspend fun getLong(key: String, default: Long = 0L): Long
|
||||
|
||||
suspend fun putFloat(key: String, value: Float)
|
||||
suspend fun getFloat(key: String, default: Float = 0f): Float
|
||||
|
||||
suspend fun putBoolean(key: String, value: Boolean)
|
||||
suspend fun getBoolean(key: String, default: Boolean = false): Boolean
|
||||
|
||||
suspend fun putStringSet(key: String, value: Set<String>)
|
||||
suspend fun getStringSet(key: String, default: Set<String> = emptySet()): Set<String>
|
||||
|
||||
suspend fun putStringList(key: String, value: List<String>)
|
||||
suspend fun getStringList(key: String, default: List<String> = emptyList()): List<String>
|
||||
|
||||
suspend fun remove(key: String)
|
||||
|
||||
suspend fun contains(key: String): Boolean
|
||||
suspend fun clear()
|
||||
}
|
||||
|
||||
/**
|
||||
* Global async settings instance
|
||||
*/
|
||||
expect val asyncSettings: AsyncSettings
|
||||
@@ -0,0 +1,33 @@
|
||||
@file:Suppress("NOTHING_TO_INLINE")
|
||||
|
||||
package com.pr0gramm3r101.utils.settings
|
||||
|
||||
interface Settings {
|
||||
companion object {
|
||||
inline fun get() = settings
|
||||
inline operator fun invoke() = settings
|
||||
}
|
||||
|
||||
fun putString(key: String, value: String)
|
||||
fun getString(key: String, default: String = ""): String
|
||||
|
||||
fun putInt(key: String, value: Int)
|
||||
fun getInt(key: String, default: Int = 0): Int
|
||||
|
||||
fun putLong(key: String, value: Long)
|
||||
fun getLong(key: String, default: Long = 0L): Long
|
||||
|
||||
fun putFloat(key: String, value: Float)
|
||||
fun getFloat(key: String, default: Float = 0f): Float
|
||||
|
||||
fun putBoolean(key: String, value: Boolean)
|
||||
fun getBoolean(key: String, default: Boolean = false): Boolean
|
||||
|
||||
fun putStringSet(key: String, value: Set<String>)
|
||||
fun getStringSet(key: String, default: Set<String> = emptySet()): Set<String>
|
||||
|
||||
fun putStringList(key: String, value: List<String>)
|
||||
fun getStringList(key: String, default: List<String> = emptyList()): List<String>
|
||||
|
||||
fun remove(key: String)
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
package com.pr0gramm3r101.utils.settings
|
||||
|
||||
expect val settings: Settings
|
||||