Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions AppSnackBar-UiState/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@ Integration module that bridges the AppSnackBar and UiState modules, providing a

```gradle.kts
// Requires both base modules
implementation("com.github.appoly.AppolyDroid-Toolbox:UiState:1.6.2")
implementation("com.github.appoly.AppolyDroid-Toolbox:AppSnackBar:1.6.2")
implementation("com.github.appoly.AppolyDroid-Toolbox:AppSnackBar-UiState:1.6.2")
implementation("com.github.appoly.AppolyDroid-Toolbox:UiState:1.6.3")
implementation("com.github.appoly.AppolyDroid-Toolbox:AppSnackBar:1.6.3")
implementation("com.github.appoly.AppolyDroid-Toolbox:AppSnackBar-UiState:1.6.3")
```

## Usage
Expand Down
2 changes: 1 addition & 1 deletion AppSnackBar/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ A customizable Jetpack Compose Snackbar implementation with support for differen
## Installation

```gradle.kts
implementation("com.github.appoly.AppolyDroid-Toolbox:AppSnackBar:1.6.2")
implementation("com.github.appoly.AppolyDroid-Toolbox:AppSnackBar:1.6.3")
```

## Usage
Expand Down
4 changes: 2 additions & 2 deletions BaseRepo-AppolyJson/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ Appoly's JSON format.

```gradle.kts
// Requires the base BaseRepo module
implementation("com.github.appoly.AppolyDroid-Toolbox:BaseRepo:1.6.2")
implementation("com.github.appoly.AppolyDroid-Toolbox:BaseRepo-AppolyJson:1.6.2")
implementation("com.github.appoly.AppolyDroid-Toolbox:BaseRepo:1.6.3")
implementation("com.github.appoly.AppolyDroid-Toolbox:BaseRepo-AppolyJson:1.6.3")
```

## API Response Structure
Expand Down
10 changes: 5 additions & 5 deletions BaseRepo-Paging-AppolyJson/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,13 @@ follow Appoly's paging format.

```gradle.kts
// Requires the base modules
implementation("com.github.appoly.AppolyDroid-Toolbox:BaseRepo:1.6.2")
implementation("com.github.appoly.AppolyDroid-Toolbox:BaseRepo-Paging:1.6.2")
implementation("com.github.appoly.AppolyDroid-Toolbox:BaseRepo-Paging-AppolyJson:1.6.2")
implementation("com.github.appoly.AppolyDroid-Toolbox:BaseRepo:1.6.3")
implementation("com.github.appoly.AppolyDroid-Toolbox:BaseRepo-Paging:1.6.3")
implementation("com.github.appoly.AppolyDroid-Toolbox:BaseRepo-Paging-AppolyJson:1.6.3")

// For Compose UI integration
implementation("com.github.appoly.AppolyDroid-Toolbox:LazyListPagingExtensions:1.6.2") // For LazyColumn
implementation("com.github.appoly.AppolyDroid-Toolbox:LazyGridPagingExtensions:1.6.2") // For LazyGrid
implementation("com.github.appoly.AppolyDroid-Toolbox:LazyListPagingExtensions:1.6.3") // For LazyColumn
implementation("com.github.appoly.AppolyDroid-Toolbox:LazyGridPagingExtensions:1.6.3") // For LazyGrid
```

## API Response Format
Expand Down
8 changes: 4 additions & 4 deletions BaseRepo-Paging/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,12 @@ extended for specific JSON formats.

```gradle.kts
// Requires the base BaseRepo module
implementation("com.github.appoly.AppolyDroid-Toolbox:BaseRepo:1.6.2")
implementation("com.github.appoly.AppolyDroid-Toolbox:BaseRepo-Paging:1.6.2")
implementation("com.github.appoly.AppolyDroid-Toolbox:BaseRepo:1.6.3")
implementation("com.github.appoly.AppolyDroid-Toolbox:BaseRepo-Paging:1.6.3")

// For Compose UI integration
implementation("com.github.appoly.AppolyDroid-Toolbox:LazyListPagingExtensions:1.6.2") // For LazyColumn
implementation("com.github.appoly.AppolyDroid-Toolbox:LazyGridPagingExtensions:1.6.2") // For LazyGrid
implementation("com.github.appoly.AppolyDroid-Toolbox:LazyListPagingExtensions:1.6.3") // For LazyColumn
implementation("com.github.appoly.AppolyDroid-Toolbox:LazyGridPagingExtensions:1.6.3") // For LazyGrid
```

