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">
|
<option name="modules">
|
||||||
<set>
|
<set>
|
||||||
<option value="$PROJECT_DIR$" />
|
<option value="$PROJECT_DIR$" />
|
||||||
<option value="$PROJECT_DIR$/app" />
|
<option value="$PROJECT_DIR$/composeApp" />
|
||||||
|
<option value="$PROJECT_DIR$/utils" />
|
||||||
</set>
|
</set>
|
||||||
</option>
|
</option>
|
||||||
</GradleProjectSettings>
|
</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 {
|
plugins {
|
||||||
alias(libs.plugins.android.application) apply false
|
// this is necessary to avoid the plugins to be loaded multiple times
|
||||||
alias(libs.plugins.kotlin.android) apply false
|
// in each subproject's classloader
|
||||||
alias(libs.plugins.kotlin.compose) apply false
|
alias(libs.plugins.androidApplication) apply false
|
||||||
alias(libs.plugins.kotlin.serialization) 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()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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
|
package ru.fromchat.api
|
||||||
|
|
||||||
import android.util.Log
|
|
||||||
import io.ktor.client.HttpClient
|
import io.ktor.client.HttpClient
|
||||||
import io.ktor.client.call.body
|
import io.ktor.client.call.body
|
||||||
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
|
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
|
||||||
import io.ktor.client.plugins.logging.LogLevel
|
import io.ktor.client.plugins.logging.LogLevel
|
||||||
import io.ktor.client.plugins.logging.Logger
|
import io.ktor.client.plugins.logging.Logger
|
||||||
import io.ktor.client.plugins.logging.Logging
|
import io.ktor.client.plugins.logging.Logging
|
||||||
|
import io.ktor.client.plugins.logging.SIMPLE
|
||||||
import io.ktor.client.plugins.websocket.WebSockets
|
import io.ktor.client.plugins.websocket.WebSockets
|
||||||
import io.ktor.client.request.bearerAuth
|
import io.ktor.client.request.bearerAuth
|
||||||
import io.ktor.client.request.delete
|
import io.ktor.client.request.delete
|
||||||
@@ -20,6 +20,15 @@ import io.ktor.serialization.kotlinx.json.json
|
|||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import ru.fromchat.API_HOST
|
import ru.fromchat.API_HOST
|
||||||
import ru.fromchat.utils.failOnError
|
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 {
|
object ApiClient {
|
||||||
val json = Json {
|
val json = Json {
|
||||||
@@ -28,19 +37,14 @@ object ApiClient {
|
|||||||
encodeDefaults = true
|
encodeDefaults = true
|
||||||
}
|
}
|
||||||
|
|
||||||
val http = HttpClient {
|
val http = createPlatformHttpClient {
|
||||||
install(ContentNegotiation) {
|
install(ContentNegotiation) {
|
||||||
json(json)
|
json(json)
|
||||||
}
|
}
|
||||||
|
|
||||||
install(Logging) {
|
install(Logging) {
|
||||||
|
logger = Logger.SIMPLE
|
||||||
level = LogLevel.INFO
|
level = LogLevel.INFO
|
||||||
logger = object : Logger {
|
|
||||||
override fun log(message: String) {
|
|
||||||
// Replace with Logcat if needed
|
|
||||||
println(message)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
install(WebSockets)
|
install(WebSockets)
|
||||||
@@ -58,7 +62,7 @@ object ApiClient {
|
|||||||
http
|
http
|
||||||
.post("$API_HOST/api/login") {
|
.post("$API_HOST/api/login") {
|
||||||
contentType(ContentType.Application.Json)
|
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()
|
.failOnError()
|
||||||
.body<LoginResponse>()
|
.body<LoginResponse>()
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
package ru.fromchat.api
|
package ru.fromchat.api
|
||||||
|
|
||||||
import android.util.Log
|
import ru.fromchat.core.Logger
|
||||||
import io.ktor.client.call.body
|
import io.ktor.client.call.body
|
||||||
import io.ktor.client.plugins.ClientRequestException
|
import io.ktor.client.plugins.ClientRequestException
|
||||||
|
|
||||||
@@ -20,12 +20,12 @@ suspend inline fun <Response> apiRequest(
|
|||||||
"Unexpected error"
|
"Unexpected error"
|
||||||
}
|
}
|
||||||
|
|
||||||
Log.e("API", "API request failed: $message, exception:", e)
|
Logger.e("API", "API request failed: $message", e)
|
||||||
|
|
||||||
onError(message, e)
|
onError(message, e)
|
||||||
return Result.failure(e)
|
return Result.failure(e)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e("API", "API request failed:", e)
|
Logger.e("API", "API request failed: ${e.message}", e)
|
||||||
onError("Unexpected error", e)
|
onError("Unexpected error", e)
|
||||||
return Result.failure(e)
|
return Result.failure(e)
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
package ru.fromchat.api
|
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.DefaultClientWebSocketSession
|
||||||
import io.ktor.client.plugins.websocket.webSocket
|
import io.ktor.client.plugins.websocket.webSocket
|
||||||
import io.ktor.client.request.url
|
import io.ktor.client.request.url
|
||||||
@@ -45,7 +45,7 @@ object WebSocketManager {
|
|||||||
@Volatile private var session: DefaultClientWebSocketSession? = null
|
@Volatile private var session: DefaultClientWebSocketSession? = null
|
||||||
|
|
||||||
fun connect() {
|
fun connect() {
|
||||||
Log.d("WebSocketManager", "Connecting to WebSocket")
|
Logger.d("WebSocketManager", "Connecting to WebSocket")
|
||||||
if (connecting) return
|
if (connecting) return
|
||||||
connecting = true
|
connecting = true
|
||||||
|
|
||||||
@@ -61,27 +61,27 @@ object WebSocketManager {
|
|||||||
session = this
|
session = this
|
||||||
connecting = false
|
connecting = false
|
||||||
|
|
||||||
Log.d("WebSocketManager", "WebSocket connected")
|
Logger.d("WebSocketManager", "WebSocket connected")
|
||||||
for (frame in incoming) {
|
for (frame in incoming) {
|
||||||
val text = (frame as? Frame.Text)?.readText() ?: continue
|
val text = (frame as? Frame.Text)?.readText() ?: continue
|
||||||
Log.d("WebSocketManager", "Received payload: $text")
|
Logger.d("WebSocketManager", "Received payload: $text")
|
||||||
try {
|
try {
|
||||||
val msg = json.decodeFromString<WebSocketMessage>(text)
|
val msg = json.decodeFromString<WebSocketMessage>(text)
|
||||||
globalHandlers.forEach { it(msg) }
|
globalHandlers.forEach { it(msg) }
|
||||||
_messages.emit(msg)
|
_messages.emit(msg)
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
Log.w("WebSocketManager", "Received malformed payload:", e)
|
Logger.w("WebSocketManager", "Received malformed payload: ${e.message}", e)
|
||||||
// ignore malformed
|
// ignore malformed
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
Log.w("WebSocketManager", "An error occurred:", e)
|
Logger.w("WebSocketManager", "An error occurred: ${e.message}", e)
|
||||||
connecting = false
|
connecting = false
|
||||||
session = null
|
session = null
|
||||||
delay(3000)
|
delay(3000)
|
||||||
} finally {
|
} finally {
|
||||||
Log.w("WebSocketManager", "WebSocket disconnected")
|
Logger.w("WebSocketManager", "WebSocket disconnected")
|
||||||
session = null
|
session = null
|
||||||
connecting = false
|
connecting = false
|
||||||
}
|
}
|
||||||
@@ -95,24 +95,24 @@ object WebSocketManager {
|
|||||||
try {
|
try {
|
||||||
session.send(Frame.Text(json.encodeToString(message)))
|
session.send(Frame.Text(json.encodeToString(message)))
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e("WebSocketManager", "Failed to send message", e)
|
Logger.e("WebSocketManager", "Failed to send message: ${e.message}", e)
|
||||||
throw e
|
throw e
|
||||||
}
|
}
|
||||||
} else {
|
} 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")
|
throw IllegalStateException("No active WebSocket session")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(DelicateCoroutinesApi::class)
|
@OptIn(DelicateCoroutinesApi::class)
|
||||||
suspend fun request(message: WebSocketMessage, timeoutMs: Long = 10_000): WebSocketMessage? {
|
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
|
var handler: ((WebSocketMessage) -> Unit)? = null
|
||||||
|
|
||||||
return try {
|
return try {
|
||||||
// Check if we have a valid session before sending
|
// Check if we have a valid session before sending
|
||||||
if (session == null) {
|
if (session == null) {
|
||||||
Log.w("WebSocketManager", "No active WebSocket session")
|
Logger.w("WebSocketManager", "No active WebSocket session")
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -130,10 +130,10 @@ object WebSocketManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (_: TimeoutCancellationException) {
|
} catch (_: TimeoutCancellationException) {
|
||||||
Log.w("WebSocketManager", "Request timed out")
|
Logger.w("WebSocketManager", "Request timed out")
|
||||||
null
|
null
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e("WebSocketManager", "Request failed", e)
|
Logger.e("WebSocketManager", "Request failed: ${e.message}", e)
|
||||||
null
|
null
|
||||||
} finally {
|
} finally {
|
||||||
handler?.let { removeGlobalMessageHandler(it) }
|
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
|
package ru.fromchat.ui
|
||||||
|
|
||||||
import android.os.Build
|
|
||||||
import androidx.compose.foundation.isSystemInDarkTheme
|
import androidx.compose.foundation.isSystemInDarkTheme
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.darkColorScheme
|
import androidx.compose.material3.darkColorScheme
|
||||||
import androidx.compose.material3.dynamicDarkColorScheme
|
|
||||||
import androidx.compose.material3.dynamicLightColorScheme
|
|
||||||
import androidx.compose.material3.lightColorScheme
|
import androidx.compose.material3.lightColorScheme
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.platform.LocalContext
|
|
||||||
|
|
||||||
private val DarkColorScheme = darkColorScheme(
|
private val DarkColorScheme = darkColorScheme(
|
||||||
primary = Color(0xFFDBB9F9),
|
primary = Color(0xFFDBB9F9),
|
||||||
@@ -90,18 +86,10 @@ private val LightColorScheme = lightColorScheme(
|
|||||||
@Composable
|
@Composable
|
||||||
fun FromChatTheme(
|
fun FromChatTheme(
|
||||||
darkTheme: Boolean = isSystemInDarkTheme(),
|
darkTheme: Boolean = isSystemInDarkTheme(),
|
||||||
dynamicColor: Boolean = true,
|
dynamicColor: Boolean = false, // Disabled for multiplatform compatibility
|
||||||
content: @Composable () -> Unit
|
content: @Composable () -> Unit
|
||||||
) {
|
) {
|
||||||
val colorScheme = when {
|
val colorScheme = if (darkTheme) DarkColorScheme else LightColorScheme
|
||||||
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
|
|
||||||
val context = LocalContext.current
|
|
||||||
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
|
|
||||||
}
|
|
||||||
|
|
||||||
darkTheme -> DarkColorScheme
|
|
||||||
else -> LightColorScheme
|
|
||||||
}
|
|
||||||
|
|
||||||
MaterialTheme(
|
MaterialTheme(
|
||||||
colorScheme = colorScheme,
|
colorScheme = colorScheme,
|
||||||
@@ -33,7 +33,7 @@ import ru.fromchat.api.ApiClient
|
|||||||
import ru.fromchat.api.LoginRequest
|
import ru.fromchat.api.LoginRequest
|
||||||
import ru.fromchat.api.apiRequest
|
import ru.fromchat.api.apiRequest
|
||||||
import ru.fromchat.ui.RowHeader
|
import ru.fromchat.ui.RowHeader
|
||||||
import ru.fromchat.utils.HashUtils
|
import com.pr0gramm3r101.utils.crypto.deriveAuthSecret
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun LoginScreen(
|
fun LoginScreen(
|
||||||
@@ -98,7 +98,7 @@ fun LoginScreen(
|
|||||||
|
|
||||||
// Derive auth secret before sending (matches frontend implementation)
|
// Derive auth secret before sending (matches frontend implementation)
|
||||||
scope.launch {
|
scope.launch {
|
||||||
val derived = HashUtils.deriveAuthSecret(username.trim(), password.trim())
|
val derived = deriveAuthSecret(username.trim(), password.trim())
|
||||||
|
|
||||||
apiRequest(
|
apiRequest(
|
||||||
onError = { message, _ ->
|
onError = { message, _ ->
|
||||||
@@ -37,7 +37,7 @@ import ru.fromchat.api.RegisterRequest
|
|||||||
import ru.fromchat.api.apiRequest
|
import ru.fromchat.api.apiRequest
|
||||||
import ru.fromchat.ui.LocalNavController
|
import ru.fromchat.ui.LocalNavController
|
||||||
import ru.fromchat.ui.RowHeader
|
import ru.fromchat.ui.RowHeader
|
||||||
import ru.fromchat.utils.HashUtils
|
import com.pr0gramm3r101.utils.crypto.deriveAuthSecret
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun RegisterScreen(
|
fun RegisterScreen(
|
||||||
@@ -148,7 +148,7 @@ fun RegisterScreen(
|
|||||||
|
|
||||||
// Derive auth secret before sending (matches frontend implementation)
|
// Derive auth secret before sending (matches frontend implementation)
|
||||||
scope.launch {
|
scope.launch {
|
||||||
val derived = HashUtils.deriveAuthSecret(username.trim(), password)
|
val derived = deriveAuthSecret(username.trim(), password)
|
||||||
|
|
||||||
apiRequest(
|
apiRequest(
|
||||||
onError = { message, _ ->
|
onError = { message, _ ->
|
||||||
@@ -7,11 +7,10 @@ import io.ktor.client.plugins.DefaultRequest
|
|||||||
import io.ktor.client.plugins.ResponseException
|
import io.ktor.client.plugins.ResponseException
|
||||||
import io.ktor.client.plugins.ServerResponseException
|
import io.ktor.client.plugins.ServerResponseException
|
||||||
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
|
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.LogLevel
|
||||||
import io.ktor.client.plugins.logging.Logger
|
import io.ktor.client.plugins.logging.Logger
|
||||||
import io.ktor.client.plugins.logging.Logging
|
import io.ktor.client.plugins.logging.Logging
|
||||||
import io.ktor.client.plugins.logging.LoggingConfig
|
import io.ktor.client.plugins.logging.SIMPLE
|
||||||
import io.ktor.client.statement.HttpResponse
|
import io.ktor.client.statement.HttpResponse
|
||||||
import io.ktor.client.statement.bodyAsText
|
import io.ktor.client.statement.bodyAsText
|
||||||
import io.ktor.serialization.Configuration
|
import io.ktor.serialization.Configuration
|
||||||
@@ -56,10 +55,10 @@ inline fun HttpClientConfig<*>.defaultRequest(
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Installs [Logging] in a Ktor client config.
|
* 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(
|
inline fun HttpClientConfig<*>.logging(
|
||||||
crossinline settings: LoggingConfig.() -> Unit
|
crossinline settings: Logging.Config.() -> Unit
|
||||||
) = install(Logging) {
|
) = install(Logging) {
|
||||||
settings()
|
settings()
|
||||||
}
|
}
|
||||||
@@ -68,7 +67,7 @@ inline fun HttpClientConfig<*>.logging(
|
|||||||
* Installs [Logging] in a Ktor client config with default settings (all logs).
|
* Installs [Logging] in a Ktor client config with default settings (all logs).
|
||||||
*/
|
*/
|
||||||
inline fun HttpClientConfig<*>.logging() = logging {
|
inline fun HttpClientConfig<*>.logging() = logging {
|
||||||
logger = Logger.DEFAULT
|
logger = Logger.SIMPLE
|
||||||
level = LogLevel.ALL
|
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]
|
[versions]
|
||||||
agp = "8.13.1"
|
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"
|
kotlin = "2.2.21"
|
||||||
coreKtx = "1.17.0"
|
adaptiveAndroid = "1.2.0"
|
||||||
kotlinxDatetime = "0.7.1"
|
kotlinStdlib = "2.2.21"
|
||||||
lifecycleRuntimeKtx = "2.10.0"
|
biometric = "1.4.0-alpha04"
|
||||||
activityCompose = "1.12.0"
|
gson = "2.13.2"
|
||||||
composeBom = "2025.11.01"
|
kotlinxIoBytestring = "0.8.2"
|
||||||
materialIconsExtended = "1.7.8"
|
kotlinxCoroutinesCore = "1.10.2"
|
||||||
navigationCompose = "2.9.6"
|
kotlinxIoCore = "0.8.2"
|
||||||
ktor = "3.3.3"
|
|
||||||
kotlinxSerializationJson = "1.9.0"
|
kotlinxSerializationJson = "1.9.0"
|
||||||
compose = "1.9.5"
|
material = "1.13.0"
|
||||||
material3 = "1.5.0-alpha09"
|
activityKtx = "1.12.0"
|
||||||
haze = "1.7.1"
|
navigationCompose = "2.9.1"
|
||||||
|
datastore = "1.2.0"
|
||||||
|
ktor = "2.3.12"
|
||||||
|
slf4j = "1.7.36"
|
||||||
|
kotlinxDatetime = "0.7.1"
|
||||||
|
|
||||||
[libraries]
|
[libraries]
|
||||||
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
|
androidx-core-splashscreen = { module = "androidx.core:core-splashscreen", version.ref = "coreSplashscreen" }
|
||||||
androidx-material-icons-extended = { module = "androidx.compose.material:material-icons-extended", version.ref = "materialIconsExtended" }
|
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "androidx-core-ktx" }
|
||||||
androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" }
|
androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "androidx-appcompat" }
|
||||||
androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" }
|
androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "androidx-activityCompose" }
|
||||||
androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" }
|
androidx-lifecycle-viewmodel = { group = "org.jetbrains.androidx.lifecycle", name = "lifecycle-viewmodel", version.ref = "androidx-lifecycle" }
|
||||||
androidx-ui = { group = "androidx.compose.ui", name = "ui", version.ref = "compose" }
|
androidx-lifecycle-runtime-compose = { group = "org.jetbrains.androidx.lifecycle", name = "lifecycle-runtime-compose", version.ref = "androidx-lifecycle" }
|
||||||
androidx-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics", version.ref = "compose" }
|
constraintlayout = { module = "tech.annexflow.compose:constraintlayout-compose-multiplatform", version.ref = "constraintlayout" }
|
||||||
androidx-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling", version.ref = "compose" }
|
jetbrains-kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinxCoroutinesCore" }
|
||||||
androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview", version.ref = "compose" }
|
jetbrains-kotlinx-io-bytestring = { module = "org.jetbrains.kotlinx:kotlinx-io-bytestring", version.ref = "kotlinxIoBytestring" }
|
||||||
androidx-material3 = { group = "androidx.compose.material3", name = "material3", version.ref = "material3" }
|
androidx-adaptive-android = { group = "androidx.compose.material3.adaptive", name = "adaptive-android", version.ref = "adaptiveAndroid" }
|
||||||
androidx-navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version.ref = "navigationCompose" }
|
kotlin-stdlib = { group = "org.jetbrains.kotlin", name = "kotlin-stdlib", version.ref = "kotlinStdlib" }
|
||||||
kotlinx-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "kotlinxDatetime" }
|
biometric = { module = "androidx.biometric:biometric", version.ref = "biometric" }
|
||||||
ktor-client-core = { group = "io.ktor", name = "ktor-client-core", version.ref = "ktor" }
|
gson = { module = "com.google.code.gson:gson", version.ref = "gson" }
|
||||||
ktor-client-okhttp = { group = "io.ktor", name = "ktor-client-okhttp", version.ref = "ktor" }
|
kotlinx-io-core = { module = "org.jetbrains.kotlinx:kotlinx-io-core", version.ref = "kotlinxIoCore" }
|
||||||
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" }
|
|
||||||
kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinxSerializationJson" }
|
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" }
|
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]
|
[plugins]
|
||||||
android-application = { id = "com.android.application", version.ref = "agp" }
|
androidApplication = { id = "com.android.application", version.ref = "agp" }
|
||||||
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
|
androidLibrary = { id = "com.android.library", version.ref = "agp" }
|
||||||
kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
|
composeMultiplatform = { id = "org.jetbrains.compose", version.ref = "compose-multiplatform" }
|
||||||
kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
|
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 {
|
pluginManagement {
|
||||||
repositories {
|
repositories {
|
||||||
google {
|
google {
|
||||||
content {
|
mavenContent {
|
||||||
includeGroupByRegex("com\\.android.*")
|
includeGroupAndSubgroups("androidx")
|
||||||
includeGroupByRegex("com\\.google.*")
|
includeGroupAndSubgroups("com.android")
|
||||||
includeGroupByRegex("androidx.*")
|
includeGroupAndSubgroups("com.google")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
gradlePluginPortal()
|
gradlePluginPortal()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencyResolutionManagement {
|
dependencyResolutionManagement {
|
||||||
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
|
|
||||||
repositories {
|
repositories {
|
||||||
google()
|
google {
|
||||||
|
mavenContent {
|
||||||
|
includeGroupAndSubgroups("androidx")
|
||||||
|
includeGroupAndSubgroups("com.android")
|
||||||
|
includeGroupAndSubgroups("com.google")
|
||||||
|
}
|
||||||
|
}
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
rootProject.name = "FromChat"
|
include(":composeApp", ":utils")
|
||||||
include(":app")
|
|
||||||
|
|
||||||
@@ -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
|
||||||