diff --git a/CHANGELOG.md b/CHANGELOG.md
index b65b71c..aa29348 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,29 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## Unreleased
+### Added
+
+* Colored borders
+* New setting to control the compression behavior when importing photos
+
+## [v1.18.1] - 2024-11-07
+
+### Fixed
+
+* The loading icon would be visible behind transparent widgets
+
+## [v1.18.0] - 2024-11-06
+
+### Added
+
+* New tap action to pause/resume automatic photo cycling
+* New app theme option for using a true black background
+
+### Changed
+
+* Skip the unsaved changes warning when there are no changes
+* Added device details to bug reports
+
## [v1.17.0] - 2024-10-27
### Added
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index a239212..a11b9a6 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -14,8 +14,8 @@ object AppInfo {
const val APPLICATION_ID = "com.fibelatti.photowidget"
private const val VERSION_MAJOR = 1
- private const val VERSION_MINOR = 17
- private const val VERSION_PATCH = 0
+ private const val VERSION_MINOR = 18
+ private const val VERSION_PATCH = 1
private const val VERSION_BUILD = 0
val versionCode: Int =
@@ -191,6 +191,7 @@ dependencies {
implementation(libs.ucrop)
implementation(libs.coil)
implementation(libs.reorderable)
+ implementation(libs.colorpicker.compose)
implementation(libs.about.libraries)
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index a642b00..d64dedb 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -78,6 +78,13 @@
android:launchMode="singleInstance"
android:theme="@style/AppTheme.TransparentActivity" />
+
+
diff --git a/app/src/main/java/com/fibelatti/photowidget/configure/PhotoCropActivity.kt b/app/src/main/java/com/fibelatti/photowidget/configure/PhotoCropActivity.kt
index 6e9cfc4..9a70d53 100644
--- a/app/src/main/java/com/fibelatti/photowidget/configure/PhotoCropActivity.kt
+++ b/app/src/main/java/com/fibelatti/photowidget/configure/PhotoCropActivity.kt
@@ -13,7 +13,6 @@ import androidx.core.view.isInvisible
import androidx.core.view.isVisible
import androidx.core.view.updatePadding
import com.fibelatti.photowidget.databinding.PhotoCropActivityBinding
-import com.fibelatti.photowidget.model.PhotoWidget
import com.fibelatti.photowidget.model.PhotoWidgetAspectRatio
import com.fibelatti.photowidget.platform.getAttributeColor
import com.fibelatti.photowidget.platform.intentExtras
@@ -68,7 +67,6 @@ class PhotoCropActivity : AppCompatActivity(), UCropFragmentCallback {
withAspectRatio(intent.aspectRatio.x, intent.aspectRatio.y)
}
}
- .withMaxResultSize(PhotoWidget.MAX_STORAGE_DIMENSION, PhotoWidget.MAX_STORAGE_DIMENSION)
.withOptions(
UCrop.Options().apply {
setCompressionFormat(Bitmap.CompressFormat.PNG)
diff --git a/app/src/main/java/com/fibelatti/photowidget/configure/PhotoWidgetBorderPicker.kt b/app/src/main/java/com/fibelatti/photowidget/configure/PhotoWidgetBorderPicker.kt
new file mode 100644
index 0000000..bde8ee4
--- /dev/null
+++ b/app/src/main/java/com/fibelatti/photowidget/configure/PhotoWidgetBorderPicker.kt
@@ -0,0 +1,320 @@
+package com.fibelatti.photowidget.configure
+
+import android.content.Context
+import android.graphics.BitmapFactory
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.border
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.IntrinsicSize
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.aspectRatio
+import androidx.compose.foundation.layout.fillMaxHeight
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.foundation.text.KeyboardOptions
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.FilledTonalButton
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.SegmentedButton
+import androidx.compose.material3.SegmentedButtonDefaults
+import androidx.compose.material3.SingleChoiceSegmentedButtonRow
+import androidx.compose.material3.Slider
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextField
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableIntStateOf
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.asImageBitmap
+import androidx.compose.ui.input.nestedscroll.nestedScroll
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.platform.rememberNestedScrollInteropConnection
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.input.ImeAction
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.unit.dp
+import com.fibelatti.photowidget.R
+import com.fibelatti.photowidget.model.PhotoWidget
+import com.fibelatti.photowidget.model.PhotoWidgetAspectRatio
+import com.fibelatti.photowidget.platform.ComposeBottomSheetDialog
+import com.fibelatti.photowidget.platform.withRoundedCorners
+import com.fibelatti.photowidget.ui.SliderSmallThumb
+import com.fibelatti.ui.preview.ThemePreviews
+import com.fibelatti.ui.theme.ExtendedTheme
+import com.github.skydoves.colorpicker.compose.BrightnessSlider
+import com.github.skydoves.colorpicker.compose.HsvColorPicker
+import com.github.skydoves.colorpicker.compose.rememberColorPickerController
+
+object PhotoWidgetBorderPicker {
+
+ fun show(
+ context: Context,
+ currentColorHex: String?,
+ currentWidth: Int,
+ onApplyClick: (String?, Int) -> Unit,
+ ) {
+ ComposeBottomSheetDialog(context) {
+ BorderPickerContent(
+ currentColorHex = currentColorHex,
+ currentWidth = currentWidth,
+ ) { newColor, newWidth ->
+ onApplyClick(newColor, newWidth)
+ dismiss()
+ }
+ }.show()
+ }
+}
+
+@Composable
+private fun BorderPickerContent(
+ currentColorHex: String?,
+ currentWidth: Int,
+ onApplyClick: (String?, Int) -> Unit,
+) {
+ var color: String? by remember { mutableStateOf(currentColorHex) }
+ var width: Int by remember { mutableIntStateOf(currentWidth) }
+
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .nestedScroll(rememberNestedScrollInteropConnection())
+ .verticalScroll(rememberScrollState())
+ .padding(all = 16.dp),
+ ) {
+ Text(
+ text = stringResource(R.string.photo_widget_configure_border),
+ modifier = Modifier.fillMaxWidth(),
+ color = MaterialTheme.colorScheme.onSurface,
+ textAlign = TextAlign.Center,
+ style = MaterialTheme.typography.titleLarge,
+ )
+
+ Spacer(modifier = Modifier.size(8.dp))
+
+ SingleChoiceSegmentedButtonRow(
+ modifier = Modifier.fillMaxWidth(),
+ ) {
+ val buttonBorderColor = SegmentedButtonDefaults.borderStroke(
+ color = SegmentedButtonDefaults.colors().activeBorderColor,
+ )
+
+ SegmentedButton(
+ selected = color == null,
+ onClick = {
+ color = null
+ width = 0
+ },
+ shape = RoundedCornerShape(topStart = 16.dp, bottomStart = 16.dp),
+ border = buttonBorderColor,
+ label = {
+ Text(
+ text = stringResource(R.string.photo_widget_configure_border_none),
+ style = MaterialTheme.typography.bodySmall,
+ )
+ },
+ )
+
+ SegmentedButton(
+ selected = color != null,
+ onClick = {
+ if (color == null) {
+ color = "ffffff"
+ width = 20
+ }
+ },
+ shape = RoundedCornerShape(topEnd = 16.dp, bottomEnd = 16.dp),
+ border = buttonBorderColor,
+ label = {
+ Text(
+ text = stringResource(R.string.photo_widget_configure_border_color),
+ style = MaterialTheme.typography.bodySmall,
+ )
+ },
+ )
+ }
+
+ Spacer(modifier = Modifier.size(16.dp))
+
+ if (color != null) {
+ ColorBorderContent(
+ currentColorHex = requireNotNull(color),
+ onColorChange = { color = it },
+ currentWidth = width,
+ onWidthChange = { width = it },
+ )
+ }
+
+ FilledTonalButton(
+ onClick = { onApplyClick(color, width) },
+ modifier = Modifier.fillMaxWidth(),
+ ) {
+ Text(
+ text = stringResource(id = R.string.photo_widget_action_apply),
+ modifier = Modifier.weight(1f),
+ textAlign = TextAlign.Center,
+ )
+ }
+ }
+}
+
+@Composable
+@OptIn(ExperimentalMaterial3Api::class)
+private fun ColorBorderContent(
+ currentColorHex: String,
+ onColorChange: (String) -> Unit,
+ currentWidth: Int,
+ onWidthChange: (Int) -> Unit,
+ modifier: Modifier = Modifier,
+) {
+ Column(
+ modifier = modifier.fillMaxWidth(),
+ verticalArrangement = Arrangement.spacedBy(8.dp),
+ ) {
+ val localContext = LocalContext.current
+ val baseBitmap = remember {
+ BitmapFactory.decodeResource(localContext.resources, R.drawable.image_sample)
+ }
+ val colorPickerController = rememberColorPickerController()
+ var colorHex by remember { mutableStateOf(currentColorHex) }
+ val hexChars = remember {
+ charArrayOf(
+ '1', '2', '3', '4', '5', '6', '7', '8', '9', '0',
+ 'a', 'b', 'c', 'd', 'e', 'f',
+ 'A', 'B', 'C', 'D', 'E', 'F',
+ )
+ }
+
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.spacedBy(16.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ Image(
+ bitmap = baseBitmap
+ .withRoundedCorners(
+ aspectRatio = PhotoWidgetAspectRatio.SQUARE,
+ radius = PhotoWidget.DEFAULT_CORNER_RADIUS,
+ borderColorHex = currentColorHex,
+ borderWidth = currentWidth,
+ )
+ .asImageBitmap(),
+ contentDescription = null,
+ modifier = Modifier
+ .weight(1f)
+ .aspectRatio(1f),
+ )
+
+ HsvColorPicker(
+ modifier = Modifier
+ .weight(1f)
+ .aspectRatio(1f),
+ controller = colorPickerController,
+ onColorChanged = { colorEnvelope ->
+ colorHex = colorEnvelope.hexCode.drop(2)
+ onColorChange(colorHex)
+ },
+ initialColor = Color(android.graphics.Color.parseColor("#$currentColorHex")),
+ )
+ }
+
+ Row(
+ modifier = Modifier
+ .padding(top = 8.dp)
+ .fillMaxWidth()
+ .height(IntrinsicSize.Min),
+ horizontalArrangement = Arrangement.spacedBy(16.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ BrightnessSlider(
+ modifier = Modifier
+ .fillMaxHeight()
+ .weight(1f)
+ .border(
+ width = 1.dp,
+ color = MaterialTheme.colorScheme.outline.copy(alpha = 0.3f),
+ shape = RoundedCornerShape(6.dp),
+ ),
+ controller = colorPickerController,
+ )
+
+ TextField(
+ value = colorHex,
+ onValueChange = { newValue ->
+ if (newValue.length <= 6 && newValue.all { it in hexChars }) {
+ colorHex = newValue
+
+ if (colorHex.length == 6) {
+ val androidColor = android.graphics.Color.parseColor("#$colorHex")
+ colorPickerController.selectByColor(color = Color(androidColor), fromUser = true)
+ onColorChange(colorHex)
+ }
+ }
+ },
+ modifier = Modifier.width(100.dp),
+ textStyle = MaterialTheme.typography.labelMedium,
+ placeholder = {
+ Text(
+ text = "ffffff",
+ style = MaterialTheme.typography.labelMedium,
+ )
+ },
+ prefix = {
+ Text(
+ text = "#",
+ style = MaterialTheme.typography.labelMedium,
+ )
+ },
+ keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done),
+ singleLine = true,
+ maxLines = 1,
+ )
+ }
+
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.spacedBy(16.dp),
+ ) {
+ Slider(
+ value = currentWidth.toFloat(),
+ onValueChange = { onWidthChange(it.toInt()) },
+ modifier = Modifier.weight(1f),
+ valueRange = 0f..40f,
+ thumb = { SliderSmallThumb() },
+ )
+
+ Text(
+ text = "$currentWidth",
+ modifier = Modifier.width(40.dp),
+ color = MaterialTheme.colorScheme.onSurface,
+ textAlign = TextAlign.End,
+ style = MaterialTheme.typography.bodyMedium,
+ )
+ }
+ }
+}
+
+@Composable
+@ThemePreviews
+private fun BorderPickerContentPreview() {
+ ExtendedTheme {
+ BorderPickerContent(
+ currentColorHex = "86D986",
+ currentWidth = 20,
+ onApplyClick = { _, _ -> },
+ )
+ }
+}
diff --git a/app/src/main/java/com/fibelatti/photowidget/configure/PhotoWidgetConfigureActivity.kt b/app/src/main/java/com/fibelatti/photowidget/configure/PhotoWidgetConfigureActivity.kt
index 272e72c..8fd86e4 100644
--- a/app/src/main/java/com/fibelatti/photowidget/configure/PhotoWidgetConfigureActivity.kt
+++ b/app/src/main/java/com/fibelatti/photowidget/configure/PhotoWidgetConfigureActivity.kt
@@ -30,6 +30,7 @@ import com.fibelatti.photowidget.model.PhotoWidgetSource
import com.fibelatti.photowidget.model.PhotoWidgetTapAction
import com.fibelatti.photowidget.platform.AppTheme
import com.fibelatti.photowidget.platform.SelectionDialog
+import com.fibelatti.photowidget.platform.setIdentifierCompat
import com.fibelatti.photowidget.widget.PhotoWidgetProvider
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import dagger.hilt.android.AndroidEntryPoint
@@ -85,9 +86,10 @@ class PhotoWidgetConfigureActivity : AppCompatActivity() {
PhotoWidgetConfigureScreen(
photoWidget = state.photoWidget,
+ isUpdating = intent.appWidgetId != AppWidgetManager.INVALID_APPWIDGET_ID,
selectedPhoto = state.selectedPhoto,
isProcessing = state.isProcessing,
- onNavClick = ::handleBackNav,
+ onNavClick = onBackPressedDispatcher::onBackPressed,
onAspectRatioClick = ::showAspectRatioPicker,
onCropClick = viewModel::requestCrop,
onRemoveClick = viewModel::photoRemoved,
@@ -104,14 +106,15 @@ class PhotoWidgetConfigureActivity : AppCompatActivity() {
onTapActionPickerClick = ::showTapActionPicker,
onShapeChange = viewModel::shapeSelected,
onCornerRadiusChange = viewModel::cornerRadiusSelected,
+ onBorderChange = viewModel::borderSelected,
onOpacityChange = viewModel::opacitySelected,
onOffsetChange = viewModel::offsetSelected,
onPaddingChange = viewModel::paddingSelected,
onAddToHomeClick = viewModel::addNewWidget,
)
- LaunchedEffect(state.photoWidget.photos.isNotEmpty()) {
- onBackPressedCallback.isEnabled = state.photoWidget.photos.isNotEmpty()
+ LaunchedEffect(state.hasEdits) {
+ onBackPressedCallback.isEnabled = state.hasEdits
}
LaunchedEffect(state.messages) {
@@ -309,11 +312,13 @@ class PhotoWidgetConfigureActivity : AppCompatActivity() {
AppWidgetManager.EXTRA_APPWIDGET_PREVIEW to remoteViews,
)
- val callbackIntent = Intent(this, PhotoWidgetPinnedReceiver::class.java)
- .apply { this.photoWidget = photoWidget }
+ val callbackIntent = Intent(this, PhotoWidgetPinnedReceiver::class.java).apply {
+ setIdentifierCompat("$PIN_REQUEST_CODE")
+ this.photoWidget = photoWidget
+ }
val successCallback = PendingIntent.getBroadcast(
/* context = */ this,
- /* requestCode = */ 0,
+ /* requestCode = */ PIN_REQUEST_CODE,
/* intent = */ callbackIntent,
/* flags = */ PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_UPDATE_CURRENT,
)
@@ -330,6 +335,7 @@ class PhotoWidgetConfigureActivity : AppCompatActivity() {
companion object {
+ private const val PIN_REQUEST_CODE = 1001
const val ACTION_FINISH = "FINISH_PHOTO_WIDGET_CONFIGURE_ACTIVITY"
}
}
diff --git a/app/src/main/java/com/fibelatti/photowidget/configure/PhotoWidgetConfigureScreen.kt b/app/src/main/java/com/fibelatti/photowidget/configure/PhotoWidgetConfigureScreen.kt
index e2ad715..869e09e 100644
--- a/app/src/main/java/com/fibelatti/photowidget/configure/PhotoWidgetConfigureScreen.kt
+++ b/app/src/main/java/com/fibelatti/photowidget/configure/PhotoWidgetConfigureScreen.kt
@@ -119,6 +119,7 @@ import sh.calvin.reorderable.rememberReorderableLazyListState
@Composable
fun PhotoWidgetConfigureScreen(
photoWidget: PhotoWidget,
+ isUpdating: Boolean,
selectedPhoto: LocalPhoto?,
isProcessing: Boolean,
onNavClick: () -> Unit,
@@ -138,6 +139,7 @@ fun PhotoWidgetConfigureScreen(
onTapActionPickerClick: (PhotoWidgetTapAction) -> Unit,
onShapeChange: (String) -> Unit,
onCornerRadiusChange: (Float) -> Unit,
+ onBorderChange: (String?, Int) -> Unit,
onOpacityChange: (Float) -> Unit,
onOffsetChange: (horizontalOffset: Int, verticalOffset: Int) -> Unit,
onPaddingChange: (Int) -> Unit,
@@ -157,6 +159,7 @@ fun PhotoWidgetConfigureScreen(
PhotoWidgetConfigureContent(
photoWidget = photoWidget,
+ isUpdating = isUpdating,
selectedPhoto = selectedPhoto,
onNavClick = onNavClick,
onMoveLeftClick = onMoveLeftClick,
@@ -195,6 +198,14 @@ fun PhotoWidgetConfigureScreen(
)
}.show()
},
+ onBorderClick = {
+ PhotoWidgetBorderPicker.show(
+ context = localContext,
+ currentColorHex = photoWidget.borderColor,
+ currentWidth = photoWidget.borderWidth,
+ onApplyClick = onBorderChange,
+ )
+ },
onOpacityClick = {
ComposeBottomSheetDialog(localContext) {
OpacityPicker(
@@ -255,6 +266,7 @@ fun PhotoWidgetConfigureScreen(
@Composable
private fun PhotoWidgetConfigureContent(
photoWidget: PhotoWidget,
+ isUpdating: Boolean,
selectedPhoto: LocalPhoto?,
onNavClick: () -> Unit,
onAspectRatioClick: () -> Unit,
@@ -273,6 +285,7 @@ private fun PhotoWidgetConfigureContent(
onTapActionPickerClick: (PhotoWidgetTapAction) -> Unit,
onShapeClick: () -> Unit,
onCornerRadiusClick: () -> Unit,
+ onBorderClick: () -> Unit,
onOpacityClick: () -> Unit,
onOffsetClick: () -> Unit,
onPaddingClick: () -> Unit,
@@ -297,6 +310,7 @@ private fun PhotoWidgetConfigureContent(
PhotoWidgetConfigureContentSettings(
photoWidget = photoWidget,
+ isUpdating = isUpdating,
onChangeSource = onChangeSource,
onShuffleClick = onShuffleClick,
onPhotoPickerClick = onPhotoPickerClick,
@@ -307,6 +321,7 @@ private fun PhotoWidgetConfigureContent(
onAspectRatioClick = onAspectRatioClick,
onShapeClick = onShapeClick,
onCornerRadiusClick = onCornerRadiusClick,
+ onBorderClick = onBorderClick,
onOpacityClick = onOpacityClick,
onOffsetClick = onOffsetClick,
onPaddingClick = onPaddingClick,
@@ -333,6 +348,7 @@ private fun PhotoWidgetConfigureContent(
PhotoWidgetConfigureContentSettings(
photoWidget = photoWidget,
+ isUpdating = isUpdating,
onChangeSource = onChangeSource,
onShuffleClick = onShuffleClick,
onPhotoPickerClick = onPhotoPickerClick,
@@ -343,6 +359,7 @@ private fun PhotoWidgetConfigureContent(
onAspectRatioClick = onAspectRatioClick,
onShapeClick = onShapeClick,
onCornerRadiusClick = onCornerRadiusClick,
+ onBorderClick = onBorderClick,
onOpacityClick = onOpacityClick,
onOffsetClick = onOffsetClick,
onPaddingClick = onPaddingClick,
@@ -378,6 +395,8 @@ private fun PhotoWidgetConfigureContentViewer(
shapeId = photoWidget.shapeId,
modifier = Modifier.fillMaxSize(),
cornerRadius = photoWidget.cornerRadius,
+ borderColorHex = photoWidget.borderColor,
+ borderWidth = photoWidget.borderWidth,
opacity = photoWidget.opacity,
)
@@ -418,6 +437,7 @@ private fun PhotoWidgetConfigureContentViewer(
@Composable
private fun PhotoWidgetConfigureContentSettings(
photoWidget: PhotoWidget,
+ isUpdating: Boolean,
onChangeSource: (currentSource: PhotoWidgetSource, syncedDir: Set) -> Unit,
onShuffleClick: () -> Unit,
onPhotoPickerClick: () -> Unit,
@@ -428,6 +448,7 @@ private fun PhotoWidgetConfigureContentSettings(
onAspectRatioClick: () -> Unit,
onShapeClick: () -> Unit,
onCornerRadiusClick: () -> Unit,
+ onBorderClick: () -> Unit,
onOpacityClick: () -> Unit,
onOffsetClick: () -> Unit,
onPaddingClick: () -> Unit,
@@ -496,6 +517,19 @@ private fun PhotoWidgetConfigureContentSettings(
)
}
+ if (PhotoWidgetAspectRatio.FILL_WIDGET != photoWidget.aspectRatio) {
+ PickerDefault(
+ title = stringResource(R.string.photo_widget_configure_border),
+ currentValue = if (photoWidget.borderColor == null) {
+ stringResource(id = R.string.photo_widget_configure_border_none)
+ } else {
+ "#${photoWidget.borderColor}"
+ },
+ onClick = onBorderClick,
+ modifier = Modifier.padding(horizontal = 16.dp),
+ )
+ }
+
PickerDefault(
title = stringResource(id = R.string.widget_defaults_opacity),
currentValue = photoWidget.opacity.toInt().toString(),
@@ -569,7 +603,15 @@ private fun PhotoWidgetConfigureContentSettings(
.fillMaxWidth()
.padding(horizontal = 16.dp),
) {
- Text(text = stringResource(id = R.string.photo_widget_configure_add_to_home))
+ Text(
+ text = stringResource(
+ id = if (isUpdating) {
+ R.string.photo_widget_configure_save_changes
+ } else {
+ R.string.photo_widget_configure_add_to_home
+ },
+ ),
+ )
}
}
}
@@ -580,6 +622,8 @@ private fun PhotoWidgetViewer(
aspectRatio: PhotoWidgetAspectRatio,
shapeId: String,
cornerRadius: Float,
+ borderColorHex: String?,
+ borderWidth: Int,
opacity: Float,
modifier: Modifier = Modifier,
) {
@@ -621,6 +665,8 @@ private fun PhotoWidgetViewer(
)
.padding(start = 32.dp, top = 32.dp, end = 32.dp, bottom = 48.dp)
.fillMaxHeight(),
+ borderColorHex = borderColorHex,
+ borderWidth = borderWidth,
)
}
}
@@ -935,6 +981,7 @@ private fun PendingDeletionPhotoPicker(
Text(
text = stringResource(R.string.photo_widget_configure_photos_pending_deletion),
modifier = Modifier.padding(horizontal = 16.dp),
+ color = MaterialTheme.colorScheme.onBackground,
style = MaterialTheme.typography.titleMedium,
)
@@ -1043,6 +1090,8 @@ fun ShapedPhoto(
cornerRadius: Float,
opacity: Float,
modifier: Modifier = Modifier,
+ borderColorHex: String? = null,
+ borderWidth: Int = 0,
badge: @Composable BoxScope.() -> Unit = {},
isLoading: Boolean = false,
) {
@@ -1054,7 +1103,7 @@ fun ShapedPhoto(
else -> null
}
},
- dataKey = arrayOf(photo, shapeId, aspectRatio, cornerRadius, opacity),
+ dataKey = arrayOf(photo, shapeId, aspectRatio, cornerRadius, opacity, borderColorHex, borderWidth),
isLoading = isLoading,
contentScale = if (aspectRatio.isConstrained) {
ContentScale.FillWidth
@@ -1068,12 +1117,16 @@ fun ShapedPhoto(
withPolygonalShape(
shapeId = shapeId,
opacity = opacity,
+ borderColorHex = borderColorHex,
+ borderWidth = borderWidth,
)
} else {
withRoundedCorners(
aspectRatio = aspectRatio,
radius = cornerRadius,
opacity = opacity,
+ borderColorHex = borderColorHex,
+ borderWidth = borderWidth,
)
}
}
@@ -1132,6 +1185,7 @@ private fun PhotoWidgetConfigureScreenPreview() {
shapeId = PhotoWidget.DEFAULT_SHAPE_ID,
cornerRadius = PhotoWidget.DEFAULT_CORNER_RADIUS,
),
+ isUpdating = false,
selectedPhoto = LocalPhoto(name = "photo-1"),
isProcessing = false,
onNavClick = {},
@@ -1151,6 +1205,7 @@ private fun PhotoWidgetConfigureScreenPreview() {
onTapActionPickerClick = {},
onShapeChange = {},
onCornerRadiusChange = {},
+ onBorderChange = { _, _ -> },
onOpacityChange = {},
onOffsetChange = { _, _ -> },
onPaddingChange = {},
@@ -1180,6 +1235,7 @@ private fun PhotoWidgetConfigureScreenTallPreview() {
cornerRadius = PhotoWidget.DEFAULT_CORNER_RADIUS,
opacity = 80f,
),
+ isUpdating = true,
selectedPhoto = LocalPhoto(name = "photo-1"),
isProcessing = false,
onNavClick = {},
@@ -1199,6 +1255,7 @@ private fun PhotoWidgetConfigureScreenTallPreview() {
onTapActionPickerClick = {},
onShapeChange = {},
onCornerRadiusChange = {},
+ onBorderChange = { _, _ -> },
onOpacityChange = {},
onOffsetChange = { _, _ -> },
onPaddingChange = {},
diff --git a/app/src/main/java/com/fibelatti/photowidget/configure/PhotoWidgetConfigureState.kt b/app/src/main/java/com/fibelatti/photowidget/configure/PhotoWidgetConfigureState.kt
index 62b637c..73309e5 100644
--- a/app/src/main/java/com/fibelatti/photowidget/configure/PhotoWidgetConfigureState.kt
+++ b/app/src/main/java/com/fibelatti/photowidget/configure/PhotoWidgetConfigureState.kt
@@ -12,6 +12,7 @@ data class PhotoWidgetConfigureState(
val cropQueue: List = emptyList(),
val messages: List = emptyList(),
val markedForDeletion: Set = emptySet(),
+ val hasEdits: Boolean = false,
) {
sealed class Message {
diff --git a/app/src/main/java/com/fibelatti/photowidget/configure/PhotoWidgetConfigureViewModel.kt b/app/src/main/java/com/fibelatti/photowidget/configure/PhotoWidgetConfigureViewModel.kt
index bcb8cd4..9eaab03 100644
--- a/app/src/main/java/com/fibelatti/photowidget/configure/PhotoWidgetConfigureViewModel.kt
+++ b/app/src/main/java/com/fibelatti/photowidget/configure/PhotoWidgetConfigureViewModel.kt
@@ -25,9 +25,12 @@ import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onCompletion
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.update
+import kotlinx.coroutines.flow.withIndex
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import timber.log.Timber
@@ -80,6 +83,7 @@ class PhotoWidgetConfigureViewModel @Inject constructor(
),
selectedPhoto = photoWidget.photos.firstOrNull(),
isProcessing = photoWidget.isLoading,
+ hasEdits = sourceWidget != null,
)
}
}
@@ -89,11 +93,19 @@ class PhotoWidgetConfigureViewModel @Inject constructor(
} else {
loadPhotoWidgetUseCase(appWidgetId = appWidgetId)
.onEach { photoWidget -> updateState(photoWidget) }
+ .onCompletion { trackEdits() }
.launchIn(viewModelScope)
}
}
}
+ private fun trackEdits() {
+ viewModelScope.launch {
+ state.withIndex().first { (index, value) -> index > 0 && !value.hasEdits }
+ _state.update { current -> current.copy(hasEdits = true) }
+ }
+ }
+
fun changeSource() {
val newSource = when (_state.value.photoWidget.source) {
PhotoWidgetSource.PHOTOS -> PhotoWidgetSource.DIRECTORY
@@ -133,6 +145,11 @@ class PhotoWidgetConfigureViewModel @Inject constructor(
} else {
PhotoWidget.DEFAULT_CORNER_RADIUS
},
+ borderColor = if (PhotoWidgetAspectRatio.FILL_WIDGET == photoWidgetAspectRatio) {
+ null
+ } else {
+ current.photoWidget.borderColor
+ },
),
)
}
@@ -325,11 +342,17 @@ class PhotoWidgetConfigureViewModel @Inject constructor(
fun restorePhoto(photo: LocalPhoto) {
_state.update { current ->
+ val updatedPhotos = current.photoWidget.photos + photo
current.copy(
photoWidget = current.photoWidget.copy(
- photos = current.photoWidget.photos + photo,
+ photos = updatedPhotos,
photosPendingDeletion = current.photoWidget.photosPendingDeletion.filterNot { it.name == photo.name },
),
+ selectedPhoto = if (updatedPhotos.size == 1) {
+ updatedPhotos.firstOrNull()
+ } else {
+ current.selectedPhoto
+ },
markedForDeletion = current.markedForDeletion - photo.name,
)
}
@@ -413,6 +436,17 @@ class PhotoWidgetConfigureViewModel @Inject constructor(
}
}
+ fun borderSelected(colorHex: String?, width: Int) {
+ _state.update { current ->
+ current.copy(
+ photoWidget = current.photoWidget.copy(
+ borderColor = colorHex,
+ borderWidth = width,
+ ),
+ )
+ }
+ }
+
fun opacitySelected(opacity: Float) {
_state.update { current ->
current.copy(
diff --git a/app/src/main/java/com/fibelatti/photowidget/configure/PhotoWidgetTapActionPicker.kt b/app/src/main/java/com/fibelatti/photowidget/configure/PhotoWidgetTapActionPicker.kt
index 93d6b8e..cd780f7 100644
--- a/app/src/main/java/com/fibelatti/photowidget/configure/PhotoWidgetTapActionPicker.kt
+++ b/app/src/main/java/com/fibelatti/photowidget/configure/PhotoWidgetTapActionPicker.kt
@@ -35,7 +35,6 @@ import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.FilledTonalButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton
-import androidx.compose.material3.Switch
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
@@ -61,6 +60,7 @@ import com.fibelatti.photowidget.model.PhotoWidgetAspectRatio
import com.fibelatti.photowidget.model.PhotoWidgetTapAction
import com.fibelatti.photowidget.platform.ComposeBottomSheetDialog
import com.fibelatti.photowidget.platform.withRoundedCorners
+import com.fibelatti.photowidget.ui.Toggle
import com.fibelatti.ui.foundation.ColumnToggleButtonGroup
import com.fibelatti.ui.foundation.ToggleButtonGroup
import com.fibelatti.ui.preview.DevicePreviews
@@ -195,14 +195,14 @@ private fun TapActionPickerContent(
) {
Toggle(
title = stringResource(id = R.string.photo_widget_configure_tap_action_increase_brightness),
- enabled = value.increaseBrightness,
- onChange = { tapAction = value.copy(increaseBrightness = it) },
+ checked = value.increaseBrightness,
+ onCheckedChange = { tapAction = value.copy(increaseBrightness = it) },
)
Toggle(
title = stringResource(R.string.photo_widget_configure_tap_action_view_original_photo),
- enabled = value.viewOriginalPhoto,
- onChange = { tapAction = value.copy(viewOriginalPhoto = it) },
+ checked = value.viewOriginalPhoto,
+ onCheckedChange = { tapAction = value.copy(viewOriginalPhoto = it) },
)
}
}
@@ -225,6 +225,18 @@ private fun TapActionPickerContent(
)
}
+ is PhotoWidgetTapAction.ToggleCycling -> {
+ Text(
+ text = stringResource(
+ id = R.string.photo_widget_configure_tap_action_toggle_cycling_description,
+ ),
+ modifier = customOptionModifier,
+ color = MaterialTheme.colorScheme.onSurfaceVariant,
+ textAlign = TextAlign.Center,
+ style = MaterialTheme.typography.labelMedium,
+ )
+ }
+
PhotoWidgetTapAction.None -> Unit
}
}
@@ -341,31 +353,6 @@ private fun TapOptionsPicker(
)
}
-@Composable
-private fun Toggle(
- title: String,
- enabled: Boolean,
- onChange: (Boolean) -> Unit,
- modifier: Modifier = Modifier,
-) {
- Row(
- modifier = modifier.fillMaxWidth(),
- horizontalArrangement = Arrangement.spacedBy(16.dp),
- verticalAlignment = Alignment.CenterVertically,
- ) {
- Switch(
- checked = enabled,
- onCheckedChange = onChange,
- )
-
- Text(
- text = title,
- color = MaterialTheme.colorScheme.onSurface,
- style = MaterialTheme.typography.labelLarge,
- )
- }
-}
-
@Composable
private fun AppPicker(
onChooseApp: () -> Unit,
diff --git a/app/src/main/java/com/fibelatti/photowidget/configure/SavePhotoWidgetUseCase.kt b/app/src/main/java/com/fibelatti/photowidget/configure/SavePhotoWidgetUseCase.kt
index 26b1294..66d6e81 100644
--- a/app/src/main/java/com/fibelatti/photowidget/configure/SavePhotoWidgetUseCase.kt
+++ b/app/src/main/java/com/fibelatti/photowidget/configure/SavePhotoWidgetUseCase.kt
@@ -1,6 +1,7 @@
package com.fibelatti.photowidget.configure
import com.fibelatti.photowidget.model.PhotoWidget
+import com.fibelatti.photowidget.model.PhotoWidgetAspectRatio
import com.fibelatti.photowidget.model.PhotoWidgetSource
import com.fibelatti.photowidget.model.PhotoWidgetTapAction
import com.fibelatti.photowidget.widget.PhotoWidgetAlarmManager
@@ -68,6 +69,14 @@ class SavePhotoWidgetUseCase @Inject constructor(
cornerRadius = photoWidget.cornerRadius,
)
+ photoWidgetStorage.saveWidgetBorderColor(
+ appWidgetId = appWidgetId,
+ colorHex = photoWidget.borderColor.takeUnless {
+ PhotoWidgetAspectRatio.FILL_WIDGET == photoWidget.aspectRatio
+ },
+ width = photoWidget.borderWidth,
+ )
+
photoWidgetStorage.saveWidgetOpacity(
appWidgetId = appWidgetId,
opacity = photoWidget.opacity,
diff --git a/app/src/main/java/com/fibelatti/photowidget/di/PhotoWidgetEntryPoint.kt b/app/src/main/java/com/fibelatti/photowidget/di/PhotoWidgetEntryPoint.kt
index 44697ae..c18d12b 100644
--- a/app/src/main/java/com/fibelatti/photowidget/di/PhotoWidgetEntryPoint.kt
+++ b/app/src/main/java/com/fibelatti/photowidget/di/PhotoWidgetEntryPoint.kt
@@ -2,6 +2,7 @@ package com.fibelatti.photowidget.di
import com.fibelatti.photowidget.configure.SavePhotoWidgetUseCase
import com.fibelatti.photowidget.platform.PhotoDecoder
+import com.fibelatti.photowidget.preferences.UserPreferencesStorage
import com.fibelatti.photowidget.widget.CyclePhotoUseCase
import com.fibelatti.photowidget.widget.LoadPhotoWidgetUseCase
import com.fibelatti.photowidget.widget.PhotoWidgetAlarmManager
@@ -15,6 +16,8 @@ import kotlinx.coroutines.CoroutineScope
@InstallIn(SingletonComponent::class)
interface PhotoWidgetEntryPoint {
+ fun userPreferencesStorage(): UserPreferencesStorage
+
fun photoWidgetStorage(): PhotoWidgetStorage
fun photoWidgetAlarmManager(): PhotoWidgetAlarmManager
diff --git a/app/src/main/java/com/fibelatti/photowidget/home/HomeActivity.kt b/app/src/main/java/com/fibelatti/photowidget/home/HomeActivity.kt
index 516e417..396a765 100644
--- a/app/src/main/java/com/fibelatti/photowidget/home/HomeActivity.kt
+++ b/app/src/main/java/com/fibelatti/photowidget/home/HomeActivity.kt
@@ -2,6 +2,7 @@ package com.fibelatti.photowidget.home
import android.content.Intent
import android.net.Uri
+import android.os.Build
import android.os.Bundle
import android.os.Parcelable
import androidx.activity.compose.setContent
@@ -10,7 +11,11 @@ import androidx.activity.viewModels
import androidx.annotation.StringRes
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.app.AppCompatDelegate
+import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.getValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
import androidx.core.app.ActivityCompat
import androidx.core.app.ShareCompat
import androidx.lifecycle.compose.collectAsStateWithLifecycle
@@ -28,8 +33,10 @@ import com.fibelatti.photowidget.platform.AppTheme
import com.fibelatti.photowidget.platform.ComposeBottomSheetDialog
import com.fibelatti.photowidget.platform.SelectionDialog
import com.fibelatti.photowidget.preferences.Appearance
+import com.fibelatti.photowidget.preferences.DataSaverPicker
import com.fibelatti.photowidget.preferences.UserPreferencesStorage
import com.fibelatti.photowidget.preferences.WidgetDefaultsActivity
+import com.fibelatti.photowidget.ui.Toggle
import com.fibelatti.photowidget.widget.PhotoWidgetProvider
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import dagger.hilt.android.AndroidEntryPoint
@@ -59,6 +66,7 @@ class HomeActivity : AppCompatActivity() {
onCurrentWidgetClick = ::showExistingWidgetMenu,
onRemovedWidgetClick = ::showRemovedWidgetMenu,
onDefaultsClick = ::showDefaults,
+ onDataSaverClick = ::showDataSaverPicker,
onAppearanceClick = ::showAppearancePicker,
onColorsClick = ::showAppColorsPicker,
onSendFeedbackClick = ::sendFeedback,
@@ -184,6 +192,10 @@ class HomeActivity : AppCompatActivity() {
startActivity(Intent(this, WidgetDefaultsActivity::class.java))
}
+ private fun showDataSaverPicker() {
+ DataSaverPicker.show(context = this)
+ }
+
private fun showAppearancePicker() {
SelectionDialog.show(
context = this,
@@ -209,6 +221,14 @@ class HomeActivity : AppCompatActivity() {
AppCompatDelegate.setDefaultNightMode(mode)
},
+ footer = {
+ Toggle(
+ title = stringResource(R.string.photo_widget_home_true_black_background),
+ checked = userPreferencesStorage.useTrueBlack,
+ onCheckedChange = { newValue -> userPreferencesStorage.useTrueBlack = newValue },
+ modifier = Modifier.padding(all = 16.dp),
+ )
+ },
)
}
@@ -243,12 +263,21 @@ class HomeActivity : AppCompatActivity() {
}
private fun sendFeedback() {
+ val emailBody = StringBuilder().apply {
+ appendLine("Android Version: ${Build.VERSION.SDK_INT}")
+ appendLine("Device Manufacturer: ${Build.MANUFACTURER}")
+ appendLine("---")
+ appendLine(getString(R.string.help_email_body))
+ appendLine()
+ }
+
val emailIntent = Intent(Intent.ACTION_SENDTO, Uri.parse("mailto:")).apply {
putExtra(Intent.EXTRA_EMAIL, arrayOf("appsupport@fibelatti.com"))
putExtra(
Intent.EXTRA_SUBJECT,
"Material Photo Widget (${BuildConfig.VERSION_NAME}) — Feature request / Bug report",
)
+ putExtra(Intent.EXTRA_TEXT, emailBody.toString())
}
startActivity(Intent.createChooser(emailIntent, getString(R.string.photo_widget_home_feedback)))
@@ -274,6 +303,7 @@ class HomeActivity : AppCompatActivity() {
private enum class MyWidgetOptions(
@StringRes val label: Int,
) {
+
EDIT(label = R.string.photo_widget_home_my_widget_action_edit),
DUPLICATE(label = R.string.photo_widget_home_my_widget_action_duplicate),
}
@@ -281,6 +311,7 @@ class HomeActivity : AppCompatActivity() {
private enum class RemovedWidgetOptions(
@StringRes val label: Int,
) {
+
RESTORE(label = R.string.photo_widget_home_removed_widget_action_restore),
DELETE(label = R.string.photo_widget_home_removed_widget_action_delete),
}
diff --git a/app/src/main/java/com/fibelatti/photowidget/home/HomeScreen.kt b/app/src/main/java/com/fibelatti/photowidget/home/HomeScreen.kt
index 6e65587..55f6b7f 100644
--- a/app/src/main/java/com/fibelatti/photowidget/home/HomeScreen.kt
+++ b/app/src/main/java/com/fibelatti/photowidget/home/HomeScreen.kt
@@ -10,6 +10,7 @@ import androidx.compose.animation.core.tween
import androidx.compose.animation.core.updateTransition
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
+import androidx.compose.animation.scaleIn
import androidx.compose.animation.togetherWith
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
@@ -89,6 +90,7 @@ import com.fibelatti.photowidget.model.PhotoWidgetCycleMode
import com.fibelatti.photowidget.model.PhotoWidgetShapeBuilder
import com.fibelatti.photowidget.model.PhotoWidgetSource
import com.fibelatti.photowidget.model.PhotoWidgetTapAction
+import com.fibelatti.photowidget.ui.ShapesBanner
import com.fibelatti.ui.foundation.conditional
import com.fibelatti.ui.foundation.grayScale
import com.fibelatti.ui.preview.DevicePreviews
@@ -105,6 +107,7 @@ fun HomeScreen(
onCurrentWidgetClick: (appWidgetId: Int) -> Unit,
onRemovedWidgetClick: (appWidgetId: Int) -> Unit,
onDefaultsClick: () -> Unit,
+ onDataSaverClick: () -> Unit,
onAppearanceClick: () -> Unit,
onColorsClick: () -> Unit,
onSendFeedbackClick: () -> Unit,
@@ -135,7 +138,11 @@ fun HomeScreen(
.background(color = MaterialTheme.colorScheme.background)
.fillMaxSize()
.padding(paddingValues),
- transitionSpec = { fadeIn() togetherWith fadeOut() },
+ transitionSpec = {
+ (fadeIn(animationSpec = tween(220)) +
+ scaleIn(initialScale = 0.97f, animationSpec = tween(220)))
+ .togetherWith(fadeOut(animationSpec = tween(90)))
+ },
contentAlignment = Alignment.Center,
label = "Home_Navigation",
) { destination ->
@@ -158,6 +165,7 @@ fun HomeScreen(
HomeNavigationDestination.SETTINGS -> {
SettingsScreen(
onDefaultsClick = onDefaultsClick,
+ onDataSaverClick = onDataSaverClick,
onAppearanceClick = onAppearanceClick,
onColorsClick = onColorsClick,
onSendFeedbackClick = onSendFeedbackClick,
@@ -252,20 +260,25 @@ private fun NewWidgetScreen(
modifier = modifier.fillMaxSize(),
contentAlignment = Alignment.Center,
) {
+ ShapesBanner(
+ modifier = Modifier.align(Alignment.TopCenter),
+ )
+
Column(
modifier = Modifier
.widthIn(max = 600.dp)
.fillMaxWidth()
+ .padding(top = 68.dp)
.verticalScroll(rememberScrollState()),
horizontalAlignment = Alignment.CenterHorizontally,
- verticalArrangement = Arrangement.spacedBy(32.dp),
+ verticalArrangement = Arrangement.spacedBy(24.dp),
) {
AutoSizeText(
text = stringResource(id = R.string.photo_widget_home_title),
modifier = Modifier.padding(horizontal = 32.dp),
maxLines = 2,
textAlign = TextAlign.Center,
- style = MaterialTheme.typography.titleLarge,
+ style = MaterialTheme.typography.titleMedium,
)
Text(
@@ -463,6 +476,8 @@ private fun MyWidgetsScreen(
predicate = isRemoved,
ifTrue = { grayScale() },
),
+ borderColorHex = widget.borderColor,
+ borderWidth = widget.borderWidth,
isLoading = widget.isLoading,
)
}
@@ -563,6 +578,7 @@ private fun MyWidgetsScreen(
@Composable
private fun SettingsScreen(
onDefaultsClick: () -> Unit,
+ onDataSaverClick: () -> Unit,
onAppearanceClick: () -> Unit,
onColorsClick: () -> Unit,
onSendFeedbackClick: () -> Unit,
@@ -595,6 +611,12 @@ private fun SettingsScreen(
onClick = onDefaultsClick,
)
+ SettingsAction(
+ icon = R.drawable.ic_hard_drive,
+ label = R.string.photo_widget_home_data_saver,
+ onClick = onDataSaverClick,
+ )
+
SettingsAction(
icon = R.drawable.ic_appearance,
label = R.string.photo_widget_home_appearance,
@@ -770,6 +792,7 @@ private fun HomeScreenPreview() {
onCurrentWidgetClick = {},
onRemovedWidgetClick = {},
onDefaultsClick = {},
+ onDataSaverClick = {},
onAppearanceClick = {},
onColorsClick = {},
onSendFeedbackClick = {},
@@ -837,6 +860,7 @@ private fun SettingsScreenPreview() {
ExtendedTheme {
SettingsScreen(
onDefaultsClick = {},
+ onDataSaverClick = {},
onAppearanceClick = {},
onColorsClick = {},
onSendFeedbackClick = {},
diff --git a/app/src/main/java/com/fibelatti/photowidget/model/PhotoWidget.kt b/app/src/main/java/com/fibelatti/photowidget/model/PhotoWidget.kt
index 054b422..578b0fd 100644
--- a/app/src/main/java/com/fibelatti/photowidget/model/PhotoWidget.kt
+++ b/app/src/main/java/com/fibelatti/photowidget/model/PhotoWidget.kt
@@ -16,6 +16,8 @@ data class PhotoWidget(
val aspectRatio: PhotoWidgetAspectRatio = PhotoWidgetAspectRatio.SQUARE,
val shapeId: String = DEFAULT_SHAPE_ID,
val cornerRadius: Float = DEFAULT_CORNER_RADIUS,
+ val borderColor: String? = null,
+ val borderWidth: Int = 0,
val opacity: Float = DEFAULT_OPACITY,
val horizontalOffset: Int = 0,
val verticalOffset: Int = 0,
@@ -45,7 +47,6 @@ data class PhotoWidget(
companion object {
const val MAX_WIDGET_DIMENSION: Int = 720
- const val MAX_STORAGE_DIMENSION: Int = 3_840
const val DEFAULT_SHAPE_ID = "rounded-square"
const val DEFAULT_CORNER_RADIUS: Float = 64f
diff --git a/app/src/main/java/com/fibelatti/photowidget/model/PhotoWidgetTapAction.kt b/app/src/main/java/com/fibelatti/photowidget/model/PhotoWidgetTapAction.kt
index ec3fd41..7081519 100644
--- a/app/src/main/java/com/fibelatti/photowidget/model/PhotoWidgetTapAction.kt
+++ b/app/src/main/java/com/fibelatti/photowidget/model/PhotoWidgetTapAction.kt
@@ -58,6 +58,16 @@ sealed interface PhotoWidgetTapAction : Parcelable {
override val serializedName: String = "APP_SHORTCUT"
}
+ @Parcelize
+ data object ToggleCycling : PhotoWidgetTapAction {
+
+ @IgnoredOnParcel
+ override val label = R.string.photo_widget_configure_tap_action_toggle_cycling
+
+ @IgnoredOnParcel
+ override val serializedName: String = "TOGGLE_CYCLING"
+ }
+
companion object {
val DEFAULT: PhotoWidgetTapAction = None
@@ -68,6 +78,7 @@ sealed interface PhotoWidgetTapAction : Parcelable {
ViewFullScreen(),
ViewInGallery,
AppShortcut(),
+ ToggleCycling,
)
}
diff --git a/app/src/main/java/com/fibelatti/photowidget/platform/AppTheme.kt b/app/src/main/java/com/fibelatti/photowidget/platform/AppTheme.kt
index 7f784e3..7e7e438 100644
--- a/app/src/main/java/com/fibelatti/photowidget/platform/AppTheme.kt
+++ b/app/src/main/java/com/fibelatti/photowidget/platform/AppTheme.kt
@@ -21,6 +21,7 @@ fun AppTheme(
ExtendedTheme(
dynamicColor = userPreferences.dynamicColors,
+ useTrueBlack = userPreferences.useTrueBlack,
content = content,
)
}
diff --git a/app/src/main/java/com/fibelatti/photowidget/platform/BitmapKtx.kt b/app/src/main/java/com/fibelatti/photowidget/platform/BitmapKtx.kt
index 803ec26..e5dd6ea 100644
--- a/app/src/main/java/com/fibelatti/photowidget/platform/BitmapKtx.kt
+++ b/app/src/main/java/com/fibelatti/photowidget/platform/BitmapKtx.kt
@@ -2,10 +2,12 @@ package com.fibelatti.photowidget.platform
import android.graphics.Bitmap
import android.graphics.Canvas
+import android.graphics.Color
import android.graphics.Paint
import android.graphics.PorterDuff
import android.graphics.PorterDuffXfermode
import android.graphics.Rect
+import androidx.annotation.IntRange
import androidx.core.graphics.toRectF
import androidx.graphics.shapes.toPath
import com.fibelatti.photowidget.model.PhotoWidget
@@ -19,9 +21,13 @@ fun Bitmap.withRoundedCorners(
aspectRatio: PhotoWidgetAspectRatio,
radius: Float = PhotoWidget.DEFAULT_CORNER_RADIUS,
opacity: Float = PhotoWidget.DEFAULT_OPACITY,
+ borderColorHex: String? = null,
+ @IntRange(from = 0) borderWidth: Int = 0,
): Bitmap = withTransformation(
aspectRatio = aspectRatio,
opacity = opacity,
+ borderColorHex = borderColorHex,
+ borderWidth = borderWidth,
) { canvas, rect, paint ->
canvas.drawRoundRect(rect.toRectF(), radius, radius, paint)
}
@@ -29,9 +35,13 @@ fun Bitmap.withRoundedCorners(
fun Bitmap.withPolygonalShape(
shapeId: String,
opacity: Float = PhotoWidget.DEFAULT_OPACITY,
+ borderColorHex: String? = null,
+ @IntRange(from = 0) borderWidth: Int = 0,
): Bitmap = withTransformation(
aspectRatio = PhotoWidgetAspectRatio.SQUARE,
opacity = opacity,
+ borderColorHex = borderColorHex,
+ borderWidth = borderWidth,
) { canvas, rect, paint ->
try {
val shape = PhotoWidgetShapeBuilder.buildShape(
@@ -52,6 +62,8 @@ fun Bitmap.withPolygonalShape(
private inline fun Bitmap.withTransformation(
aspectRatio: PhotoWidgetAspectRatio,
opacity: Float,
+ borderColorHex: String? = null,
+ @IntRange(from = 0) borderWidth: Int = 0,
body: (Canvas, Rect, Paint) -> Unit,
): Bitmap {
val source = when (aspectRatio) {
@@ -116,5 +128,15 @@ private inline fun Bitmap.withTransformation(
paint.setXfermode(PorterDuffXfermode(PorterDuff.Mode.SRC_IN))
canvas.drawBitmap(this, source, destination, paint)
+ val borderColor = runCatching { Color.parseColor("#$borderColorHex") }.getOrNull()
+ if (borderColor != null && borderWidth > 0) {
+ val stroke = paint.apply {
+ style = Paint.Style.STROKE
+ color = borderColor
+ strokeWidth = borderWidth.toFloat()
+ }
+ body(canvas, if (PhotoWidgetAspectRatio.SQUARE == aspectRatio) source else destination, stroke)
+ }
+
return output
}
diff --git a/app/src/main/java/com/fibelatti/photowidget/platform/IntentKtx.kt b/app/src/main/java/com/fibelatti/photowidget/platform/IntentKtx.kt
new file mode 100644
index 0000000..32301f3
--- /dev/null
+++ b/app/src/main/java/com/fibelatti/photowidget/platform/IntentKtx.kt
@@ -0,0 +1,10 @@
+package com.fibelatti.photowidget.platform
+
+import android.content.Intent
+import android.os.Build
+
+fun Intent.setIdentifierCompat(value: String?): Intent = apply {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+ setIdentifier(value)
+ }
+}
diff --git a/app/src/main/java/com/fibelatti/photowidget/platform/PhotoDecoder.kt b/app/src/main/java/com/fibelatti/photowidget/platform/PhotoDecoder.kt
index bd0dc62..27ab65f 100644
--- a/app/src/main/java/com/fibelatti/photowidget/platform/PhotoDecoder.kt
+++ b/app/src/main/java/com/fibelatti/photowidget/platform/PhotoDecoder.kt
@@ -20,14 +20,14 @@ class PhotoDecoder @Inject constructor(
suspend fun decode(
data: Any?,
- maxDimension: Int,
+ maxDimension: Int? = null,
): Bitmap? = withContext(Dispatchers.IO) {
Timber.d("Decoding $data into a bitmap (maxDimension=$maxDimension)")
val request = ImageRequest.Builder(context)
.data(data)
.allowHardware(enable = false)
- .size(maxDimension)
+ .apply { if (maxDimension != null) size(maxDimension) }
.build()
imageLoader.execute(request)
diff --git a/app/src/main/java/com/fibelatti/photowidget/platform/SelectionDialog.kt b/app/src/main/java/com/fibelatti/photowidget/platform/SelectionDialog.kt
index 288a355..52e29e5 100644
--- a/app/src/main/java/com/fibelatti/photowidget/platform/SelectionDialog.kt
+++ b/app/src/main/java/com/fibelatti/photowidget/platform/SelectionDialog.kt
@@ -29,6 +29,7 @@ object SelectionDialog {
optionName: (T) -> String,
optionIcon: (T) -> Int? = { null },
onOptionSelected: (T) -> Unit,
+ footer: @Composable () -> Unit = {},
) {
ComposeBottomSheetDialog(context) {
SelectionDialogContent(
@@ -40,6 +41,7 @@ object SelectionDialog {
onOptionSelected(option)
dismiss()
},
+ footer = footer,
)
}.show()
}
@@ -53,6 +55,7 @@ private fun SelectionDialogContent(
optionName: (T) -> String,
optionIcon: (T) -> Int? = { null },
onOptionSelected: (T) -> Unit,
+ footer: @Composable () -> Unit = {},
) {
LazyColumn(
modifier = Modifier
@@ -93,5 +96,9 @@ private fun SelectionDialogContent(
}
}
}
+
+ item {
+ footer()
+ }
}
}
diff --git a/app/src/main/java/com/fibelatti/photowidget/preferences/DataSaverPicker.kt b/app/src/main/java/com/fibelatti/photowidget/preferences/DataSaverPicker.kt
new file mode 100644
index 0000000..8526168
--- /dev/null
+++ b/app/src/main/java/com/fibelatti/photowidget/preferences/DataSaverPicker.kt
@@ -0,0 +1,86 @@
+package com.fibelatti.photowidget.preferences
+
+import android.content.Context
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.unit.dp
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import com.fibelatti.photowidget.R
+import com.fibelatti.photowidget.di.PhotoWidgetEntryPoint
+import com.fibelatti.photowidget.di.entryPoint
+import com.fibelatti.photowidget.platform.ComposeBottomSheetDialog
+import com.fibelatti.photowidget.ui.Toggle
+import com.fibelatti.ui.preview.LocalePreviews
+import com.fibelatti.ui.preview.ThemePreviews
+import com.fibelatti.ui.theme.ExtendedTheme
+
+object DataSaverPicker {
+
+ fun show(context: Context) {
+ val userPreferencesStorage = entryPoint(context).userPreferencesStorage()
+
+ ComposeBottomSheetDialog(context) {
+ val preferences by userPreferencesStorage.userPreferences.collectAsStateWithLifecycle()
+
+ DataSaverPickerContent(
+ dataSaver = preferences.dataSaver,
+ onDataSaverChange = { newValue -> userPreferencesStorage.dataSaver = newValue },
+ )
+ }.show()
+ }
+}
+
+@Composable
+private fun DataSaverPickerContent(
+ dataSaver: Boolean,
+ onDataSaverChange: (Boolean) -> Unit,
+) {
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(all = 16.dp),
+ verticalArrangement = Arrangement.spacedBy(16.dp),
+ ) {
+ Text(
+ text = stringResource(R.string.photo_widget_home_data_saver),
+ modifier = Modifier.fillMaxWidth(),
+ color = MaterialTheme.colorScheme.onSurface,
+ textAlign = TextAlign.Center,
+ style = MaterialTheme.typography.titleLarge,
+ )
+
+ Toggle(
+ title = stringResource(R.string.preferences_data_saver),
+ checked = dataSaver,
+ onCheckedChange = onDataSaverChange,
+ )
+
+ Text(
+ text = stringResource(R.string.preferences_data_saver_description),
+ modifier = Modifier.fillMaxWidth(),
+ color = MaterialTheme.colorScheme.onSurface,
+ style = MaterialTheme.typography.bodyMedium,
+ )
+ }
+}
+
+@Composable
+@ThemePreviews
+@LocalePreviews
+private fun DataSaverPickerContentPreview() {
+ ExtendedTheme {
+ DataSaverPickerContent(
+ dataSaver = true,
+ onDataSaverChange = {},
+ )
+ }
+}
diff --git a/app/src/main/java/com/fibelatti/photowidget/preferences/UserPreferences.kt b/app/src/main/java/com/fibelatti/photowidget/preferences/UserPreferences.kt
index fd99c07..f5f0d74 100644
--- a/app/src/main/java/com/fibelatti/photowidget/preferences/UserPreferences.kt
+++ b/app/src/main/java/com/fibelatti/photowidget/preferences/UserPreferences.kt
@@ -5,7 +5,9 @@ import com.fibelatti.photowidget.model.PhotoWidgetSource
import com.fibelatti.photowidget.model.PhotoWidgetTapAction
data class UserPreferences(
+ val dataSaver: Boolean,
val appearance: Appearance,
+ val useTrueBlack: Boolean,
val dynamicColors: Boolean,
val defaultSource: PhotoWidgetSource,
val defaultShuffle: Boolean,
diff --git a/app/src/main/java/com/fibelatti/photowidget/preferences/UserPreferencesStorage.kt b/app/src/main/java/com/fibelatti/photowidget/preferences/UserPreferencesStorage.kt
index aa704a4..5009c74 100644
--- a/app/src/main/java/com/fibelatti/photowidget/preferences/UserPreferencesStorage.kt
+++ b/app/src/main/java/com/fibelatti/photowidget/preferences/UserPreferencesStorage.kt
@@ -29,7 +29,9 @@ class UserPreferencesStorage @Inject constructor(@ApplicationContext context: Co
private val _userPreferences = MutableStateFlow(
UserPreferences(
+ dataSaver = dataSaver,
appearance = appearance,
+ useTrueBlack = useTrueBlack,
dynamicColors = dynamicColors,
defaultSource = defaultSource,
defaultShuffle = defaultShuffle,
@@ -43,6 +45,15 @@ class UserPreferencesStorage @Inject constructor(@ApplicationContext context: Co
val userPreferences: StateFlow = _userPreferences.asStateFlow()
// region App
+ var dataSaver: Boolean
+ get() {
+ return sharedPreferences.getBoolean(Preference.DATA_SAVER.value, true)
+ }
+ set(value) {
+ sharedPreferences.edit { putBoolean(Preference.DATA_SAVER.value, value) }
+ _userPreferences.update { current -> current.copy(dataSaver = value) }
+ }
+
var appearance: Appearance
get() {
val name = sharedPreferences.getString(Preference.APP_APPEARANCE.value, null)
@@ -53,6 +64,15 @@ class UserPreferencesStorage @Inject constructor(@ApplicationContext context: Co
_userPreferences.update { current -> current.copy(appearance = value) }
}
+ var useTrueBlack: Boolean
+ get() {
+ return sharedPreferences.getBoolean(Preference.USE_TRUE_BLACK.value, false)
+ }
+ set(value) {
+ sharedPreferences.edit { putBoolean(Preference.USE_TRUE_BLACK.value, value) }
+ _userPreferences.update { current -> current.copy(useTrueBlack = value) }
+ }
+
var dynamicColors: Boolean
get() {
return sharedPreferences.getBoolean(Preference.APP_DYNAMIC_COLORS.value, true)
@@ -235,7 +255,9 @@ class UserPreferencesStorage @Inject constructor(@ApplicationContext context: Co
}
_userPreferences.update {
UserPreferences(
+ dataSaver = dataSaver,
appearance = appearance,
+ useTrueBlack = useTrueBlack,
dynamicColors = dynamicColors,
defaultSource = defaultSource,
defaultShuffle = defaultShuffle,
@@ -249,7 +271,9 @@ class UserPreferencesStorage @Inject constructor(@ApplicationContext context: Co
}
private enum class Preference(val value: String) {
+ DATA_SAVER(value = "user_preferences_data_saver"),
APP_APPEARANCE(value = "user_preferences_appearance"),
+ USE_TRUE_BLACK(value = "user_preferences_use_true_black"),
APP_DYNAMIC_COLORS(value = "user_preferences_dynamic_colors"),
DEFAULT_ASPECT_RATIO(value = "default_aspect_ratio"),
diff --git a/app/src/main/java/com/fibelatti/photowidget/preferences/WidgetDefaultsScreen.kt b/app/src/main/java/com/fibelatti/photowidget/preferences/WidgetDefaultsScreen.kt
index 43a4c39..47d8e00 100644
--- a/app/src/main/java/com/fibelatti/photowidget/preferences/WidgetDefaultsScreen.kt
+++ b/app/src/main/java/com/fibelatti/photowidget/preferences/WidgetDefaultsScreen.kt
@@ -40,6 +40,7 @@ import androidx.compose.material3.Slider
import androidx.compose.material3.Switch
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
+import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf
@@ -180,6 +181,9 @@ private fun WidgetDefaultsScreen(
)
}
},
+ colors = TopAppBarDefaults.topAppBarColors(
+ containerColor = MaterialTheme.colorScheme.background,
+ ),
)
},
contentWindowInsets = WindowInsets.safeDrawing,
@@ -601,7 +605,9 @@ private fun WidgetDefaultsScreenPreview() {
ExtendedTheme {
WidgetDefaultsScreen(
userPreferences = UserPreferences(
+ dataSaver = true,
appearance = Appearance.FOLLOW_SYSTEM,
+ useTrueBlack = false,
dynamicColors = true,
defaultSource = PhotoWidgetSource.PHOTOS,
defaultShuffle = false,
diff --git a/app/src/main/java/com/fibelatti/photowidget/ui/AsyncPhotoViewer.kt b/app/src/main/java/com/fibelatti/photowidget/ui/AsyncPhotoViewer.kt
index d3d2ff3..7ed44d3 100644
--- a/app/src/main/java/com/fibelatti/photowidget/ui/AsyncPhotoViewer.kt
+++ b/app/src/main/java/com/fibelatti/photowidget/ui/AsyncPhotoViewer.kt
@@ -23,13 +23,15 @@ import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
-import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalInspectionMode
import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.unit.max
import com.fibelatti.photowidget.R
import com.fibelatti.photowidget.di.PhotoWidgetEntryPoint
import com.fibelatti.photowidget.di.entryPoint
import com.fibelatti.photowidget.model.PhotoWidget
+import com.fibelatti.ui.foundation.dpToPx
+import kotlin.math.roundToInt
import kotlinx.coroutines.delay
@Composable
@@ -39,6 +41,7 @@ fun AsyncPhotoViewer(
isLoading: Boolean,
contentScale: ContentScale,
modifier: Modifier = Modifier,
+ constrainBitmapSize: Boolean = true,
transformer: (Bitmap?) -> Bitmap? = { it },
badge: @Composable BoxScope.() -> Unit = {},
) {
@@ -70,10 +73,11 @@ fun AsyncPhotoViewer(
val decoder by remember {
lazy { entryPoint(localContext).photoDecoder() }
}
- val maxDimension = with(LocalDensity.current) {
- remember(maxWidth) {
- maxWidth.toPx().toInt().coerceAtMost(maximumValue = PhotoWidget.MAX_WIDGET_DIMENSION)
- }
+ val largestSize = max(maxWidth, maxHeight)
+ val maxDimension = if (constrainBitmapSize) {
+ largestSize.dpToPx().roundToInt().coerceAtMost(maximumValue = PhotoWidget.MAX_WIDGET_DIMENSION)
+ } else {
+ null
}
LaunchedEffect(*dataKey) {
diff --git a/app/src/main/java/com/fibelatti/photowidget/ui/ShapesBanner.kt b/app/src/main/java/com/fibelatti/photowidget/ui/ShapesBanner.kt
new file mode 100644
index 0000000..5ce0053
--- /dev/null
+++ b/app/src/main/java/com/fibelatti/photowidget/ui/ShapesBanner.kt
@@ -0,0 +1,97 @@
+package com.fibelatti.photowidget.ui
+
+import androidx.compose.animation.core.animateFloat
+import androidx.compose.animation.core.infiniteRepeatable
+import androidx.compose.animation.core.rememberInfiniteTransition
+import androidx.compose.animation.core.tween
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableFloatStateOf
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.graphicsLayer
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+import com.fibelatti.photowidget.configure.ColoredShape
+import com.fibelatti.photowidget.model.PhotoWidgetShapeBuilder
+import com.fibelatti.ui.preview.DevicePreviews
+import com.fibelatti.ui.theme.ExtendedTheme
+
+@Composable
+fun ShapesBanner(
+ modifier: Modifier = Modifier,
+ polygonSize: Dp = 48.dp,
+) {
+ val polygons = remember {
+ listOf("scallop", "medal", "clover", "octagon", "hexagon")
+ .shuffled()
+ .map(PhotoWidgetShapeBuilder::buildShape)
+ }
+ var isAnimating by remember { mutableStateOf(false) }
+ val transition = rememberInfiniteTransition(label = "ShapesBannerTransition")
+ var desiredEasing by remember { mutableFloatStateOf(0f) }
+ val coefficient by transition.animateFloat(
+ initialValue = 0f,
+ targetValue = 1f,
+ animationSpec = infiniteRepeatable(
+ tween(
+ durationMillis = 10_000,
+ easing = {
+ if (isAnimating) {
+ desiredEasing += 0.0016669631f
+
+ if (desiredEasing >= 1f) {
+ desiredEasing = 0f
+ }
+ }
+
+ desiredEasing
+ },
+ ),
+ ),
+ label = "ShapesBannerAnimation_Rotation",
+ )
+
+ Row(
+ modifier = modifier
+ .fillMaxWidth()
+ .padding(vertical = 10.dp)
+ .clickable(
+ interactionSource = remember { MutableInteractionSource() },
+ indication = null,
+ ) {
+ isAnimating = !isAnimating
+ },
+ horizontalArrangement = Arrangement.SpaceEvenly
+ ) {
+ polygons.forEach { roundedPolygon ->
+ ColoredShape(
+ polygon = roundedPolygon,
+ color = MaterialTheme.colorScheme.primary,
+ modifier = Modifier
+ .size(polygonSize)
+ .graphicsLayer {
+ rotationZ = 360f * coefficient
+ },
+ )
+ }
+ }
+}
+
+@Composable
+@DevicePreviews
+private fun ShapesBannerPreview() {
+ ExtendedTheme {
+ ShapesBanner()
+ }
+}
diff --git a/app/src/main/java/com/fibelatti/photowidget/ui/Toggle.kt b/app/src/main/java/com/fibelatti/photowidget/ui/Toggle.kt
new file mode 100644
index 0000000..c4dd156
--- /dev/null
+++ b/app/src/main/java/com/fibelatti/photowidget/ui/Toggle.kt
@@ -0,0 +1,37 @@
+package com.fibelatti.photowidget.ui
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Switch
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+
+@Composable
+fun Toggle(
+ title: String,
+ checked: Boolean,
+ onCheckedChange: (Boolean) -> Unit,
+ modifier: Modifier = Modifier,
+) {
+ Row(
+ modifier = modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.spacedBy(16.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ Switch(
+ checked = checked,
+ onCheckedChange = onCheckedChange,
+ )
+
+ Text(
+ text = title,
+ color = MaterialTheme.colorScheme.onSurface,
+ style = MaterialTheme.typography.bodyLarge,
+ )
+ }
+}
diff --git a/app/src/main/java/com/fibelatti/photowidget/viewer/PhotoWidgetViewerActivity.kt b/app/src/main/java/com/fibelatti/photowidget/viewer/PhotoWidgetViewerActivity.kt
index 57ff97b..3b4b76a 100644
--- a/app/src/main/java/com/fibelatti/photowidget/viewer/PhotoWidgetViewerActivity.kt
+++ b/app/src/main/java/com/fibelatti/photowidget/viewer/PhotoWidgetViewerActivity.kt
@@ -181,6 +181,7 @@ private fun ScreenContent(
}
.transformable(state = state)
.aspectRatio(ratio = aspectRatio.aspectRatio),
+ constrainBitmapSize = false,
)
Row(
diff --git a/app/src/main/java/com/fibelatti/photowidget/widget/LoadPhotoWidgetUseCase.kt b/app/src/main/java/com/fibelatti/photowidget/widget/LoadPhotoWidgetUseCase.kt
index 879d896..e7b8adc 100644
--- a/app/src/main/java/com/fibelatti/photowidget/widget/LoadPhotoWidgetUseCase.kt
+++ b/app/src/main/java/com/fibelatti/photowidget/widget/LoadPhotoWidgetUseCase.kt
@@ -30,6 +30,8 @@ class LoadPhotoWidgetUseCase @Inject constructor(
aspectRatio = getWidgetAspectRatio(appWidgetId = appWidgetId),
shapeId = getWidgetShapeId(appWidgetId = appWidgetId),
cornerRadius = getWidgetCornerRadius(appWidgetId = appWidgetId),
+ borderColor = getWidgetBorderColorHex(appWidgetId = appWidgetId),
+ borderWidth = getWidgetBorderWidth(appWidgetId = appWidgetId),
opacity = getWidgetOpacity(appWidgetId = appWidgetId),
horizontalOffset = horizontalOffset,
verticalOffset = verticalOffset,
diff --git a/app/src/main/java/com/fibelatti/photowidget/widget/PhotoWidgetAlarmManager.kt b/app/src/main/java/com/fibelatti/photowidget/widget/PhotoWidgetAlarmManager.kt
index 9274052..bd14dfc 100644
--- a/app/src/main/java/com/fibelatti/photowidget/widget/PhotoWidgetAlarmManager.kt
+++ b/app/src/main/java/com/fibelatti/photowidget/widget/PhotoWidgetAlarmManager.kt
@@ -11,6 +11,7 @@ import com.fibelatti.photowidget.configure.appWidgetId
import com.fibelatti.photowidget.di.PhotoWidgetEntryPoint
import com.fibelatti.photowidget.di.entryPoint
import com.fibelatti.photowidget.model.PhotoWidgetCycleMode
+import com.fibelatti.photowidget.platform.setIdentifierCompat
import com.fibelatti.photowidget.widget.data.PhotoWidgetStorage
import dagger.hilt.android.qualifiers.ApplicationContext
import java.util.Calendar
@@ -168,6 +169,7 @@ class ExactRepeatingAlarmReceiver : BroadcastReceiver() {
appWidgetId: Int,
): PendingIntent {
val intent = Intent(context, ExactRepeatingAlarmReceiver::class.java).apply {
+ setIdentifierCompat("$appWidgetId")
this.appWidgetId = appWidgetId
}
return PendingIntent.getBroadcast(
diff --git a/app/src/main/java/com/fibelatti/photowidget/widget/PhotoWidgetProvider.kt b/app/src/main/java/com/fibelatti/photowidget/widget/PhotoWidgetProvider.kt
index f3c3a45..2e67f57 100644
--- a/app/src/main/java/com/fibelatti/photowidget/widget/PhotoWidgetProvider.kt
+++ b/app/src/main/java/com/fibelatti/photowidget/widget/PhotoWidgetProvider.kt
@@ -22,6 +22,7 @@ import com.fibelatti.photowidget.model.PhotoWidget
import com.fibelatti.photowidget.model.PhotoWidgetAspectRatio
import com.fibelatti.photowidget.model.PhotoWidgetTapAction
import com.fibelatti.photowidget.platform.WidgetSizeProvider
+import com.fibelatti.photowidget.platform.setIdentifierCompat
import com.fibelatti.photowidget.platform.withPolygonalShape
import com.fibelatti.photowidget.platform.withRoundedCorners
import com.fibelatti.photowidget.viewer.PhotoWidgetViewerActivity
@@ -102,13 +103,17 @@ class PhotoWidgetProvider : AppWidgetProvider() {
val entryPoint = entryPoint(context)
val coroutineScope = entryPoint.coroutineScope()
val loadPhotoWidgetUseCase = entryPoint.loadPhotoWidgetUseCase()
+ val photoWidgetStorage = entryPoint.photoWidgetStorage()
if (loadedWidgets[appWidgetId] != true) {
Timber.d("Dispatching pre-load remote views to AppWidgetManager")
- appWidgetManager.updateAppWidget(
- /* appWidgetId = */ appWidgetId,
- /* views = */ RemoteViews(context.packageName, R.layout.photo_widget_placeholder),
- )
+
+ val aspectRatio = photoWidgetStorage.getWidgetAspectRatio(appWidgetId)
+ val remoteViews = createBaseView(context = context, aspectRatio = aspectRatio).apply {
+ setImageViewResource(R.id.iv_placeholder, R.drawable.ic_hourglass)
+ }
+
+ appWidgetManager.updateAppWidget(appWidgetId, remoteViews)
loadedWidgets[appWidgetId] = true
}
@@ -154,7 +159,11 @@ class PhotoWidgetProvider : AppWidgetProvider() {
recoveryMode: Boolean = false,
): RemoteViews {
Timber.d("Creating remote views")
- val currentPhoto = photoWidget.currentPhoto ?: return createErrorView(context = context)
+ val errorView = createBaseView(context = context, aspectRatio = photoWidget.aspectRatio).apply {
+ setImageViewResource(R.id.iv_placeholder, R.drawable.ic_file_not_found)
+ }
+
+ val currentPhoto = photoWidget.currentPhoto ?: return errorView
val entryPoint = entryPoint(context)
val decoder = entryPoint.photoDecoder()
@@ -164,7 +173,7 @@ class PhotoWidgetProvider : AppWidgetProvider() {
val data = when {
!currentPhoto.path.isNullOrEmpty() -> currentPhoto.path
currentPhoto.externalUri != null -> currentPhoto.externalUri
- else -> return createErrorView(context = context)
+ else -> return errorView
}
val displayMetrics: DisplayMetrics = context.resources.displayMetrics
val maxMemoryAllowed: Int = if (!recoveryMode) {
@@ -189,7 +198,7 @@ class PhotoWidgetProvider : AppWidgetProvider() {
requireNotNull(decoder.decode(data = data, maxDimension = maxDimension))
} catch (_: Exception) {
- return createErrorView(context = context)
+ return errorView
}
Timber.d("Transforming the bitmap")
@@ -197,6 +206,8 @@ class PhotoWidgetProvider : AppWidgetProvider() {
bitmap.withPolygonalShape(
shapeId = photoWidget.shapeId,
opacity = photoWidget.opacity,
+ borderColorHex = photoWidget.borderColor,
+ borderWidth = photoWidget.borderWidth,
)
} else {
bitmap.withRoundedCorners(
@@ -207,16 +218,13 @@ class PhotoWidgetProvider : AppWidgetProvider() {
photoWidget.cornerRadius
},
opacity = photoWidget.opacity,
+ borderColorHex = photoWidget.borderColor,
+ borderWidth = photoWidget.borderWidth,
)
}
- val layoutId = if (PhotoWidgetAspectRatio.FILL_WIDGET == photoWidget.aspectRatio) {
- R.layout.photo_widget_fill
- } else {
- R.layout.photo_widget
- }
-
- return RemoteViews(context.packageName, layoutId).apply {
+ return createBaseView(context = context, aspectRatio = photoWidget.aspectRatio).apply {
+ setViewVisibility(R.id.iv_placeholder, View.GONE)
setImageViewBitmap(R.id.iv_widget, transformedBitmap)
setViewPadding(
/* viewId = */ R.id.iv_widget,
@@ -228,10 +236,14 @@ class PhotoWidgetProvider : AppWidgetProvider() {
}
}
- private fun createErrorView(context: Context): RemoteViews {
- return RemoteViews(context.packageName, R.layout.photo_widget_placeholder).apply {
- setImageViewResource(R.id.image_view_placeholder, R.drawable.ic_file_not_found)
+ private fun createBaseView(context: Context, aspectRatio: PhotoWidgetAspectRatio): RemoteViews {
+ val layoutId = if (PhotoWidgetAspectRatio.FILL_WIDGET == aspectRatio) {
+ R.layout.photo_widget_fill
+ } else {
+ R.layout.photo_widget
}
+
+ return RemoteViews(context.packageName, layoutId)
}
private fun getDimensionValue(context: Context, value: Int): Int {
@@ -306,13 +318,14 @@ class PhotoWidgetProvider : AppWidgetProvider() {
is PhotoWidgetTapAction.ViewFullScreen -> {
val clickIntent = Intent(context, PhotoWidgetViewerActivity::class.java).apply {
+ setIdentifierCompat("$appWidgetId")
this.appWidgetId = appWidgetId
}
return PendingIntent.getActivity(
- context,
- appWidgetId,
- clickIntent,
- PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_UPDATE_CURRENT,
+ /* context = */ context,
+ /* requestCode = */ appWidgetId,
+ /* intent = */ clickIntent,
+ /* flags = */ PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_UPDATE_CURRENT,
)
}
@@ -320,14 +333,15 @@ class PhotoWidgetProvider : AppWidgetProvider() {
if (externalUri == null) return null
val intent = Intent(Intent.ACTION_VIEW, externalUri).apply {
+ setIdentifierCompat("$appWidgetId")
flags = Intent.FLAG_GRANT_READ_URI_PERMISSION
}
return PendingIntent.getActivity(
- context,
- appWidgetId,
- intent,
- PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT,
+ /* context = */ context,
+ /* requestCode = */ appWidgetId,
+ /* intent = */ intent,
+ /* flags = */ PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT,
)
}
@@ -348,11 +362,26 @@ class PhotoWidgetProvider : AppWidgetProvider() {
if (launchIntent == null) return null
+ launchIntent.setIdentifierCompat("$appWidgetId")
+
+ return PendingIntent.getActivity(
+ /* context = */ context,
+ /* requestCode = */ appWidgetId,
+ /* intent = */ launchIntent,
+ /* flags = */ PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_UPDATE_CURRENT,
+ )
+ }
+
+ is PhotoWidgetTapAction.ToggleCycling -> {
+ val intent = Intent(context, ToggleCyclingFeedbackActivity::class.java).apply {
+ setIdentifierCompat("$appWidgetId")
+ this.appWidgetId = appWidgetId
+ }
return PendingIntent.getActivity(
- context,
- appWidgetId,
- launchIntent,
- PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_UPDATE_CURRENT,
+ /* context = */ context,
+ /* requestCode = */ appWidgetId,
+ /* intent = */ intent,
+ /* flags = */ PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_UPDATE_CURRENT,
)
}
}
@@ -364,6 +393,7 @@ class PhotoWidgetProvider : AppWidgetProvider() {
flipBackwards: Boolean = false,
): PendingIntent {
val intent = Intent(context, PhotoWidgetProvider::class.java).apply {
+ setIdentifierCompat("$appWidgetId")
this.appWidgetId = appWidgetId
this.action = if (flipBackwards) ACTION_VIEW_PREVIOUS_PHOTO else ACTION_VIEW_NEXT_PHOTO
}
diff --git a/app/src/main/java/com/fibelatti/photowidget/widget/PhotoWidgetRescheduleReceiver.kt b/app/src/main/java/com/fibelatti/photowidget/widget/PhotoWidgetRescheduleReceiver.kt
index 6289123..93c831a 100644
--- a/app/src/main/java/com/fibelatti/photowidget/widget/PhotoWidgetRescheduleReceiver.kt
+++ b/app/src/main/java/com/fibelatti/photowidget/widget/PhotoWidgetRescheduleReceiver.kt
@@ -33,9 +33,10 @@ class PhotoWidgetRescheduleReceiver : BroadcastReceiver() {
coroutineScope.launch {
for (id in ids) {
val cycleMode = photoWidgetStorage.getWidgetCycleMode(appWidgetId = id)
- Timber.d("Processing widget (id=$id, cycleMode=$cycleMode)")
+ val paused = photoWidgetStorage.getWidgetCyclePaused(appWidgetId = id)
+ Timber.d("Processing widget (id=$id, cycleMode=$cycleMode, paused=$paused)")
- if (cycleMode !is PhotoWidgetCycleMode.Disabled) {
+ if (cycleMode !is PhotoWidgetCycleMode.Disabled && !paused) {
photoWidgetAlarmManager.setup(appWidgetId = id)
}
diff --git a/app/src/main/java/com/fibelatti/photowidget/widget/ToggleCyclingFeedbackActivity.kt b/app/src/main/java/com/fibelatti/photowidget/widget/ToggleCyclingFeedbackActivity.kt
new file mode 100644
index 0000000..1750cb4
--- /dev/null
+++ b/app/src/main/java/com/fibelatti/photowidget/widget/ToggleCyclingFeedbackActivity.kt
@@ -0,0 +1,52 @@
+package com.fibelatti.photowidget.widget
+
+import android.os.Bundle
+import android.widget.Toast
+import androidx.appcompat.app.AppCompatActivity
+import com.fibelatti.photowidget.R
+import com.fibelatti.photowidget.configure.appWidgetId
+import com.fibelatti.photowidget.widget.data.PhotoWidgetStorage
+import dagger.hilt.android.AndroidEntryPoint
+import javax.inject.Inject
+
+/**
+ * This could have been a `BroadcastReceiver` but the action would be quite confusing without any
+ * sort of user feedback. This transparent activity does the work and shows a toast at the end to
+ * confirm.
+ */
+@AndroidEntryPoint
+class ToggleCyclingFeedbackActivity : AppCompatActivity() {
+
+ @Inject
+ lateinit var photoWidgetStorage: PhotoWidgetStorage
+
+ @Inject
+ lateinit var photoWidgetAlarmManager: PhotoWidgetAlarmManager
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ val paused = photoWidgetStorage.getWidgetCyclePaused(appWidgetId = intent.appWidgetId)
+
+ if (paused) {
+ photoWidgetAlarmManager.setup(appWidgetId = intent.appWidgetId)
+ } else {
+ photoWidgetAlarmManager.cancel(appWidgetId = intent.appWidgetId)
+ }
+
+ photoWidgetStorage.saveWidgetCyclePaused(appWidgetId = intent.appWidgetId, value = !paused)
+
+ Toast.makeText(
+ /* context = */ this,
+ /* resId = */
+ if (paused) {
+ R.string.photo_widget_cycling_feedback_resumed
+ } else {
+ R.string.photo_widget_cycling_feedback_paused
+ },
+ /* duration = */ Toast.LENGTH_SHORT,
+ ).show()
+
+ finish()
+ }
+}
diff --git a/app/src/main/java/com/fibelatti/photowidget/widget/data/PhotoWidgetInternalFileStorage.kt b/app/src/main/java/com/fibelatti/photowidget/widget/data/PhotoWidgetInternalFileStorage.kt
index 748b5ff..9138cb9 100644
--- a/app/src/main/java/com/fibelatti/photowidget/widget/data/PhotoWidgetInternalFileStorage.kt
+++ b/app/src/main/java/com/fibelatti/photowidget/widget/data/PhotoWidgetInternalFileStorage.kt
@@ -3,10 +3,11 @@ package com.fibelatti.photowidget.widget.data
import android.content.Context
import android.graphics.Bitmap
import android.net.Uri
+import android.webkit.MimeTypeMap
import com.fibelatti.photowidget.model.LocalPhoto
-import com.fibelatti.photowidget.model.PhotoWidget
import com.fibelatti.photowidget.model.PhotoWidgetSource
import com.fibelatti.photowidget.platform.PhotoDecoder
+import com.fibelatti.photowidget.preferences.UserPreferencesStorage
import dagger.hilt.android.qualifiers.ApplicationContext
import java.io.File
import java.io.FileInputStream
@@ -22,9 +23,12 @@ import timber.log.Timber
class PhotoWidgetInternalFileStorage @Inject constructor(
@ApplicationContext private val context: Context,
+ private val userPreferencesStorage: UserPreferencesStorage,
private val decoder: PhotoDecoder,
) {
+ private val contentResolver = context.contentResolver
+ private val mimeTypeMap = MimeTypeMap.getSingleton()
private val rootDir by lazy {
File("${context.filesDir}/widgets").apply {
mkdirs()
@@ -33,36 +37,43 @@ class PhotoWidgetInternalFileStorage @Inject constructor(
suspend fun newWidgetPhoto(appWidgetId: Int, source: Uri): LocalPhoto? {
return withContext(Dispatchers.IO) {
- Timber.d("New widget photo: $source (appWidgetId=$appWidgetId)")
- val widgetDir = getWidgetDir(appWidgetId = appWidgetId)
- val originalPhotosDir = File("$widgetDir/original").apply { mkdirs() }
- val newPhotoName = "${UUID.randomUUID()}.png"
+ runCatching {
+ Timber.d("New widget photo: $source (appWidgetId=$appWidgetId)")
- val originalPhoto = File("$originalPhotosDir/$newPhotoName")
- val croppedPhoto = File("$widgetDir/$newPhotoName")
+ val widgetDir = getWidgetDir(appWidgetId = appWidgetId)
+ val originalPhotosDir = File("$widgetDir/original").apply { mkdirs() }
+ val extension = mimeTypeMap.getExtensionFromMimeType(contentResolver.getType(source)) ?: "png"
+ val newPhotoName = "${UUID.randomUUID()}.$extension"
- val newFiles = listOf(originalPhoto, croppedPhoto)
+ val originalPhoto = File("$originalPhotosDir/$newPhotoName")
+ val croppedPhoto = File("$widgetDir/$newPhotoName")
- runCatching {
- decoder.decode(data = source, maxDimension = PhotoWidget.MAX_STORAGE_DIMENSION)?.let { importedPhoto ->
- newFiles.map { file ->
- file.createNewFile()
+ val newFiles = listOf(originalPhoto, croppedPhoto)
+ val dataSaver = userPreferencesStorage.dataSaver
+
+ Timber.d("Data saver: $dataSaver")
+ if (dataSaver) {
+ decoder.decode(data = source, maxDimension = 2560)?.let { importedPhoto ->
+ val format = if (extension == "png") Bitmap.CompressFormat.PNG else Bitmap.CompressFormat.JPEG
+ newFiles.map { file ->
+ async {
+ writeToFile(file) { fos -> importedPhoto.compress(format, 95, fos) }
+ }
+ }.awaitAll()
+ }
+ } else {
+ newFiles.map { file ->
async {
- FileOutputStream(file).use { fos ->
- importedPhoto.compress(Bitmap.CompressFormat.PNG, 100, fos)
- Timber.d("$source saved to $file")
+ contentResolver.openInputStream(source)?.use { input ->
+ writeToFile(file, input::copyTo)
}
}
}.awaitAll()
- } ?: return@withContext null // Exit early if the bitmap can't be decoded
+ }
- // Safety check to ensure the photos were copied correctly
return@withContext if (newFiles.all { it.exists() }) {
- LocalPhoto(
- name = newPhotoName,
- path = croppedPhoto.path,
- )
+ LocalPhoto(name = newPhotoName, path = croppedPhoto.path)
} else {
null
}
@@ -70,6 +81,18 @@ class PhotoWidgetInternalFileStorage @Inject constructor(
}
}
+ private fun writeToFile(file: File, operation: (FileOutputStream) -> Unit) {
+ file.createNewFile()
+ runCatching {
+ FileOutputStream(file).use { fos -> operation(fos) }
+ }.onSuccess {
+ Timber.d("Successfully saved to $file")
+ }.onFailure {
+ Timber.d("Failed to save to $file")
+ file.delete()
+ }
+ }
+
suspend fun getCropSources(appWidgetId: Int, localPhoto: LocalPhoto): Pair = withContext(Dispatchers.IO) {
val widgetDir = getWidgetDir(appWidgetId = appWidgetId)
val croppedPhoto = File("$widgetDir/${localPhoto.name}").apply { createNewFile() }
diff --git a/app/src/main/java/com/fibelatti/photowidget/widget/data/PhotoWidgetSharedPreferences.kt b/app/src/main/java/com/fibelatti/photowidget/widget/data/PhotoWidgetSharedPreferences.kt
index 785c488..860b93a 100644
--- a/app/src/main/java/com/fibelatti/photowidget/widget/data/PhotoWidgetSharedPreferences.kt
+++ b/app/src/main/java/com/fibelatti/photowidget/widget/data/PhotoWidgetSharedPreferences.kt
@@ -144,6 +144,16 @@ class PhotoWidgetSharedPreferences @Inject constructor(
return sharedPreferences.getLong("${PreferencePrefix.NEXT_CYCLE_TIME}$appWidgetId", -1)
}
+ fun saveWidgetCyclePaused(appWidgetId: Int, value: Boolean) {
+ sharedPreferences.edit {
+ putBoolean("${PreferencePrefix.CYCLE_PAUSED}$appWidgetId", value)
+ }
+ }
+
+ fun getWidgetCyclePaused(appWidgetId: Int): Boolean {
+ return sharedPreferences.getBoolean("${PreferencePrefix.CYCLE_PAUSED}$appWidgetId", false)
+ }
+
private fun getWidgetInterval(appWidgetId: Int): PhotoWidgetLoopingInterval {
val legacyName = sharedPreferences.getString("${PreferencePrefix.LEGACY_INTERVAL}$appWidgetId", null)
val legacyValue = enumValueOfOrNull(legacyName)
@@ -229,6 +239,26 @@ class PhotoWidgetSharedPreferences @Inject constructor(
)
}
+ fun saveWidgetBorderColor(appWidgetId: Int, colorHex: String?, width: Int) {
+ sharedPreferences.edit {
+ if (colorHex != null) {
+ putString("${PreferencePrefix.BORDER_COLOR_HEX}$appWidgetId", colorHex)
+ putInt("${PreferencePrefix.BORDER_WIDTH}$appWidgetId", width)
+ } else {
+ remove("${PreferencePrefix.BORDER_COLOR_HEX}$appWidgetId")
+ remove("${PreferencePrefix.BORDER_WIDTH}$appWidgetId")
+ }
+ }
+ }
+
+ fun getWidgetBorderColorHex(appWidgetId: Int): String? {
+ return sharedPreferences.getString("${PreferencePrefix.BORDER_COLOR_HEX}$appWidgetId", null)
+ }
+
+ fun getWidgetBorderWidth(appWidgetId: Int): Int {
+ return sharedPreferences.getInt("${PreferencePrefix.BORDER_WIDTH}$appWidgetId", 0)
+ }
+
fun saveWidgetOpacity(appWidgetId: Int, opacity: Float) {
sharedPreferences.edit {
putFloat("${PreferencePrefix.OPACITY}$appWidgetId", opacity)
@@ -363,11 +393,14 @@ class PhotoWidgetSharedPreferences @Inject constructor(
INTERVAL_ENABLED(value = "appwidget_interval_enabled_"),
SCHEDULE(value = "appwidget_schedule_"),
NEXT_CYCLE_TIME(value = "appwidget_next_cycle_time_"),
+ CYCLE_PAUSED(value = "appwidget_cycle_paused_"),
INDEX(value = "appwidget_index_"),
PAST_INDICES(value = "appwidget_past_indices_"),
RATIO(value = "appwidget_aspect_ratio_"),
SHAPE(value = "appwidget_shape_"),
CORNER_RADIUS(value = "appwidget_corner_radius_"),
+ BORDER_COLOR_HEX(value = "appwidget_border_color_hex_"),
+ BORDER_WIDTH(value = "appwidget_border_width_"),
OPACITY(value = "appwidget_opacity_"),
HORIZONTAL_OFFSET(value = "appwidget_horizontal_offset_"),
VERTICAL_OFFSET(value = "appwidget_vertical_offset_"),
diff --git a/app/src/main/java/com/fibelatti/photowidget/widget/data/PhotoWidgetStorage.kt b/app/src/main/java/com/fibelatti/photowidget/widget/data/PhotoWidgetStorage.kt
index 6b7972a..0614d5d 100644
--- a/app/src/main/java/com/fibelatti/photowidget/widget/data/PhotoWidgetStorage.kt
+++ b/app/src/main/java/com/fibelatti/photowidget/widget/data/PhotoWidgetStorage.kt
@@ -171,6 +171,14 @@ class PhotoWidgetStorage @Inject constructor(
return sharedPreferences.getWidgetNextCycleTime(appWidgetId = appWidgetId)
}
+ fun saveWidgetCyclePaused(appWidgetId: Int, value: Boolean) {
+ sharedPreferences.saveWidgetCyclePaused(appWidgetId = appWidgetId, value = value)
+ }
+
+ fun getWidgetCyclePaused(appWidgetId: Int): Boolean {
+ return sharedPreferences.getWidgetCyclePaused(appWidgetId = appWidgetId)
+ }
+
fun saveWidgetIndex(appWidgetId: Int, index: Int) {
sharedPreferences.saveWidgetIndex(appWidgetId = appWidgetId, index = index)
}
@@ -211,6 +219,18 @@ class PhotoWidgetStorage @Inject constructor(
return sharedPreferences.getWidgetCornerRadius(appWidgetId = appWidgetId)
}
+ fun saveWidgetBorderColor(appWidgetId: Int, colorHex: String?, width: Int) {
+ sharedPreferences.saveWidgetBorderColor(appWidgetId = appWidgetId, colorHex = colorHex, width = width)
+ }
+
+ fun getWidgetBorderColorHex(appWidgetId: Int): String? {
+ return sharedPreferences.getWidgetBorderColorHex(appWidgetId = appWidgetId)
+ }
+
+ fun getWidgetBorderWidth(appWidgetId: Int): Int {
+ return sharedPreferences.getWidgetBorderWidth(appWidgetId = appWidgetId)
+ }
+
fun saveWidgetOpacity(appWidgetId: Int, opacity: Float) {
sharedPreferences.saveWidgetOpacity(appWidgetId = appWidgetId, opacity = opacity)
}
diff --git a/app/src/main/res/drawable/ic_hard_drive.xml b/app/src/main/res/drawable/ic_hard_drive.xml
new file mode 100644
index 0000000..2879948
--- /dev/null
+++ b/app/src/main/res/drawable/ic_hard_drive.xml
@@ -0,0 +1,13 @@
+
+
+
diff --git a/app/src/main/res/layout/photo_widget.xml b/app/src/main/res/layout/photo_widget.xml
index a2ad24d..462ec94 100644
--- a/app/src/main/res/layout/photo_widget.xml
+++ b/app/src/main/res/layout/photo_widget.xml
@@ -8,6 +8,14 @@
tools:layout_width="400dp"
tools:targetApi="s">
+
+
+
+
-
-
-
diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml
index 4aaa8d3..b9171ad 100644
--- a/app/src/main/res/values-es/strings.xml
+++ b/app/src/main/res/values-es/strings.xml
@@ -11,7 +11,9 @@
Nuevo Widget
Mis Widgets
Ajustes
+ Almacenamiento optimizado
Tema de la aplicación
+ Fondo negro verdadero
Colores de la aplicación
Comparte la aplicación
Escribe una reseña
@@ -37,12 +39,15 @@
Los widgets en gris se eliminaron de su pantalla de inicio y se eliminarán permanentemente después de una semana
- Editar
+ Ver / Editar
Duplicar
Restaurar
Borrar permanentemente
+ Reducir el uso del almacenamiento
+ Cuando está habilitada, las fotos agregadas a los widgets se comprimen para ahorrar espacio de almacenamiento. Por lo general, esto no afectará la calidad, pero si las fotos contienen texto pequeño, se recomienda desactivarla.\n\nEsta configuración solo afecta las fotos agregadas después de que se modifique.\n\nNo se aplica a las carpetas sincronizadas, ya que esas fotos se leen directamente desde el almacenamiento de su dispositivo.
+
Seguir el sistema
Claro
Oscuro
@@ -117,7 +122,7 @@
El widget mostrará la próxima foto en cada intervalo de tiempo seleccionado:
El widget mostrará la próxima foto en cada horario seleccionado:
- El widget no mostrará la próxima foto automáticamente. Usa la acción de toque para alternar entre las fotos, o elige otro modo.
+ El widget no pasará de una foto a otra automáticamente. Toca los bordes del widget para pasar a la siguiente foto.
Cada %s
@@ -161,6 +166,10 @@
Android restringe el trabajo en segundo plano para optimizar la duración de la batería, lo que puede llevar a intervalos más largos entre cada foto.\n\nSi deseas que los widgets se actualicen precisamente en el intervalo seleccionado, abre la configuración y habilita este permiso.
Abrir configuración
+ Borde
+ Ninguno
+ Color
+
Desplazamiento
Horizontal: %1$d, Vertical: %2$d
Horizontal: %1$d
@@ -174,6 +183,8 @@
Ver foto en pantalla completa
Ver en galería
Abrir una aplicación
+ Habilitar/deshabilitar ciclo
+ Toque para pausar el ciclo automático de fotos. Toque nuevamente para reanudarlo.
Aumentar automáticamente el brillo
Ver foto original (sin recorte ni rotación)
@@ -181,6 +192,7 @@
Elige la aplicación
Agregar a la pantalla principal
+ Guardar cambios
Seleccione la hora
@@ -199,11 +211,17 @@
¿Algo más no está funcionando?
+ Escribe tu mensaje a continuación. Si estás informando un problema, intenta describir lo que estabas haciendo cuando se produjo el problema.
+
Pellizcar para hacer zoom
Arrastra para mover
Toca para cerrar
+
+ Ciclo pausado
+ Ciclo reanudado
+
Sí
No
diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml
index 2d702e2..7b2bc3c 100644
--- a/app/src/main/res/values-fr/strings.xml
+++ b/app/src/main/res/values-fr/strings.xml
@@ -11,7 +11,9 @@
Nouveau widget
Mes widgets
Paramètres
+ Stockage optimisé
Thème de l\'application
+ Fond noir véritable
Couleurs de l\'application
Partager l\'application
Écrire un avis
@@ -37,12 +39,15 @@
Les widgets grisés ont été supprimés de votre écran d\'accueil et seront définitivement supprimés après une semaine
- Modifier
+ Afficher / Modifier
Dupliquer
Restaurer
Supprimer définitivement
+ Réduire l\'utilisation du stockage
+ Lorsque cette option est activée, les photos ajoutées aux widgets sont compressées pour économiser de l\'espace de stockage. Cela n\'affecte généralement pas la qualité, mais si vos photos contiennent du texte de petite taille, il est recommandé de la désactiver.\n\nCe paramètre affecte uniquement les photos ajoutées après sa modification.\n\nIl ne s\'applique pas aux dossiers synchronisés, car ces photos sont lues directement à partir du stockage de votre appareil.
+
Suivre le système
Clair
Sombre
@@ -55,7 +60,7 @@
Ces valeurs sont préchargées lorsque vous créez un nouveau widget. Elles sont utiles pour configurer plusieurs widgets avec la même configuration, sans avoir besoin d\'ajuster les paramètres à chaque fois. Notez que toute modification de ces valeurs n\'affectera pas les widgets existants.
Source
Aléatoire
- Changement de photo
+ Cycle des photos
Forme
Arrondi
Opacité
@@ -110,14 +115,14 @@
Activé
Désactivé
- Changement de photo
+ Cycle des photos
Intervalle
Planification
Désactivé
Le widget passera automatiquement à la photo suivante à l\'intervalle suivant :
Le widget passera automatiquement à la photo suivante tous les jours aux horaires suivants :
- Le widget ne fera pas défiler les photos automatiquement. Configurez une action tactile pour faire défiler les photos ou choisissez un autre mode.
+ Le widget ne parcourt pas automatiquement les photos. Appuyez sur les bords du widget pour passer à la photo suivante.
Toutes les %s
@@ -161,6 +166,10 @@
Android limite le travail en arrière-plan pour optimiser l\'autonomie de la batterie, ce qui peut entraîner des intervalles plus longs entre chaque photo.\n\nSi vous souhaitez que les widgets se mettent à jour précisément à l\'intervalle sélectionné, ouvrez les paramètres et activez cette autorisation.
Ouvrir les paramètres
+ Bordure
+ Aucune
+ Couleur
+
Décalage
Horizontal : %1$d, Vertical : %2$d
Horizontal : %1$d
@@ -174,6 +183,8 @@
Afficher la photo en plein écran
Voir dans la galerie
Raccourci vers l\'application
+ Activer/désactiver le cycle
+ Appuyez pour mettre en pause le cycle automatique des photos. Appuyez à nouveau pour reprendre.
Augmenter automatiquement la luminosité
Voir la photo originale (sans recadrage ni rotation)
@@ -181,6 +192,7 @@
Choisir une application
Ajouter à l\'écran d\'accueil
+ Enregistrer les modifications
Sélectionnez l\'heure
@@ -199,11 +211,17 @@
Autre chose ne fonctionne pas ?
+ Rédigez votre message ci-dessous. Si vous signalez un problème, essayez de décrire ce que vous faisiez lorsque le problème est survenu.
+
Pincer pour zoomer
Faites glisser pour déplacer
Appuyez pour fermer
+
+ Cycle en pause
+ Cycle repris
+
Oui
Non
diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml
index 450639b..9cac47d 100644
--- a/app/src/main/res/values-pt/strings.xml
+++ b/app/src/main/res/values-pt/strings.xml
@@ -11,7 +11,9 @@
Novo Widget
Meus Widgets
Configurações
+ Armazenamento otimizado
Tema do app
+ Fundo preto verdadeiro
Cores do app
Compartilhe o app
Deixe sua avaliação
@@ -37,12 +39,15 @@
Os widgets acinzentados foram removidos da tela inicial e serão excluídos permanentemente após uma semana
- Editar
+ Ver / Editar
Duplicar
Restaurar
Apagar permanentemente
+ Reduzir o uso de armazenamento
+ Quando habilitado, as fotos adicionadas aos widgets são compactadas para economizar espaço de armazenamento. Isso geralmente não afeta a qualidade, mas se suas fotos contiverem texto pequeno, é recomendável desativá-lo.\n\nEsta configuração afeta apenas as fotos adicionadas após sua alteração.\n\nEla não se aplica a pastas sincronizadas, pois essas fotos são lidas diretamente do armazenamento do seu dispositivo.
+
Mesmo do sistema
Claro
Escuro
@@ -117,7 +122,7 @@
O widget irá mostrar a próxima foto a cada intervalo de tempo selecionado:
O widget irá mostrar a próxima foto a cada horário selecionado:
- O widget não irá mostrar a próxima foto automaticamente. Use a ação de toque para alternar entre as fotos, ou escolha outro modo.
+ O widget não alternará entre as fotos automaticamente. Toque nas bordas do widget para alternar para a próxima foto.
A cada %s
@@ -161,6 +166,10 @@
O Android restringe atividades em segundo plano para otimizar a duração da bateria, o que pode levar a intervalos mais longos entre cada foto.\n\nSe você deseja que os widgets sejam atualizados precisamente no intervalo selecionado, abra as configurações e habilite esta permissão.
Abrir configurações
+ Borda
+ Nenhuma
+ Cor
+
Deslocamento
Horizontal: %1$d, Vertical: %2$d
Horizontal: %1$d
@@ -174,6 +183,8 @@
Ver foto em tela cheia
Ver na galeria
Abrir um app
+ Habilitar/desabilitar ciclo
+ Toque para pausar o ciclo automático de fotos. Toque novamente para retomar.
Aumentar o brilho automaticamente
Ver foto original (sem corte ou rotação)
@@ -181,6 +192,7 @@
Escolher app
Adicionar à tela inicial
+ Salvar alterações
Escolha o horário
@@ -199,11 +211,17 @@
Encontrou algum outro problema?
+ Escreva sua mensagem abaixo. Se estiver relatando um problema, tente descrever o que estava fazendo quando o problema aconteceu.
+
Pince para ampliar
Arraste para mover
Toque para fechar
+
+ Ciclo pausado
+ Ciclo retomado
+
Sim
Não
diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml
index c70e287..3a646a1 100644
--- a/app/src/main/res/values-ru/strings.xml
+++ b/app/src/main/res/values-ru/strings.xml
@@ -11,7 +11,9 @@
Новый виджет
Мои виджеты
Настройки
+ Оптимизированное хранилище
Тема приложения
+ Настоящий черный фон
Цвета приложения
Поделиться приложением
Оставить отзыв
@@ -37,12 +39,15 @@
Затемнённые виджеты были удалены с главного экрана и будут окончательно удалены через неделю
- Редактировать
+ Просмотреть / Редактировать
Дублировать
Восстановить
Удалить навсегда
+ Уменьшить использование хранилища
+ При включении фотографии, добавленные в виджеты, сжимаются для экономии места. Обычно это не влияет на качество, но если ваши фотографии содержат мелкий текст, рекомендуется отключить эту функцию.\n\nЭта настройка влияет только на фотографии, добавленные после ее изменения.\n\nОна не применяется к синхронизированным папкам, так как эти фотографии считываются непосредственно из хранилища вашего устройства.
+
Следовать за системой
Светлая
Тёмная
@@ -117,7 +122,7 @@
Виджет будет автоматически переключаться на следующую фотографию через следующий интервал:
Виджет будет автоматически переключаться на следующую фотографию каждый день в следующие запланированные часы:
- Виджет не будет автоматически переключаться между фотографиями. Установите действие по нажатию для переключения между фотографиями или выберите другой режим.
+ Виджет не будет автоматически переключаться между фотографиями. Коснитесь краев виджета, чтобы переключиться на следующую фотографию.
Каждые %s
@@ -161,6 +166,10 @@
Android ограничивает фоновую работу для оптимизации времени работы от батареи, что может привести к более длительным интервалам между каждой фотографией.\n\nЕсли вы хотите, чтобы виджеты обновлялись точно в выбранный интервал, откройте настройки и включите это разрешение.
Открыть настройки
+ Граница
+ Нет
+ Цвет
+
Смещение
Горизонтальное: %1$d, Вертикальное: %2$d
Горизонтальное: %1$d
@@ -174,6 +183,8 @@
Просмотр фотографии на весь экран
Просмотреть в галерее
Ярлык приложения
+ Включить/выключить цикл
+ Нажмите, чтобы приостановить автоматическую смену фотографий. Нажмите еще раз, чтобы возобновить.
Автоматически увеличить яркость
Просмотреть оригинальную фотографию (без обрезки или поворота)
@@ -181,6 +192,7 @@
Выбрать приложение
Добавить на главный экран
+ Сохранить изменения
Выбрать время
@@ -199,11 +211,17 @@
Что-то еще не работает?
+ Напишите свое сообщение ниже. Если вы сообщаете о проблеме, постарайтесь описать, что вы делали, когда возникла проблема.
+
Сожмите для увеличения
Перетащите для перемещения
Нажмите, чтобы закрыть
+
+ Цикл приостановлен
+ Цикл возобновлен
+
Да
Нет
diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml
index 64d7ebb..518c67b 100644
--- a/app/src/main/res/values-tr/strings.xml
+++ b/app/src/main/res/values-tr/strings.xml
@@ -11,7 +11,9 @@
Yeni Widget
Widget\'larım
Ayarlar
+ Optimize edilmiş depolama
Uygulama teması
+ Gerçek siyah arka plan
Uygulama renkleri
Uygulamayı paylaş
Yorum yaz
@@ -36,12 +38,15 @@
Gri renkteki widget\'lar ana ekranınızdan kaldırıldı ve bir hafta sonra kalıcı olarak silinecek
- Düzenle
+ Görüntüle / Düzenle
Çoğalt
Eski haline getirmek
kalıcı olarak sil
+ Depolama kullanımını azaltın
+ Etkinleştirildiğinde, widget\'lara eklenen fotoğraflar depolama alanından tasarruf etmek için sıkıştırılır. Bu genellikle kaliteyi etkilemez, ancak fotoğraflarınız küçük metin içeriyorsa, bunu kapatmanız önerilir.\n\nBu ayar yalnızca değiştirildikten sonra eklenen fotoğrafları etkiler.\n\nSenkronize klasörler için geçerli değildir, çünkü bu fotoğraflar doğrudan cihazınızın depolama alanından okunur.
+
Sistemi takip et
Aydınlık
Karanlık
@@ -116,7 +121,7 @@
Widget, belirli aralıklarla otomatik olarak bir sonraki fotoğrafa geçecektir:
Widget, her gün belirlenen zamanlarda otomatik olarak bir sonraki fotoğrafa geçecektir:
- Widget, fotoğraflar arasında otomatik olarak geçiş yapmayacaktır. Fotoğraflar arasında geçiş yapmak için bir dokunma eylemi ayarlayın veya farklı bir mod seçin.
+ Widget fotoğraflar arasında otomatik olarak geçiş yapmaz. Bir sonraki fotoğrafa geçmek için widget kenarlarına dokunun.
Her %s
@@ -155,6 +160,10 @@
Android, pil ömrünü optimize etmek için arka plan çalışmalarını kısıtlar, bu da her fotoğraf arasındaki aralıkların daha uzun olmasına neden olabilir.\n\nWidget\'ların seçilen aralıkta tam olarak güncellenmesini istiyorsanız, ayarları açın ve bu izni etkinleştirin.
Ayarları aç
+ Kenarlık
+ Yok
+ Renk
+
Ofset
Yatay: %1$d, Dikey: %2$d
Yatay: %1$d
@@ -168,6 +177,8 @@
Fotoğrafı tam ekranda görüntüle
Galeride görüntüle
Uygulama kısayolu
+ Döngüyü etkinleştir/devre dışı bırak
+ Otomatik fotoğraf döngüsünü duraklatmak için dokunun. Devam etmek için tekrar dokunun.
Parlaklığı otomatik olarak artır
Orijinal fotoğrafı görüntüle (kırpma veya döndürme yok)
@@ -175,6 +186,7 @@
Uygulama seç
Ana ekrana ekle
+ Değişiklikleri kaydet
Zamanı seçin
@@ -193,11 +205,17 @@
Başka bir şey mi çalışmıyor?
+ Mesajınızı aşağıya yazın. Bir sorun bildiriyorsanız, sorun oluştuğunda ne yaptığınızı açıklamaya çalışın.
+
Yakınlaştırmak için kıstır
Taşımak için sürükle
Kapatmak için dokun
+
+ Duraklatılan döngü
+ Döngü devam ettirildi
+
Evet
Hayır
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 104154e..5725433 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -11,7 +11,9 @@
New Widget
My Widgets
Settings
+ Optimized storage
App theme
+ True black background
App colors
Share the app
Write a review
@@ -36,12 +38,15 @@
Grayed out widgets were removed from your home screen and will be permanently deleted after a week
- Edit
+ View / Edit
Duplicate
Restore
Delete permanently
+ Reduce storage usage
+ When enabled, photos added to widgets are compressed to save storage. This usually won\'t affect quality, but if your photos contain small text, it\'s recommended to turn this off.\n\nThis setting only affects photos added after it is changed.\n\nIt does not apply to synced folders, as those photos are read directly from your device storage.
+
Follow system
Light
Dark
@@ -116,7 +121,7 @@
The widget will switch automatically to the next photo at the following interval:
The widget will switch automatically to the next photo every day at the following scheduled times:
- The widget won\'t cycle through photos automatically. Set a tap action to cycle through photos or choose a different mode.
+ The widget won\'t cycle through photos automatically. Tap the widget edges to switch to the next photo.
Every %s
@@ -155,6 +160,10 @@
Android restricts background work to optimize battery life which can lead to longer intervals between each photo.\n\nIf you want widgets to update precisely at the selected interval open settings and enable this permission.
Open settings
+ Border
+ None
+ Color
+
Offset
Horizontal: %1$d, Vertical: %2$d
Horizontal: %1$d
@@ -168,6 +177,8 @@
View photo in full screen
View in gallery
App shortcut
+ Enable/disable cycle
+ Tap to pause the automatic photo cycling. Tap again to resume.
Automatically increase brightness
View original photo (no crop or rotation)
@@ -175,6 +186,7 @@
Choose app
Add to home screen
+ Save changes
Select time
@@ -193,11 +205,17 @@
Something else is not working?
+ Write your message below. If you\'re reporting an issue, try to describe what you were doing when the problem happened.
+
Pinch to zoom
Drag to move
Tap to dismiss
+
+ Cycle paused
+ Cycle resumed
+
Yes
No
diff --git a/fastlane/metadata/android/en-US/changelogs/1180000.txt b/fastlane/metadata/android/en-US/changelogs/1180000.txt
new file mode 100644
index 0000000..0d8ac3e
--- /dev/null
+++ b/fastlane/metadata/android/en-US/changelogs/1180000.txt
@@ -0,0 +1,6 @@
+Version 1.18.0 is now available with these updates:
+
+* New tap action: pause and resume the automatic photo cycle
+* New app theme option for using a true black background
+
+We welcome your feedback and suggestions! Visit the project page to contribute.
diff --git a/fastlane/metadata/android/en-US/changelogs/1180100.txt b/fastlane/metadata/android/en-US/changelogs/1180100.txt
new file mode 100644
index 0000000..30add0d
--- /dev/null
+++ b/fastlane/metadata/android/en-US/changelogs/1180100.txt
@@ -0,0 +1,6 @@
+Version 1.18.1 is now available with these updates:
+
+* New tap action: pause and resume the automatic photo cycle
+* New app theme option for using a true black background
+
+We welcome your feedback and suggestions! Visit the project page to contribute.
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index a40e89d..2d64e55 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -1,5 +1,5 @@
[versions]
-agp = "8.7.1"
+agp = "8.7.2"
coroutines = "1.9.0"
hilt = "2.52"
hiltExtensions = "1.2.0"
@@ -19,7 +19,7 @@ material = { module = "com.google.android.material:material", version = "1.12.0"
lifecycle-runtime-compose = { module = "androidx.lifecycle:lifecycle-runtime-compose", version = "2.8.7" }
graphics-shapes = { module = "androidx.graphics:graphics-shapes", version = "1.0.1" }
-compose-bom = { module = "androidx.compose:compose-bom", version = "2024.10.01" }
+compose-bom = { module = "androidx.compose:compose-bom", version = "2024.11.00" }
compose-runtime = { module = "androidx.compose.runtime:runtime" }
compose-material3 = { module = "androidx.compose.material3:material3" }
compose-ui = { module = "androidx.compose.ui:ui" }
@@ -36,6 +36,7 @@ room-compiler = { module = "androidx.room:room-compiler", version.ref = "room" }
ucrop = { module = "com.github.yalantis:ucrop", version = "2.2.9" }
coil = { module = "io.coil-kt:coil", version = "2.7.0" }
reorderable = { module = "sh.calvin.reorderable:reorderable", version = "2.4.0" }
+colorpicker-compose = { module = "com.github.skydoves:colorpicker-compose", version = "1.1.2" }
about-libraries = { module = "com.mikepenz:aboutlibraries-compose", version.ref = "aboutLibraries" }
@@ -48,7 +49,7 @@ android-library = { id = "com.android.library", version.ref = "agp" }
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
kotlin-parcelize = { id = "org.jetbrains.kotlin.plugin.parcelize", version.ref = "kotlin" }
compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
-ksp = { id = "com.google.devtools.ksp", version = "2.0.21-1.0.26" }
+ksp = { id = "com.google.devtools.ksp", version = "2.0.21-1.0.27" }
hilt = { id = "com.google.dagger.hilt.android", version.ref = "hilt" }
room = { id = "androidx.room", version.ref = "room" }
spotless = { id = "com.diffplug.spotless", version = "6.25.0" }
diff --git a/ui/src/main/java/com/fibelatti/ui/theme/ExtendedTheme.kt b/ui/src/main/java/com/fibelatti/ui/theme/ExtendedTheme.kt
index 5c70161..e4abf3b 100644
--- a/ui/src/main/java/com/fibelatti/ui/theme/ExtendedTheme.kt
+++ b/ui/src/main/java/com/fibelatti/ui/theme/ExtendedTheme.kt
@@ -9,21 +9,39 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.dynamicDarkColorScheme
import androidx.compose.material3.dynamicLightColorScheme
import androidx.compose.runtime.Composable
+import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
@Composable
fun ExtendedTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
dynamicColor: Boolean = true,
+ useTrueBlack: Boolean = false,
content: @Composable () -> Unit,
) {
val colorScheme: ColorScheme = when {
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
val context = LocalContext.current
- if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
+ if (darkTheme) {
+ val dynamicDarkColorScheme = dynamicDarkColorScheme(context)
+ if (useTrueBlack) {
+ dynamicDarkColorScheme.copy(background = Color.Black)
+ } else {
+ dynamicDarkColorScheme
+ }
+ } else {
+ dynamicLightColorScheme(context)
+ }
+ }
+
+ darkTheme -> {
+ if (useTrueBlack) {
+ DarkColorScheme.copy(background = Color.Black)
+ } else {
+ DarkColorScheme
+ }
}
- darkTheme -> DarkColorScheme
else -> LightColorScheme
}