mirror of
https://github.com/fromchat-messenger/app.git
synced 2026-09-22 19:15:05 +03:00
Refactor code to support AGP 9.0
This commit is contained in:
@@ -0,0 +1 @@
|
||||
/build
|
||||
@@ -0,0 +1,64 @@
|
||||
import com.android.build.api.dsl.androidLibrary
|
||||
|
||||
plugins {
|
||||
alias(libs.plugins.kotlin.multiplatform)
|
||||
alias(libs.plugins.compose.multiplatform)
|
||||
alias(libs.plugins.compose.compiler)
|
||||
alias(libs.plugins.android.kotlin.multiplatform.library)
|
||||
}
|
||||
|
||||
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 = "shared"
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
implementation(libs.navigation.compose)
|
||||
}
|
||||
|
||||
androidMain.dependencies {
|
||||
implementation(libs.androidx.adaptive.android)
|
||||
implementation(libs.datastore.preferences)
|
||||
implementation(libs.datastore.core)
|
||||
implementation(libs.biometric)
|
||||
implementation(libs.androidx.appcompat)
|
||||
implementation(libs.material)
|
||||
}
|
||||
|
||||
iosMain.dependencies {
|
||||
// nothing
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
compose.resources {
|
||||
publicResClass = true
|
||||
packageOfResClass = "com.pr0gramm3r101.shared"
|
||||
generateResClass = auto
|
||||
}
|
||||
+42
@@ -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.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", "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,17 @@
|
||||
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>
|
||||
val context get() = _context.get()!!
|
||||
|
||||
actual fun init(vararg args: Any) {
|
||||
if (!init) {
|
||||
_context = WeakReference(args[0] as Context)
|
||||
init = true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
+114
@@ -0,0 +1,114 @@
|
||||
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 kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.runBlocking
|
||||
|
||||
class AndroidSettings(): 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
|
||||
}
|
||||
}
|
||||
+128
@@ -0,0 +1,128 @@
|
||||
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 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()
|
||||
+17
@@ -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
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
package com.pr0gramm3r101.utils.settings
|
||||
|
||||
actual val settings: Settings get() = AndroidSettings()
|
||||
@@ -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 com.pr0gramm3r101.shared.Res
|
||||
import com.pr0gramm3r101.shared.license
|
||||
import com.pr0gramm3r101.shared.siren_filled
|
||||
import com.pr0gramm3r101.shared.siren_outlined
|
||||
import org.jetbrains.compose.resources.vectorResource
|
||||
|
||||
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,38 @@
|
||||
package com.pr0gramm3r101.utils
|
||||
|
||||
import androidx.navigation.NavController
|
||||
|
||||
/**
|
||||
* Navigates to the specified route and clears the entire back stack.
|
||||
* This ensures the destination becomes the new root of the navigation stack.
|
||||
*
|
||||
* @param route The destination route to navigate to
|
||||
* @param launchSingleTop If true, prevents multiple instances of the same destination
|
||||
*/
|
||||
fun NavController.navigateAndWipeBackStack(route: String, launchSingleTop: Boolean = true) {
|
||||
// First, pop all previous entries (keeping current screen for now)
|
||||
while (previousBackStackEntry != null) {
|
||||
if (!popBackStack()) {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Get current route to pop it with the navigation
|
||||
val currentRoute = currentBackStackEntry?.destination?.route
|
||||
|
||||
// Navigate to the new route, removing the current screen as well
|
||||
if (currentRoute != null && currentRoute != route) {
|
||||
navigate(route) {
|
||||
popUpTo(currentRoute) {
|
||||
inclusive = true
|
||||
}
|
||||
this.launchSingleTop = launchSingleTop
|
||||
}
|
||||
} else {
|
||||
// Fallback: just navigate if current route is same or null
|
||||
navigate(route) {
|
||||
this.launchSingleTop = launchSingleTop
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,184 @@
|
||||
@file:Suppress("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
|
||||
|
||||
@Suppress("ComposableNaming")
|
||||
@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
|
||||
@@ -0,0 +1,29 @@
|
||||
package com.pr0gramm3r101.utils.storage
|
||||
|
||||
/**
|
||||
* Authentication token storage
|
||||
*/
|
||||
object AuthStorage {
|
||||
private val storage = NamespacedStorage("auth")
|
||||
|
||||
suspend fun getToken(): String? {
|
||||
val token = storage.getString("token")
|
||||
return token.ifEmpty { null }
|
||||
}
|
||||
|
||||
suspend fun setToken(token: String?) {
|
||||
if (token != null) {
|
||||
storage.putString("token", token)
|
||||
} else {
|
||||
storage.remove("token")
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun clearToken() {
|
||||
storage.remove("token")
|
||||
}
|
||||
|
||||
suspend fun putString(key: String, value: String) = storage.putString(key, value)
|
||||
suspend fun getString(key: String, default: String = ""): String = storage.getString(key, default)
|
||||
suspend fun remove(key: String) = storage.remove(key)
|
||||
}
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
package com.pr0gramm3r101.utils.storage
|
||||
|
||||
/**
|
||||
* Server configuration data
|
||||
*/
|
||||
data class ServerConfigData(
|
||||
val serverUrl: String,
|
||||
val httpsEnabled: Boolean
|
||||
)
|
||||
|
||||
/**
|
||||
* Server configuration storage
|
||||
*/
|
||||
object ServerConfigStorage {
|
||||
private val storage = NamespacedStorage("server_config")
|
||||
|
||||
private const val SERVER_URL_KEY = "server_url"
|
||||
private const val HTTPS_ENABLED_KEY = "https_enabled"
|
||||
|
||||
suspend fun getServerUrl(): String? {
|
||||
val url = storage.getString(SERVER_URL_KEY)
|
||||
return url.ifEmpty { null }
|
||||
}
|
||||
|
||||
suspend fun setServerUrl(url: String) {
|
||||
storage.putString(SERVER_URL_KEY, url)
|
||||
}
|
||||
|
||||
suspend fun getHttpsEnabled(): Boolean? {
|
||||
return if (storage.contains(HTTPS_ENABLED_KEY)) {
|
||||
storage.getBoolean(HTTPS_ENABLED_KEY, true)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun setHttpsEnabled(enabled: Boolean) {
|
||||
storage.putBoolean(HTTPS_ENABLED_KEY, enabled)
|
||||
}
|
||||
|
||||
suspend fun hasConfiguration(): Boolean {
|
||||
return getServerUrl() != null
|
||||
}
|
||||
|
||||
suspend fun getConfig(): ServerConfigData {
|
||||
val url = getServerUrl() ?: throw IllegalStateException("Server URL not found in storage")
|
||||
val https = getHttpsEnabled() ?: true
|
||||
return ServerConfigData(url, https)
|
||||
}
|
||||
|
||||
suspend fun saveConfig(config: ServerConfigData) {
|
||||
setServerUrl(config.serverUrl)
|
||||
setHttpsEnabled(config.httpsEnabled)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package com.pr0gramm3r101.utils.storage
|
||||
|
||||
import com.pr0gramm3r101.utils.settings.AsyncSettings
|
||||
import com.pr0gramm3r101.utils.settings.asyncSettings
|
||||
|
||||
/**
|
||||
* Generic key-value storage using AsyncSettings
|
||||
*/
|
||||
class Storage(private val settings: AsyncSettings = asyncSettings) {
|
||||
suspend fun putString(key: String, value: String) = settings.putString(key, value)
|
||||
suspend fun getString(key: String, default: String = ""): String = settings.getString(key, default)
|
||||
|
||||
suspend fun putInt(key: String, value: Int) = settings.putInt(key, value)
|
||||
suspend fun getInt(key: String, default: Int = 0): Int = settings.getInt(key, default)
|
||||
|
||||
suspend fun putLong(key: String, value: Long) = settings.putLong(key, value)
|
||||
suspend fun getLong(key: String, default: Long = 0L): Long = settings.getLong(key, default)
|
||||
|
||||
suspend fun putFloat(key: String, value: Float) = settings.putFloat(key, value)
|
||||
suspend fun getFloat(key: String, default: Float = 0f): Float = settings.getFloat(key, default)
|
||||
|
||||
suspend fun putBoolean(key: String, value: Boolean) = settings.putBoolean(key, value)
|
||||
suspend fun getBoolean(key: String, default: Boolean = false): Boolean = settings.getBoolean(key, default)
|
||||
|
||||
suspend fun putStringSet(key: String, value: Set<String>) = settings.putStringSet(key, value)
|
||||
suspend fun getStringSet(key: String, default: Set<String> = emptySet()): Set<String> = settings.getStringSet(key, default)
|
||||
|
||||
suspend fun putStringList(key: String, value: List<String>) = settings.putStringList(key, value)
|
||||
suspend fun getStringList(key: String, default: List<String> = emptyList()): List<String> = settings.getStringList(key, default)
|
||||
|
||||
suspend fun remove(key: String) = settings.remove(key)
|
||||
suspend fun contains(key: String): Boolean = settings.contains(key)
|
||||
suspend fun clear() = settings.clear()
|
||||
}
|
||||
|
||||
/**
|
||||
* Namespaced storage for specific features
|
||||
*/
|
||||
class NamespacedStorage(private val prefix: String, private val storage: Storage = Storage()) {
|
||||
private fun key(name: String) = "$prefix:$name"
|
||||
|
||||
suspend fun putString(name: String, value: String) = storage.putString(key(name), value)
|
||||
suspend fun getString(name: String, default: String = ""): String = storage.getString(key(name), default)
|
||||
|
||||
suspend fun putInt(name: String, value: Int) = storage.putInt(key(name), value)
|
||||
suspend fun getInt(name: String, default: Int = 0): Int = storage.getInt(key(name), default)
|
||||
|
||||
suspend fun putLong(name: String, value: Long) = storage.putLong(key(name), value)
|
||||
suspend fun getLong(name: String, default: Long = 0L): Long = storage.getLong(key(name), default)
|
||||
|
||||
suspend fun putFloat(name: String, value: Float) = storage.putFloat(key(name), value)
|
||||
suspend fun getFloat(name: String, default: Float = 0f): Float = storage.getFloat(key(name), default)
|
||||
|
||||
suspend fun putBoolean(name: String, value: Boolean) = storage.putBoolean(key(name), value)
|
||||
suspend fun getBoolean(name: String, default: Boolean = false): Boolean = storage.getBoolean(key(name), default)
|
||||
|
||||
suspend fun remove(name: String) = storage.remove(key(name))
|
||||
suspend fun contains(name: String): Boolean = storage.contains(key(name))
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.pr0gramm3r101.utils
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.ExperimentalComposeUiApi
|
||||
import androidx.compose.ui.platform.LocalWindowInfo
|
||||
|
||||
@OptIn(ExperimentalComposeUiApi::class)
|
||||
@Composable
|
||||
actual inline fun currentWindowSize() = LocalWindowInfo.current.containerSize
|
||||
@@ -0,0 +1,25 @@
|
||||
@file:OptIn(ExperimentalNativeApi::class)
|
||||
@file:Suppress("NOTHING_TO_INLINE")
|
||||
|
||||
package com.pr0gramm3r101.utils
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import platform.posix._OSSwapInt16
|
||||
import platform.posix._OSSwapInt32
|
||||
import platform.posix._OSSwapInt64
|
||||
import kotlin.experimental.ExperimentalNativeApi
|
||||
import kotlin.native.Platform.isLittleEndian
|
||||
|
||||
actual fun Modifier.clearFocusOnKeyboardDismiss() = this
|
||||
|
||||
@Composable
|
||||
actual fun ToggleNavScrimEffect(enabled: Boolean) {}
|
||||
actual val materialYouAvailable get() = false
|
||||
|
||||
inline fun htons(short: UShort) = if (isLittleEndian) _OSSwapInt16(short) else short
|
||||
inline fun htonl(data: UInt) = if (isLittleEndian) _OSSwapInt32(data) else data
|
||||
inline fun htonll(data: ULong) = if (isLittleEndian) _OSSwapInt64(data) else data
|
||||
inline fun ntohs(data: UShort) = if (isLittleEndian) _OSSwapInt16(data) else data
|
||||
inline fun ntohl(data: UInt) = if (isLittleEndian) _OSSwapInt32(data) else data
|
||||
inline fun ntohll(data: ULong) = if (isLittleEndian) _OSSwapInt64(data) else data
|
||||
@@ -0,0 +1,5 @@
|
||||
package com.pr0gramm3r101.utils
|
||||
|
||||
actual object UtilsLibrary {
|
||||
actual fun init(vararg args: Any) {}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package com.pr0gramm3r101.utils.crypto
|
||||
|
||||
import kotlinx.cinterop.BetaInteropApi
|
||||
import kotlinx.cinterop.ByteVar
|
||||
import kotlinx.cinterop.ExperimentalForeignApi
|
||||
import kotlinx.cinterop.addressOf
|
||||
import kotlinx.cinterop.get
|
||||
import kotlinx.cinterop.reinterpret
|
||||
import kotlinx.cinterop.usePinned
|
||||
import platform.Foundation.NSData
|
||||
import platform.Foundation.base64EncodedStringWithOptions
|
||||
import platform.Foundation.create
|
||||
|
||||
actual object Base64 {
|
||||
@OptIn(ExperimentalForeignApi::class, BetaInteropApi::class)
|
||||
actual fun encode(bytes: ByteArray): String {
|
||||
return bytes.usePinned { pinned ->
|
||||
val nsData = NSData.create(bytes = pinned.addressOf(0), length = bytes.size.toULong())
|
||||
nsData.base64EncodedStringWithOptions(0u)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(BetaInteropApi::class, ExperimentalForeignApi::class)
|
||||
actual fun decode(base64: String): ByteArray {
|
||||
val nsData = NSData.create(base64EncodedString = base64, options = 0u) ?: return ByteArray(0)
|
||||
val length = nsData.length.toInt()
|
||||
val bytesPtr = nsData.bytes ?: return ByteArray(0)
|
||||
val bytePtr = bytesPtr.reinterpret<ByteVar>()
|
||||
return ByteArray(length) { index ->
|
||||
bytePtr[index]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package com.pr0gramm3r101.utils.crypto
|
||||
|
||||
import kotlinx.cinterop.ExperimentalForeignApi
|
||||
import kotlinx.cinterop.addressOf
|
||||
import kotlinx.cinterop.usePinned
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import platform.CoreCrypto.CCHmac
|
||||
import platform.CoreCrypto.kCCHmacAlgSHA256
|
||||
|
||||
actual object Hmac {
|
||||
@OptIn(ExperimentalForeignApi::class)
|
||||
actual suspend fun hmacSha256(key: ByteArray, data: ByteArray): ByteArray = withContext(Dispatchers.Default) {
|
||||
val result = ByteArray(32)
|
||||
|
||||
key.usePinned { keyPinned ->
|
||||
data.usePinned { dataPinned ->
|
||||
result.usePinned { resultPinned ->
|
||||
CCHmac(
|
||||
algorithm = kCCHmacAlgSHA256,
|
||||
key = keyPinned.addressOf(0),
|
||||
keyLength = key.size.toULong(),
|
||||
data = dataPinned.addressOf(0),
|
||||
dataLength = data.size.toULong(),
|
||||
macOut = resultPinned.addressOf(0)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
package com.pr0gramm3r101.utils.settings
|
||||
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import platform.Foundation.NSBundle
|
||||
import platform.Foundation.NSUserDefaults
|
||||
|
||||
class IosAsyncSettings : AsyncSettings {
|
||||
private val defaults: NSUserDefaults = NSUserDefaults.standardUserDefaults
|
||||
|
||||
override suspend fun putString(key: String, value: String): Unit = withContext(Dispatchers.Default) {
|
||||
defaults.setObject(value, key)
|
||||
defaults.synchronize()
|
||||
}
|
||||
|
||||
override suspend fun getString(key: String, default: String): String = withContext(Dispatchers.Default) {
|
||||
defaults.stringForKey(key) ?: default
|
||||
}
|
||||
|
||||
override suspend fun putInt(key: String, value: Int): Unit = withContext(Dispatchers.Default) {
|
||||
defaults.setInteger(value.toLong(), key)
|
||||
defaults.synchronize()
|
||||
}
|
||||
|
||||
override suspend fun getInt(key: String, default: Int): Int = withContext(Dispatchers.Default) {
|
||||
defaults.integerForKey(key).toInt()
|
||||
}
|
||||
|
||||
override suspend fun putLong(key: String, value: Long): Unit = withContext(Dispatchers.Default) {
|
||||
defaults.setObject(value, key)
|
||||
defaults.synchronize()
|
||||
}
|
||||
|
||||
override suspend fun getLong(key: String, default: Long): Long = withContext(Dispatchers.Default) {
|
||||
(defaults.objectForKey(key) as? platform.Foundation.NSNumber)?.longValue ?: default
|
||||
}
|
||||
|
||||
override suspend fun putFloat(key: String, value: Float): Unit = withContext(Dispatchers.Default) {
|
||||
defaults.setFloat(value, key)
|
||||
defaults.synchronize()
|
||||
}
|
||||
|
||||
override suspend fun getFloat(key: String, default: Float): Float = withContext(Dispatchers.Default) {
|
||||
defaults.floatForKey(key)
|
||||
}
|
||||
|
||||
override suspend fun putBoolean(key: String, value: Boolean): Unit = withContext(Dispatchers.Default) {
|
||||
defaults.setBool(value, key)
|
||||
defaults.synchronize()
|
||||
}
|
||||
|
||||
override suspend fun getBoolean(key: String, default: Boolean): Boolean = withContext(Dispatchers.Default) {
|
||||
if (defaults.objectForKey(key) != null) {
|
||||
defaults.boolForKey(key)
|
||||
} else {
|
||||
default
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun putStringSet(key: String, value: Set<String>): Unit = withContext(Dispatchers.Default) {
|
||||
defaults.setObject(value.toList(), key)
|
||||
defaults.synchronize()
|
||||
}
|
||||
|
||||
override suspend fun getStringSet(key: String, default: Set<String>): Set<String> = withContext(Dispatchers.Default) {
|
||||
val list = defaults.objectForKey(key) as? List<*> ?: return@withContext default
|
||||
list.filterIsInstance<String>().toSet()
|
||||
}
|
||||
|
||||
override suspend fun putStringList(key: String, value: List<String>): Unit = withContext(Dispatchers.Default) {
|
||||
defaults.setObject(value, key)
|
||||
defaults.synchronize()
|
||||
}
|
||||
|
||||
override suspend fun getStringList(key: String, default: List<String>): List<String> = withContext(Dispatchers.Default) {
|
||||
val list = defaults.objectForKey(key) as? List<*> ?: return@withContext default
|
||||
list.filterIsInstance<String>()
|
||||
}
|
||||
|
||||
override suspend fun remove(key: String): Unit = withContext(Dispatchers.Default) {
|
||||
defaults.removeObjectForKey(key)
|
||||
defaults.synchronize()
|
||||
}
|
||||
|
||||
override suspend fun contains(key: String): Boolean = withContext(Dispatchers.Default) {
|
||||
defaults.objectForKey(key) != null
|
||||
}
|
||||
|
||||
override suspend fun clear(): Unit = withContext(Dispatchers.Default) {
|
||||
val bundleId = NSBundle.mainBundle.bundleIdentifier ?: ""
|
||||
val domain = defaults.persistentDomainForName(bundleId)
|
||||
domain?.keys?.forEach { key ->
|
||||
defaults.removeObjectForKey(key as String)
|
||||
}
|
||||
defaults.synchronize()
|
||||
}
|
||||
}
|
||||
|
||||
actual val asyncSettings: AsyncSettings = IosAsyncSettings()
|
||||
@@ -0,0 +1,70 @@
|
||||
package com.pr0gramm3r101.utils.settings
|
||||
|
||||
import platform.Foundation.NSUserDefaults
|
||||
|
||||
// TODO fix
|
||||
class IosSettings : Settings {
|
||||
private val defaults: NSUserDefaults = NSUserDefaults.standardUserDefaults
|
||||
|
||||
override fun putString(key: String, value: String) {
|
||||
defaults.setObject(value, key)
|
||||
}
|
||||
|
||||
override fun getString(key: String, default: String): String {
|
||||
return defaults.stringForKey(key) ?: default
|
||||
}
|
||||
|
||||
override fun putInt(key: String, value: Int) {
|
||||
defaults.setInteger(value.toLong(), key)
|
||||
}
|
||||
|
||||
override fun getInt(key: String, default: Int): Int {
|
||||
return defaults.integerForKey(key).toInt()
|
||||
}
|
||||
|
||||
override fun putLong(key: String, value: Long) {
|
||||
defaults.setObject(value, key)
|
||||
}
|
||||
|
||||
override fun getLong(key: String, default: Long): Long {
|
||||
return defaults.objectForKey(key) as? Long ?: default
|
||||
}
|
||||
|
||||
override fun putFloat(key: String, value: Float) {
|
||||
defaults.setFloat(value, key)
|
||||
}
|
||||
|
||||
override fun getFloat(key: String, default: Float): Float {
|
||||
return defaults.floatForKey(key)
|
||||
}
|
||||
|
||||
override fun putBoolean(key: String, value: Boolean) {
|
||||
defaults.setBool(value, key)
|
||||
}
|
||||
|
||||
override fun getBoolean(key: String, default: Boolean): Boolean {
|
||||
return defaults.boolForKey(key)
|
||||
}
|
||||
|
||||
override fun putStringSet(key: String, value: Set<String>) {
|
||||
defaults.setObject(value.toList(), key)
|
||||
}
|
||||
|
||||
override fun getStringSet(key: String, default: Set<String>): Set<String> {
|
||||
val list = defaults.objectForKey(key) as? List<*> ?: return default
|
||||
return list.filterIsInstance<String>().toSet()
|
||||
}
|
||||
|
||||
override fun putStringList(key: String, value: List<String>) {
|
||||
defaults.setObject(value, key)
|
||||
}
|
||||
|
||||
override fun getStringList(key: String, default: List<String>): List<String> {
|
||||
val list = defaults.objectForKey(key) as? List<*> ?: return default
|
||||
return list.filterIsInstance<String>()
|
||||
}
|
||||
|
||||
override fun remove(key: String) {
|
||||
defaults.removeObjectForKey(key)
|
||||
}
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
package com.pr0gramm3r101.utils.settings
|
||||
|
||||
actual val settings: Settings get() = IosSettings()
|
||||
Reference in New Issue
Block a user