Set up multiplatform structure

This commit is contained in:
2025-12-05 23:03:29 +03:00
Unverified
parent 84e07e338d
commit 7696d26fd3
111 changed files with 4586 additions and 280 deletions
+8
View File
@@ -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>
+2 -1
View File
@@ -10,7 +10,8 @@
<option name="modules">
<set>
<option value="$PROJECT_DIR$" />
<option value="$PROJECT_DIR$/app" />
<option value="$PROJECT_DIR$/composeApp" />
<option value="$PROJECT_DIR$/utils" />
</set>
</option>
</GradleProjectSettings>
-91
View File
@@ -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
}
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 982 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.6 KiB

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

Some files were not shown because too many files have changed in this diff Show More