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,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))
|
||||
}
|
||||
Reference in New Issue
Block a user