## Extensions
Expand Down
6 changes: 3 additions & 3 deletions BaseRepo-S3Uploader-Multipart/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@ Extension module that bridges BaseRepo and S3Uploader-Multipart, enabling pausab

```gradle.kts
// Requires the base modules
implementation("com.github.appoly.AppolyDroid-Toolbox:BaseRepo:1.6.2")
implementation("com.github.appoly.AppolyDroid-Toolbox:S3Uploader-Multipart:1.6.2")
implementation("com.github.appoly.AppolyDroid-Toolbox:BaseRepo-S3Uploader-Multipart:1.6.2")
implementation("com.github.appoly.AppolyDroid-Toolbox:BaseRepo:1.6.3")
implementation("com.github.appoly.AppolyDroid-Toolbox:S3Uploader-Multipart:1.6.3")
implementation("com.github.appoly.AppolyDroid-Toolbox:BaseRepo-S3Uploader-Multipart:1.6.3")
```

## Usage
Expand Down
6 changes: 3 additions & 3 deletions BaseRepo-S3Uploader/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,9 @@ An extension module that bridges BaseRepo and S3Uploader, enabling seamless file

```gradle.kts
// Requires both the base modules
implementation("com.github.appoly.AppolyDroid-Toolbox:BaseRepo:1.6.2")
implementation("com.github.appoly.AppolyDroid-Toolbox:S3Uploader:1.6.2")
implementation("com.github.appoly.AppolyDroid-Toolbox:BaseRepo-S3Uploader:1.6.2")
implementation("com.github.appoly.AppolyDroid-Toolbox:BaseRepo:1.6.3")
implementation("com.github.appoly.AppolyDroid-Toolbox:S3Uploader:1.6.3")
implementation("com.github.appoly.AppolyDroid-Toolbox:BaseRepo-S3Uploader:1.6.3")
```

## How it Works
Expand Down
2 changes: 1 addition & 1 deletion BaseRepo/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ Foundation module for implementing the repository pattern with standardized API
## Installation

```gradle.kts
implementation("com.github.appoly.AppolyDroid-Toolbox:BaseRepo:1.6.2")
implementation("com.github.appoly.AppolyDroid-Toolbox:BaseRepo:1.6.3")
```

## Extensions
Expand Down
94 changes: 94 additions & 0 deletions ComposeExtensions/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
# ComposeExtensions

Compose utilities for insets/IME padding, padding arithmetic, serialization-safe `MutableState`, and clipboard copying.

## Features

- Navigation-bar / IME padding helpers (`navigationBarsOrImePadding`, `navigationBarsOrNoneIfImePadding`)
- Keyboard visibility as state (`keyboardAsState`)
- `PaddingValues` addition operators and `hideWithIme` / `gradientTint` modifiers
- Serialization-safe `MutableState` holders for Voyager Screens (`serializableMutableStateOf`, `transientMutableStateOf`)
- Clipboard copier that wraps the suspend `LocalClipboard` API with optional pre-Android-13 toasts

## Installation

```gradle.kts
implementation("com.github.appoly.AppolyDroid-Toolbox:ComposeExtensions:1.6.3")
```

## Usage

### Insets and IME padding

Pad content by whichever is larger of the navigation bars or the keyboard:

```kotlin
Column(Modifier.navigationBarsOrImePadding()) { /* … */ }
```

For bottom bars that should lose nav-bar padding when the keyboard is up:

```kotlin
BottomBar(Modifier.navigationBarsOrNoneIfImePadding())
```

Observe keyboard visibility:

```kotlin
val keyboardVisible by keyboardAsState()
```

### Padding helpers

Add two `PaddingValues` (or a `PaddingValues` and `WindowInsets`) together:

```kotlin
val combined = contentPadding + WindowInsets.navigationBars
```

### Serialization-safe Compose state

Voyager `Screen`s are Java-serialized across process death. A plain `mutableStateOf` is not
`Serializable`, and a `@Transient` field restores as `null` (JVM does not run field initializers),
crashing on the next read. Use these drop-in holders instead:

```kotlin
// Persist & restore — one-shot guards where a reset would re-fire an effect
var firstOpen by serializableMutableStateOf(true)

// Reset to initial on restore — ephemeral presentation/event state
var sheetVisible by transientMutableStateOf(false)
```

The value (or initial, for transient) must be `Serializable` or `null`; construction and assignment
fail fast with `IllegalArgumentException` otherwise.

### Clipboard copier

`LocalClipboardManager` is deprecated in favour of `LocalClipboard`, whose `setClipEntry` is suspend.
`rememberClipboardCopier` centralises the scope/await/toast plumbing (and suppresses the app toast on
API 33+ where the system already shows one):

```kotlin
@Composable
fun PromoCodeRow(code: String) {
val copier = rememberClipboardCopier()
TextButton(onClick = {
copier.copyPlainText(
text = code,
label = "Promo code",
confirmationMessage = "Copied!", // toast only on Android < 13
)
}) {
Text("Copy")
}
}
```

Also available: `copyHtmlText`, `copyRawUri`, `copyUri`, `copyIntent`, or pass a raw `ClipEntry` to
`ClipboardCopier.copy`.

## Dependencies

- Jetpack Compose
- kotlinx-coroutines-android
3 changes: 3 additions & 0 deletions ComposeExtensions/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -60,12 +60,15 @@ dependencies {
implementation(libs.androidx.ui)
implementation(libs.androidx.material3)

implementation(libs.kotlinx.coroutines.android)

testImplementation(libs.junit)
testImplementation(libs.robolectric)
testImplementation(libs.androidx.junit)
testImplementation(platform(libs.androidx.compose.bom))
testImplementation(libs.androidx.ui.test.junit4)
testImplementation(libs.androidx.ui.test.manifest)
testImplementation(libs.kotlinx.coroutines.test)
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)
}
Expand Down
20 changes: 15 additions & 5 deletions ComposeExtensions/consumer-rules.pro
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
# No consumer R8/ProGuard rules required for this module.
#
# It ships no @Serializable models, Room entities/converters, reflection, or JNI — nothing
# R8 would strip that a consumer relies on at runtime. This file is intentionally rule-free
# (kept so the absence of keeps is a deliberate, reviewed decision rather than an oversight).
# ComposeExtensions ships Serializable MutableState holders (SerializableMutableState,
# TransientMutableState) that are Java-serialized inside Voyager Screens to survive process
# death. Java serialization invokes writeObject/readObject/serialVersionUID reflectively, so R8
# must be told to keep them — otherwise the value silently restores as null (NPE on next read)
# in minified consumer builds. Scoped to this package so we don't impose keeps on the consumer's
# own Serializable classes.
-keepclassmembers class uk.co.appoly.droid.compose.extensions.** implements java.io.Serializable {
static final long serialVersionUID;
private static final java.io.ObjectStreamField[] serialPersistentFields;
private void writeObject(java.io.ObjectOutputStream);
private void readObject(java.io.ObjectInputStream);
private void readObjectNoData();
java.lang.Object writeReplace();
java.lang.Object readResolve();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
package uk.co.appoly.droid.compose.extensions

import android.content.ClipData
import android.content.ContentResolver
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.widget.Toast
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.platform.ClipEntry
import androidx.compose.ui.platform.LocalClipboard
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.toClipEntry
import kotlinx.coroutines.launch

/**
* Copies a [ClipEntry] to the system clipboard and, on Android < 13, shows a confirmation Toast.
* Android 13+ surfaces its own clipboard confirmation, so we suppress ours to avoid a double toast.
*
* Obtain one with [rememberClipboardCopier]. It centralises the [LocalClipboard] plumbing:
* `setClipEntry` is a suspend function, so each copy runs on a remembered scope and the confirmation
* only fires once the write has returned. The payload is a raw [ClipEntry], so it handles text, a
* URI, multiple items, etc.; use [copyPlainText] for the common plain-text case.
*/
fun interface ClipboardCopier {
/**
* @param clipEntry the payload to copy.
* @param confirmationMessage pre-Android-13 Toast text; pass `null` to skip it. Resolve it in
* composition (e.g. `stringResource`) so a configuration change re-reads it.
*/
fun copy(clipEntry: ClipEntry, confirmationMessage: String?)
}

/**
* Copies plain [text] to the clipboard — the common case (wraps [ClipData.newPlainText]).
*
* @param text the text to copy.
* @param label human-readable [ClipData] label (required, mirroring `ClipData.newX`); read by
* clipboard managers and accessibility services, though not shown in the modern paste UI.
* @param confirmationMessage pre-Android-13 Toast text; pass `null` to skip it. Resolve it in
* composition (e.g. `stringResource`) so a configuration change re-reads it.
*/
fun ClipboardCopier.copyPlainText(
text: CharSequence,
label: CharSequence,
confirmationMessage: String? = null,
) = copy(ClipData.newPlainText(label, text).toClipEntry(), confirmationMessage)

/**
* Copies styled [htmlText] to the clipboard, with [text] as the plain-text fallback for consumers
* that can't render HTML (wraps [ClipData.newHtmlText]).
*
* @param text the plain-text representation.
* @param htmlText the HTML-markup representation.
* @param label human-readable [ClipData] label (required, mirroring `ClipData.newX`); read by
* clipboard managers and accessibility services, though not shown in the modern paste UI.
* @param confirmationMessage pre-Android-13 Toast text; pass `null` to skip it. Resolve it in
* composition (e.g. `stringResource`) so a configuration change re-reads it.
*/
fun ClipboardCopier.copyHtmlText(
text: CharSequence,
htmlText: String,
label: CharSequence,
confirmationMessage: String? = null,
) = copy(ClipData.newHtmlText(label, text, htmlText).toClipEntry(), confirmationMessage)

/**
* Copies a raw [uri] to the clipboard without resolving it through a [ContentResolver]
* (wraps [ClipData.newRawUri]). Use for URIs that aren't `content://` provider URIs — e.g. an
* `http`/`https` link or a `mailto:` address; for content URIs use [copyUri] instead.
*
* @param uri the URI to copy verbatim.
* @param label human-readable [ClipData] label (required, mirroring `ClipData.newX`); read by
* clipboard managers and accessibility services, though not shown in the modern paste UI.
* @param confirmationMessage pre-Android-13 Toast text; pass `null` to skip it. Resolve it in
* composition (e.g. `stringResource`) so a configuration change re-reads it.
*/
fun ClipboardCopier.copyRawUri(
uri: Uri,
label: CharSequence,
confirmationMessage: String? = null,
) = copy(ClipData.newRawUri(label, uri).toClipEntry(), confirmationMessage)

/**
* Copies a `content://` [uri] to the clipboard, querying its available MIME types from [resolver]
* so pasting apps receive the right type (wraps [ClipData.newUri]). For plain web/mail URIs prefer
* [copyRawUri].
*
* @param resolver resolves the URI's MIME types.
* @param uri the content URI to copy.
* @param label human-readable [ClipData] label (required, mirroring `ClipData.newX`); read by
* clipboard managers and accessibility services, though not shown in the modern paste UI.
* @param confirmationMessage pre-Android-13 Toast text; pass `null` to skip it. Resolve it in
* composition (e.g. `stringResource`) so a configuration change re-reads it.
*/
fun ClipboardCopier.copyUri(
resolver: ContentResolver,
uri: Uri,
label: CharSequence,
confirmationMessage: String? = null,
) = copy(ClipData.newUri(resolver, label, uri).toClipEntry(), confirmationMessage)

/**
* Copies an [intent] to the clipboard (wraps [ClipData.newIntent]) — e.g. a launcher shortcut.
*
* @param intent the Intent to copy.
* @param label human-readable [ClipData] label (required, mirroring `ClipData.newX`); read by
* clipboard managers and accessibility services, though not shown in the modern paste UI.
* @param confirmationMessage pre-Android-13 Toast text; pass `null` to skip it. Resolve it in
* composition (e.g. `stringResource`) so a configuration change re-reads it.
*/
fun ClipboardCopier.copyIntent(
intent: Intent,
label: CharSequence,
confirmationMessage: String? = null,
) = copy(ClipData.newIntent(label, intent).toClipEntry(), confirmationMessage)

/**
* Remembers a [ClipboardCopier] bound to the current [LocalClipboard] / [LocalContext] and a
* composition-scoped coroutine scope. Call one of the `copyX` extensions (e.g. [copyPlainText]) on
* the result to copy — see [ClipboardCopier] for the confirmation-Toast behaviour.
*/
@Composable
fun rememberClipboardCopier(): ClipboardCopier {
val clipboard = LocalClipboard.current
val context = LocalContext.current
val scope = rememberCoroutineScope()
return remember(clipboard, context, scope) {
ClipboardCopier { clipEntry, confirmationMessage ->
scope.launch {
clipboard.setClipEntry(clipEntry)
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU && confirmationMessage != null) {
Toast.makeText(context, confirmationMessage, Toast.LENGTH_SHORT).show()
}
}
}
}
}
Loading
Loading