From 37c6efcfa19e40991ff118d90c41966f7f276164 Mon Sep 17 00:00:00 2001
From: DeweyReed <32358486+DeweyReed@users.noreply.github.com>
Date: Mon, 29 Jan 2024 10:54:13 +0800
Subject: [PATCH 01/31] Create the image behavior
---
app-base/src/main/res/drawable/ic_image.xml | 9 ++++++++
app-base/src/main/res/values/strings.xml | 3 +++
.../key/behaviour/BehaviourTypeUtils.kt | 3 +++
.../timer/domain/entities/BehaviourEntity.kt | 23 ++++++++++++++++---
4 files changed, 35 insertions(+), 3 deletions(-)
create mode 100644 app-base/src/main/res/drawable/ic_image.xml
diff --git a/app-base/src/main/res/drawable/ic_image.xml b/app-base/src/main/res/drawable/ic_image.xml
new file mode 100644
index 00000000..b9710395
--- /dev/null
+++ b/app-base/src/main/res/drawable/ic_image.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app-base/src/main/res/values/strings.xml b/app-base/src/main/res/values/strings.xml
index 014fa5d6..cb347eea 100644
--- a/app-base/src/main/res/values/strings.xml
+++ b/app-base/src/main/res/values/strings.xml
@@ -279,6 +279,9 @@
Flashlight
Turn the flashlight on and off
+ Image
+ Attach an image to the step
+
Timer Template
Prepare
diff --git a/component-key/src/main/java/xyz/aprildown/timer/component/key/behaviour/BehaviourTypeUtils.kt b/component-key/src/main/java/xyz/aprildown/timer/component/key/behaviour/BehaviourTypeUtils.kt
index 3ed0c745..adbf16e3 100644
--- a/component-key/src/main/java/xyz/aprildown/timer/component/key/behaviour/BehaviourTypeUtils.kt
+++ b/component-key/src/main/java/xyz/aprildown/timer/component/key/behaviour/BehaviourTypeUtils.kt
@@ -18,6 +18,7 @@ val BehaviourType.iconRes: Int
BehaviourType.COUNT -> RBase.drawable.ic_count
BehaviourType.NOTIFICATION -> RBase.drawable.ic_notification
BehaviourType.FLASHLIGHT -> RBase.drawable.ic_flashlight
+ BehaviourType.IMAGE -> RBase.drawable.ic_image
}
val BehaviourType.nameRes: Int
@@ -33,6 +34,7 @@ val BehaviourType.nameRes: Int
BehaviourType.COUNT -> RBase.string.behaviour_count
BehaviourType.NOTIFICATION -> RBase.string.behaviour_notification
BehaviourType.FLASHLIGHT -> RBase.string.behaviour_flashlight
+ BehaviourType.IMAGE -> RBase.string.behaviour_image
}
val BehaviourType.despRes: Int
@@ -48,4 +50,5 @@ val BehaviourType.despRes: Int
BehaviourType.COUNT -> RBase.string.behaviour_count_help
BehaviourType.NOTIFICATION -> RBase.string.behaviour_notification_help
BehaviourType.FLASHLIGHT -> RBase.string.behaviour_flashlight_help
+ BehaviourType.IMAGE -> RBase.string.behaviour_image_help
}
diff --git a/domain/src/main/java/xyz/aprildown/timer/domain/entities/BehaviourEntity.kt b/domain/src/main/java/xyz/aprildown/timer/domain/entities/BehaviourEntity.kt
index 84efdf09..7f41ad47 100644
--- a/domain/src/main/java/xyz/aprildown/timer/domain/entities/BehaviourEntity.kt
+++ b/domain/src/main/java/xyz/aprildown/timer/domain/entities/BehaviourEntity.kt
@@ -24,9 +24,11 @@ package xyz.aprildown.timer.domain.entities
* str1: last n seconds, 0 for always
* FLASHLIGHT:
* empty for now
+ * IMAGE:
+ * str1: The image path
*/
enum class BehaviourType {
- MUSIC, VIBRATION, SCREEN, HALT, VOICE, BEEP, HALF, COUNT, NOTIFICATION, FLASHLIGHT;
+ MUSIC, VIBRATION, SCREEN, HALT, VOICE, BEEP, HALF, COUNT, NOTIFICATION, FLASHLIGHT, IMAGE;
val hasBoolValue: Boolean
get() = this == MUSIC || this == BEEP
@@ -358,7 +360,7 @@ fun BehaviourEntity.toNotificationAction(): NotificationAction {
// endregion Notification
-// region Notification
+// region Flashlight
data class FlashlightAction(val step: Long = 500L) : Action {
override fun toBehaviourEntity(): BehaviourEntity {
@@ -371,4 +373,19 @@ fun BehaviourEntity.toFlashlightAction(): FlashlightAction {
return FlashlightAction()
}
-// endregion Notification
+// endregion Flashlight
+
+// region Image
+
+data class ImageAction(val path: String) : Action {
+ override fun toBehaviourEntity(): BehaviourEntity {
+ return BehaviourEntity(type = BehaviourType.IMAGE, str1 = path)
+ }
+}
+
+fun BehaviourEntity.toImageAction(): ImageAction {
+ require(type == BehaviourType.IMAGE)
+ return ImageAction(path = str1)
+}
+
+// endregion Image
From cda1ae2eb6d15746ebea9ff2becf8a376134c406 Mon Sep 17 00:00:00 2001
From: DeweyReed <32358486+DeweyReed@users.noreply.github.com>
Date: Mon, 5 Feb 2024 08:29:26 +0800
Subject: [PATCH 02/31] Pick an image for a step
---
app-base/src/main/res/values/strings.xml | 2 +
.../app/timer/edit/BehaviourSettingsView.kt | 12 ++++
.../timer/app/timer/edit/EditActivity.kt | 38 +++++++++++-
.../timer/app/timer/edit/EditableStep.kt | 7 +++
component-key/build.gradle | 1 +
.../key/behaviour/EditableBehaviourLayout.kt | 58 ++++++++++++++-----
.../res/layout/layout_editable_behaviour.xml | 10 +++-
.../layout_editable_behaviour_image.xml | 8 +++
gradle/libs.versions.toml | 1 +
.../timer/presentation/edit/EditViewModel.kt | 11 ++++
.../presentation/edit/EditViewModelTest.kt | 1 +
11 files changed, 133 insertions(+), 16 deletions(-)
create mode 100644 component-key/src/main/res/layout/layout_editable_behaviour_image.xml
diff --git a/app-base/src/main/res/values/strings.xml b/app-base/src/main/res/values/strings.xml
index cb347eea..3c6aa86b 100644
--- a/app-base/src/main/res/values/strings.xml
+++ b/app-base/src/main/res/values/strings.xml
@@ -282,6 +282,8 @@
Image
Attach an image to the step
+ Pick an image
+
Timer Template
Prepare
diff --git a/app-timer-edit/src/main/java/xyz/aprildown/timer/app/timer/edit/BehaviourSettingsView.kt b/app-timer-edit/src/main/java/xyz/aprildown/timer/app/timer/edit/BehaviourSettingsView.kt
index ce561ab7..284c4e00 100644
--- a/app-timer-edit/src/main/java/xyz/aprildown/timer/app/timer/edit/BehaviourSettingsView.kt
+++ b/app-timer-edit/src/main/java/xyz/aprildown/timer/app/timer/edit/BehaviourSettingsView.kt
@@ -295,3 +295,15 @@ internal fun MaterialPopupMenuBuilder.addNotificationItems(
}
}
}
+
+internal fun MaterialPopupMenuBuilder.addImageItems(
+ context: Context,
+ onPick: () -> Unit,
+) {
+ section {
+ item {
+ label = context.getString(RBase.string.image_pick)
+ callback = onPick
+ }
+ }
+}
diff --git a/app-timer-edit/src/main/java/xyz/aprildown/timer/app/timer/edit/EditActivity.kt b/app-timer-edit/src/main/java/xyz/aprildown/timer/app/timer/edit/EditActivity.kt
index dfae6215..c8f43245 100644
--- a/app-timer-edit/src/main/java/xyz/aprildown/timer/app/timer/edit/EditActivity.kt
+++ b/app-timer-edit/src/main/java/xyz/aprildown/timer/app/timer/edit/EditActivity.kt
@@ -12,6 +12,7 @@ import android.view.View
import androidx.activity.addCallback
import androidx.activity.result.ActivityResult
import androidx.activity.result.ActivityResultCallback
+import androidx.activity.result.PickVisualMediaRequest
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels
import androidx.annotation.VisibleForTesting
@@ -59,6 +60,7 @@ import xyz.aprildown.timer.domain.entities.TimerEntity
import xyz.aprildown.timer.domain.entities.toBeepAction
import xyz.aprildown.timer.domain.entities.toCountAction
import xyz.aprildown.timer.domain.entities.toHalfAction
+import xyz.aprildown.timer.domain.entities.toImageAction
import xyz.aprildown.timer.domain.entities.toMusicAction
import xyz.aprildown.timer.domain.entities.toNotificationAction
import xyz.aprildown.timer.domain.entities.toScreenAction
@@ -95,6 +97,18 @@ class EditActivity :
getRingtonePickerResultCallback()
)
+ private val pickImageLauncher = registerForActivityResult(
+ ActivityResultContracts.PickVisualMedia()
+ ) { uri ->
+ if (uri == null || uri == Uri.EMPTY) return@registerForActivityResult
+ val position = viewModel.imagePosition
+ if (position == -1) return@registerForActivityResult
+ changeBehaviour(type = BehaviourType.IMAGE, position = position) {
+ it.toImageAction().copy(path = uri.toString()).toBehaviourEntity()
+ }
+ viewModel.imagePosition = -1
+ }
+
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityEditTimerBinding.inflate(layoutInflater)
@@ -759,6 +773,12 @@ class EditActivity :
}
)
}
+ BehaviourType.IMAGE -> {
+ addImageItems(
+ context = this@EditActivity,
+ onPick = { onImageClick(position) },
+ )
+ }
else -> Unit
}
section {
@@ -802,8 +822,17 @@ class EditActivity :
) {
val step = getStepFromFastAdapter(position)
val newBehaviours = mutableListOf()
+ var found = false
step.behaviour.forEach {
- newBehaviours.add(if (it.type == type) transform(it) else it)
+ if (it.type == type) {
+ newBehaviours += transform(it)
+ found = true
+ } else {
+ newBehaviours += it
+ }
+ }
+ if (!found) {
+ newBehaviours += transform(BehaviourEntity(type))
}
// The code after this causes setBehaviors which doesn't call onBehaviourAddedOrRemoved
// so we check here.
@@ -820,6 +849,13 @@ class EditActivity :
}
}
+ override fun onImageClick(position: Int) {
+ viewModel.imagePosition = position
+ pickImageLauncher.launch(
+ PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly)
+ )
+ }
+
private fun postUpdateTotalTime() {
if (shouldUpdateTotalTime) {
binding.viewEditStepInfo.run {
diff --git a/app-timer-edit/src/main/java/xyz/aprildown/timer/app/timer/edit/EditableStep.kt b/app-timer-edit/src/main/java/xyz/aprildown/timer/app/timer/edit/EditableStep.kt
index 0de22965..8d0809c6 100644
--- a/app-timer-edit/src/main/java/xyz/aprildown/timer/app/timer/edit/EditableStep.kt
+++ b/app-timer-edit/src/main/java/xyz/aprildown/timer/app/timer/edit/EditableStep.kt
@@ -58,6 +58,8 @@ class EditableStep(
position: Int
)
+ fun onImageClick(position: Int)
+
fun onBehaviourAddedOrRemoved(position: Int, newBehaviours: List)
}
@@ -118,6 +120,11 @@ class EditableStep(
bindingAdapterPosition
)
}
+
+ override fun onImageClick() {
+ super.onImageClick()
+ handler.onImageClick(bindingAdapterPosition)
+ }
}
)
}
diff --git a/component-key/build.gradle b/component-key/build.gradle
index a41957d5..ddd5f78c 100644
--- a/component-key/build.gradle
+++ b/component-key/build.gradle
@@ -17,5 +17,6 @@ dependencies {
implementation libs.hmsPicker
implementation libs.materialPopupMenu
implementation libs.scrollHmsPicker
+ implementation libs.glide
}
diff --git a/component-key/src/main/java/xyz/aprildown/timer/component/key/behaviour/EditableBehaviourLayout.kt b/component-key/src/main/java/xyz/aprildown/timer/component/key/behaviour/EditableBehaviourLayout.kt
index 71bb8919..38c40d0d 100644
--- a/component-key/src/main/java/xyz/aprildown/timer/component/key/behaviour/EditableBehaviourLayout.kt
+++ b/component-key/src/main/java/xyz/aprildown/timer/component/key/behaviour/EditableBehaviourLayout.kt
@@ -4,25 +4,29 @@ import android.content.Context
import android.graphics.Color
import android.util.AttributeSet
import android.view.Gravity
+import android.view.LayoutInflater
import android.view.View
import android.view.View.OnClickListener
import android.widget.ImageButton
-import android.widget.TextView
import androidx.annotation.ColorInt
import androidx.appcompat.widget.TooltipCompat
import androidx.appcompat.widget.TooltipCompatFix
+import androidx.core.view.isVisible
+import com.bumptech.glide.Glide
+import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions
import com.github.deweyreed.tools.helper.drawable
-import com.github.deweyreed.tools.helper.gone
import com.github.deweyreed.tools.helper.setTextIfChanged
-import com.github.deweyreed.tools.helper.show
import com.github.deweyreed.tools.helper.toColorStateList
import com.github.zawadz88.materialpopupmenu.popupMenu
import com.google.android.flexbox.FlexWrap
import com.google.android.flexbox.FlexboxLayout
import com.google.android.material.chip.Chip
import xyz.aprildown.timer.component.key.R
+import xyz.aprildown.timer.component.key.databinding.LayoutEditableBehaviourBinding
+import xyz.aprildown.timer.component.key.databinding.LayoutEditableBehaviourImageBinding
import xyz.aprildown.timer.domain.entities.BehaviourEntity
import xyz.aprildown.timer.domain.entities.BehaviourType
+import xyz.aprildown.timer.domain.entities.toImageAction
class EditableBehaviourLayout(
context: Context,
@@ -36,8 +40,13 @@ class EditableBehaviourLayout(
layout: EditableBehaviourLayout,
current: BehaviourEntity
)
+
+ fun onImageClick(): Unit = Unit
}
+ private val binding =
+ LayoutEditableBehaviourBinding.inflate(LayoutInflater.from(context), this)
+
@ColorInt
private var colorEnabled: Int = Color.RED
@@ -45,8 +54,7 @@ class EditableBehaviourLayout(
private var listener: Listener? = null
private var onBehaviourAddedOrRemovedCallback: (() -> Unit)? = null
- val addButton: ImageButton
- private val emptyText: TextView
+ val addButton: ImageButton get() = binding.btnBehaviourAdd
private val enabledBehaviourTypes = BehaviourType.entries
@@ -55,11 +63,7 @@ class EditableBehaviourLayout(
setShowDivider(SHOW_DIVIDER_MIDDLE)
setDividerDrawable(context.drawable(R.drawable.divider_normal_flexbox))
- View.inflate(context, R.layout.layout_editable_behaviour, this)
-
- emptyText = findViewById(R.id.textBehaviourEmpty)
- addButton = findViewById(R.id.btnBehaviourAdd)
- addButton.setOnClickListener { view ->
+ binding.btnBehaviourAdd.setOnClickListener { view ->
val currentTypes = data.keys
val showTypes = enabledBehaviourTypes.filter { type -> type !in currentTypes }
if (showTypes.isNotEmpty()) {
@@ -77,7 +81,11 @@ class EditableBehaviourLayout(
)
}
callback = {
- addStubBehaviour(BehaviourEntity(type), true)
+ if (type == BehaviourType.IMAGE) {
+ listener?.onImageClick()
+ } else {
+ addStubBehaviour(BehaviourEntity(type), true)
+ }
}
}
}
@@ -115,6 +123,24 @@ class EditableBehaviourLayout(
addStubBehaviour(it, false)
setViewWithEntity(it)
}
+
+ val imageAction = bs.find { it.type == BehaviourType.IMAGE }?.toImageAction()
+ if (imageAction != null) {
+ val imageView = if (binding.layoutImage.childCount == 0) {
+ LayoutEditableBehaviourImageBinding.inflate(
+ LayoutInflater.from(context), binding.layoutImage, true
+ ).root
+ } else {
+ LayoutEditableBehaviourImageBinding.bind(binding.layoutImage).root
+ }
+ Glide.with(imageView)
+ .load(imageAction.path)
+ .transition(DrawableTransitionOptions.withCrossFade())
+ .into(imageView)
+ } else {
+ binding.layoutImage.removeAllViews()
+ }
+
updateHelperViews()
}
@@ -125,7 +151,7 @@ class EditableBehaviourLayout(
val type = behaviour.type
removeBehaviour(type)
val newView = generateBehaviourView(type)
- addView(newView, childCount - 1)
+ addView(newView, indexOfChild(binding.layoutBottomBar))
data[type] = newView to behaviour
if (fromUser) {
@@ -141,6 +167,10 @@ class EditableBehaviourLayout(
data.remove(behaviourType)
updateHelperViews()
+ if (behaviourType == BehaviourType.IMAGE) {
+ binding.layoutImage.removeAllViews()
+ }
+
onBehaviourAddedOrRemovedCallback?.invoke()
}
@@ -167,8 +197,8 @@ class EditableBehaviourLayout(
private fun updateHelperViews() {
val dataSize = data.size
- if (dataSize >= enabledBehaviourTypes.size) addButton.gone() else addButton.show()
- if (dataSize == 0) emptyText.show() else emptyText.gone()
+ binding.btnBehaviourAdd.isVisible = dataSize < enabledBehaviourTypes.size
+ binding.textBehaviourEmpty.isVisible = dataSize == 0
}
private fun createNewBehaviourChip(
diff --git a/component-key/src/main/res/layout/layout_editable_behaviour.xml b/component-key/src/main/res/layout/layout_editable_behaviour.xml
index 70a00262..bd0b50c4 100644
--- a/component-key/src/main/res/layout/layout_editable_behaviour.xml
+++ b/component-key/src/main/res/layout/layout_editable_behaviour.xml
@@ -10,6 +10,7 @@
tools:showDivider="middle">
@@ -36,4 +37,11 @@
-
\ No newline at end of file
+
+
+
diff --git a/component-key/src/main/res/layout/layout_editable_behaviour_image.xml b/component-key/src/main/res/layout/layout_editable_behaviour_image.xml
new file mode 100644
index 00000000..43e6d1a5
--- /dev/null
+++ b/component-key/src/main/res/layout/layout_editable_behaviour_image.xml
@@ -0,0 +1,8 @@
+
+
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 8239e4f6..6a15f268 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -132,6 +132,7 @@ ultimateRingtonePicker = "com.github.DeweyReed:UltimateRingtonePicker:3.3.0"
verticalStepperForm = "com.ernestoyaquello.stepperform:vertical-stepper-form:2.5.0"
zip4j = "net.lingala.zip4j:zip4j:2.11.5"
taskerPlugin = "com.joaomgcd:taskerpluginlibrary:0.4.10"
+glide = "com.github.bumptech.glide:glide:4.16.0"
diskLruCache = "com.github.bumptech.glide:disklrucache:4.16.0"
[bundles]
diff --git a/presentation/src/main/java/xyz/aprildown/timer/presentation/edit/EditViewModel.kt b/presentation/src/main/java/xyz/aprildown/timer/presentation/edit/EditViewModel.kt
index 2e861296..c51087d5 100644
--- a/presentation/src/main/java/xyz/aprildown/timer/presentation/edit/EditViewModel.kt
+++ b/presentation/src/main/java/xyz/aprildown/timer/presentation/edit/EditViewModel.kt
@@ -2,6 +2,7 @@ package xyz.aprildown.timer.presentation.edit
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.viewModelScope
import com.github.deweyreed.tools.arch.Event
import dagger.Lazy
@@ -41,6 +42,8 @@ import javax.inject.Named
*/
@HiltViewModel
class EditViewModel @Inject constructor(
+ private val savedStateHandle: SavedStateHandle,
+
@MainDispatcher mainDispatcher: CoroutineDispatcher,
private val addTimer: AddTimer,
private val saveTimer: SaveTimer,
@@ -56,6 +59,12 @@ class EditViewModel @Inject constructor(
private var timerHash: Int = 0
+ var imagePosition: Int
+ get() = savedStateHandle[KEY_IMAGE_POSITION] ?: -1
+ set(value) {
+ savedStateHandle[KEY_IMAGE_POSITION] = value
+ }
+
// Updated instantly
val name = MutableLiveData()
val loop = MutableLiveData()
@@ -287,5 +296,7 @@ class EditViewModel @Inject constructor(
const val UPDATE_CREATE = 0
const val UPDATE_UPDATE = 1
const val UPDATE_DELETE = 2
+
+ private const val KEY_IMAGE_POSITION = "image_position"
}
}
diff --git a/presentation/src/test/java/xyz/aprildown/timer/presentation/edit/EditViewModelTest.kt b/presentation/src/test/java/xyz/aprildown/timer/presentation/edit/EditViewModelTest.kt
index 77dcd9a5..4ce21811 100644
--- a/presentation/src/test/java/xyz/aprildown/timer/presentation/edit/EditViewModelTest.kt
+++ b/presentation/src/test/java/xyz/aprildown/timer/presentation/edit/EditViewModelTest.kt
@@ -58,6 +58,7 @@ class EditViewModelTest {
private fun TestScope.getViewModel(): EditViewModel {
val viewModel = EditViewModel(
+ savedStateHandle = mock(),
mainDispatcher = StandardTestDispatcher(testScheduler),
addTimer = addTimer,
saveTimer = saveTimer,
From 37c50b8e037abe0d4dd5a455a7addbcc71382bf7 Mon Sep 17 00:00:00 2001
From: DeweyReed <32358486+DeweyReed@users.noreply.github.com>
Date: Mon, 5 Feb 2024 09:08:38 +0800
Subject: [PATCH 03/31] Show the image in the list
---
.../timer/app/timer/edit/EditActivity.kt | 2 +
.../timer/app/timer/one/step/VisibleStep.kt | 3 +-
.../key/behaviour/BehaviourLayout.kt | 43 +++++++++++++++----
.../key/behaviour/EditableBehaviourLayout.kt | 5 ++-
.../src/main/res/layout/layout_behaviour.xml | 22 ++++++++++
.../layout_editable_behaviour_image.xml | 1 -
6 files changed, 63 insertions(+), 13 deletions(-)
create mode 100644 component-key/src/main/res/layout/layout_behaviour.xml
diff --git a/app-timer-edit/src/main/java/xyz/aprildown/timer/app/timer/edit/EditActivity.kt b/app-timer-edit/src/main/java/xyz/aprildown/timer/app/timer/edit/EditActivity.kt
index c8f43245..e50e4193 100644
--- a/app-timer-edit/src/main/java/xyz/aprildown/timer/app/timer/edit/EditActivity.kt
+++ b/app-timer-edit/src/main/java/xyz/aprildown/timer/app/timer/edit/EditActivity.kt
@@ -2,6 +2,7 @@ package xyz.aprildown.timer.app.timer.edit
import android.app.Activity
import android.content.ClipboardManager
+import android.content.Intent
import android.graphics.Typeface
import android.net.Uri
import android.os.Bundle
@@ -103,6 +104,7 @@ class EditActivity :
if (uri == null || uri == Uri.EMPTY) return@registerForActivityResult
val position = viewModel.imagePosition
if (position == -1) return@registerForActivityResult
+ contentResolver.takePersistableUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION)
changeBehaviour(type = BehaviourType.IMAGE, position = position) {
it.toImageAction().copy(path = uri.toString()).toBehaviourEntity()
}
diff --git a/app-timer-one/src/main/java/xyz/aprildown/timer/app/timer/one/step/VisibleStep.kt b/app-timer-one/src/main/java/xyz/aprildown/timer/app/timer/one/step/VisibleStep.kt
index d4a385d5..891ec60e 100644
--- a/app-timer-one/src/main/java/xyz/aprildown/timer/app/timer/one/step/VisibleStep.kt
+++ b/app-timer-one/src/main/java/xyz/aprildown/timer/app/timer/one/step/VisibleStep.kt
@@ -10,7 +10,6 @@ import androidx.core.widget.ImageViewCompat
import androidx.core.widget.TextViewCompat
import androidx.recyclerview.widget.RecyclerView
import com.github.deweyreed.tools.helper.attachToView
-import com.github.deweyreed.tools.helper.gone
import com.github.deweyreed.tools.helper.onLongClick
import com.github.deweyreed.tools.helper.toColorStateList
import com.mikepenz.fastadapter.items.AbstractItem
@@ -80,7 +79,7 @@ internal class VisibleStep(
binding.textTime,
R.style.TextAppearance_Stepper_NonSelected
)
- binding.layoutBehaviour.gone()
+ binding.layoutBehaviour.isVisible = false
}
val typeColor = step.type.getTypeColor(context)
diff --git a/component-key/src/main/java/xyz/aprildown/timer/component/key/behaviour/BehaviourLayout.kt b/component-key/src/main/java/xyz/aprildown/timer/component/key/behaviour/BehaviourLayout.kt
index 50f9990b..b59f0f6e 100644
--- a/component-key/src/main/java/xyz/aprildown/timer/component/key/behaviour/BehaviourLayout.kt
+++ b/component-key/src/main/java/xyz/aprildown/timer/component/key/behaviour/BehaviourLayout.kt
@@ -3,27 +3,35 @@ package xyz.aprildown.timer.component.key.behaviour
import android.content.Context
import android.content.res.ColorStateList
import android.util.AttributeSet
+import android.view.LayoutInflater
import android.view.View
+import android.widget.LinearLayout
import androidx.annotation.ColorInt
import androidx.appcompat.widget.TooltipCompat
-import com.github.deweyreed.tools.anko.dp
+import androidx.core.view.isEmpty
+import androidx.core.view.isVisible
+import com.bumptech.glide.Glide
+import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions
import com.github.deweyreed.tools.helper.setTextIfChanged
import com.google.android.material.chip.Chip
-import com.google.android.material.chip.ChipGroup
import xyz.aprildown.timer.component.key.R
+import xyz.aprildown.timer.component.key.databinding.LayoutBehaviourBinding
+import xyz.aprildown.timer.component.key.databinding.LayoutEditableBehaviourImageBinding
import xyz.aprildown.timer.domain.entities.BehaviourEntity
+import xyz.aprildown.timer.domain.entities.BehaviourType
+import xyz.aprildown.timer.domain.entities.toImageAction
class BehaviourLayout @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null
-) : ChipGroup(context, attrs) {
+) : LinearLayout(context, attrs) {
+
+ private val binding = LayoutBehaviourBinding.inflate(LayoutInflater.from(context), this)
private val currentBehaviours = mutableListOf()
init {
- val spacing = context.dp(4).toInt()
- chipSpacingHorizontal = spacing
- chipSpacingVertical = spacing
+ orientation = VERTICAL
}
fun setBehaviours(list: List) {
@@ -34,7 +42,7 @@ class BehaviourLayout @JvmOverloads constructor(
repeat(removeCount) {
val index = currentSize - it - 1
val chipView = currentBehaviours[index]
- removeView(chipView.chip)
+ binding.chipGroup.removeView(chipView.chip)
currentBehaviours.remove(chipView)
}
}
@@ -50,6 +58,25 @@ class BehaviourLayout @JvmOverloads constructor(
}
require(currentBehaviours.size == list.size)
+
+ val imageBehaviour = list.find { it.type == BehaviourType.IMAGE }
+ if (imageBehaviour != null) {
+ binding.layoutImage.isVisible = true
+ val imageView = if (binding.layoutImage.isEmpty()) {
+ LayoutEditableBehaviourImageBinding.inflate(
+ LayoutInflater.from(context), binding.layoutImage, true
+ ).root
+ } else {
+ LayoutEditableBehaviourImageBinding.bind(binding.layoutImage.getChildAt(0)).root
+ }
+ Glide.with(imageView)
+ .load(imageBehaviour.toImageAction().path)
+ .transition(DrawableTransitionOptions.withCrossFade())
+ .into(imageView)
+ } else {
+ binding.layoutImage.isVisible = false
+ binding.layoutImage.removeAllViews()
+ }
}
fun setEnabledColor(@ColorInt color: Int) {
@@ -60,7 +87,7 @@ class BehaviourLayout @JvmOverloads constructor(
private fun createNewChip(): Chip {
val chip = View.inflate(context, R.layout.view_behavior_chip, null) as Chip
- addView(chip)
+ binding.chipGroup.addView(chip)
return chip
}
diff --git a/component-key/src/main/java/xyz/aprildown/timer/component/key/behaviour/EditableBehaviourLayout.kt b/component-key/src/main/java/xyz/aprildown/timer/component/key/behaviour/EditableBehaviourLayout.kt
index 38c40d0d..e0ed864a 100644
--- a/component-key/src/main/java/xyz/aprildown/timer/component/key/behaviour/EditableBehaviourLayout.kt
+++ b/component-key/src/main/java/xyz/aprildown/timer/component/key/behaviour/EditableBehaviourLayout.kt
@@ -11,6 +11,7 @@ import android.widget.ImageButton
import androidx.annotation.ColorInt
import androidx.appcompat.widget.TooltipCompat
import androidx.appcompat.widget.TooltipCompatFix
+import androidx.core.view.isEmpty
import androidx.core.view.isVisible
import com.bumptech.glide.Glide
import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions
@@ -126,12 +127,12 @@ class EditableBehaviourLayout(
val imageAction = bs.find { it.type == BehaviourType.IMAGE }?.toImageAction()
if (imageAction != null) {
- val imageView = if (binding.layoutImage.childCount == 0) {
+ val imageView = if (binding.layoutImage.isEmpty()) {
LayoutEditableBehaviourImageBinding.inflate(
LayoutInflater.from(context), binding.layoutImage, true
).root
} else {
- LayoutEditableBehaviourImageBinding.bind(binding.layoutImage).root
+ LayoutEditableBehaviourImageBinding.bind(binding.layoutImage.getChildAt(0)).root
}
Glide.with(imageView)
.load(imageAction.path)
diff --git a/component-key/src/main/res/layout/layout_behaviour.xml b/component-key/src/main/res/layout/layout_behaviour.xml
new file mode 100644
index 00000000..5936d321
--- /dev/null
+++ b/component-key/src/main/res/layout/layout_behaviour.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+
diff --git a/component-key/src/main/res/layout/layout_editable_behaviour_image.xml b/component-key/src/main/res/layout/layout_editable_behaviour_image.xml
index 43e6d1a5..2abe05ab 100644
--- a/component-key/src/main/res/layout/layout_editable_behaviour_image.xml
+++ b/component-key/src/main/res/layout/layout_editable_behaviour_image.xml
@@ -4,5 +4,4 @@
android:layout_width="match_parent"
android:layout_height="256dp"
android:importantForAccessibility="no"
- android:scaleType="fitCenter"
tools:src="@tools:sample/backgrounds/scenic" />
From c1962985282cccef2c9edc764b9fe69143519319 Mon Sep 17 00:00:00 2001
From: DeweyReed <32358486+DeweyReed@users.noreply.github.com>
Date: Wed, 7 Feb 2024 10:14:31 +0800
Subject: [PATCH 04/31] Create ImagePreviewActivity with Jetpack Compose
---
.../timer/app/base/ui/AppNavigator.kt | 1 +
.../deweyreed/timer/ui/AppNavigatorImpl.kt | 5 ++++
component-key/build.gradle | 11 +++++++
component-key/src/main/AndroidManifest.xml | 4 ++-
.../component/key/ImagePreviewActivity.kt | 30 +++++++++++++++++++
gradle/libs.versions.toml | 16 +++++++++-
6 files changed, 65 insertions(+), 2 deletions(-)
create mode 100644 component-key/src/main/java/xyz/aprildown/timer/component/key/ImagePreviewActivity.kt
diff --git a/app-base/src/main/java/xyz/aprildown/timer/app/base/ui/AppNavigator.kt b/app-base/src/main/java/xyz/aprildown/timer/app/base/ui/AppNavigator.kt
index 31e022c2..42281047 100644
--- a/app-base/src/main/java/xyz/aprildown/timer/app/base/ui/AppNavigator.kt
+++ b/app-base/src/main/java/xyz/aprildown/timer/app/base/ui/AppNavigator.kt
@@ -18,6 +18,7 @@ interface AppNavigator {
fun getStartTimerShortcutIntent(timerId: Int, openOnClick: Boolean): Intent
fun getShortcutCreatedIntent(): Intent
fun getIntroIntent(isOnBoarding: Boolean = false): Intent
+ fun getImagePreviewIntent(path: String): Intent
/**
* @param [timerInfo] is always not empty.
diff --git a/app/src/main/java/io/github/deweyreed/timer/ui/AppNavigatorImpl.kt b/app/src/main/java/io/github/deweyreed/timer/ui/AppNavigatorImpl.kt
index 83aa5d87..a9fa4247 100644
--- a/app/src/main/java/io/github/deweyreed/timer/ui/AppNavigatorImpl.kt
+++ b/app/src/main/java/io/github/deweyreed/timer/ui/AppNavigatorImpl.kt
@@ -11,6 +11,7 @@ import xyz.aprildown.timer.app.timer.edit.EditActivity
import xyz.aprildown.timer.app.timer.list.TimerPicker
import xyz.aprildown.timer.app.timer.one.OneActivity
import xyz.aprildown.timer.app.timer.run.PhantomActivity
+import xyz.aprildown.timer.component.key.ImagePreviewActivity
import xyz.aprildown.timer.domain.utils.Constants
import javax.inject.Inject
@@ -54,6 +55,10 @@ class AppNavigatorImpl @Inject constructor(
return IntroActivity.getIntent(context, isOnBoarding)
}
+ override fun getImagePreviewIntent(path: String): Intent {
+ return ImagePreviewActivity.getIntent(context, path)
+ }
+
override fun pickTimer(
fm: FragmentManager,
multiple: Boolean,
diff --git a/component-key/build.gradle b/component-key/build.gradle
index ddd5f78c..3f6b4de9 100644
--- a/component-key/build.gradle
+++ b/component-key/build.gradle
@@ -6,6 +6,12 @@ plugins {
android {
namespace 'xyz.aprildown.timer.component.key'
+ buildFeatures {
+ compose true
+ }
+ composeOptions {
+ kotlinCompilerExtensionVersion = libs.versions.composeCompiler.get()
+ }
}
dependencies {
@@ -13,6 +19,11 @@ dependencies {
implementation libs.androidx.preference
+ implementation platform(libs.compose.bom)
+ implementation libs.bundles.compose
+ implementation libs.compose.tooling.preview
+ debugImplementation libs.compose.tooling.debug
+
implementation libs.flexbox
implementation libs.hmsPicker
implementation libs.materialPopupMenu
diff --git a/component-key/src/main/AndroidManifest.xml b/component-key/src/main/AndroidManifest.xml
index d92cf126..de90252b 100644
--- a/component-key/src/main/AndroidManifest.xml
+++ b/component-key/src/main/AndroidManifest.xml
@@ -1,4 +1,6 @@
-
+
+
+
diff --git a/component-key/src/main/java/xyz/aprildown/timer/component/key/ImagePreviewActivity.kt b/component-key/src/main/java/xyz/aprildown/timer/component/key/ImagePreviewActivity.kt
new file mode 100644
index 00000000..2da5c4c9
--- /dev/null
+++ b/component-key/src/main/java/xyz/aprildown/timer/component/key/ImagePreviewActivity.kt
@@ -0,0 +1,30 @@
+package xyz.aprildown.timer.component.key
+
+import android.content.Context
+import android.content.Intent
+import android.os.Bundle
+import androidx.activity.compose.setContent
+import androidx.compose.material3.Text
+import xyz.aprildown.timer.app.base.ui.BaseActivity
+
+class ImagePreviewActivity : BaseActivity() {
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ val path = intent?.getStringExtra(EXTRA_PATH)
+ if (path.isNullOrBlank()) {
+ finish()
+ return
+ }
+ setContent {
+ Text(text = path)
+ }
+ }
+
+ companion object {
+ private const val EXTRA_PATH = "path"
+ fun getIntent(context: Context, path: String): Intent {
+ return Intent(context, ImagePreviewActivity::class.java)
+ .putExtra(EXTRA_PATH, path)
+ }
+ }
+}
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 6a15f268..5223cb17 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -10,8 +10,8 @@ versionCode = "7050100"
versionName = "7.5.1"
kotlin = "1.9.24"
-
dagger = "2.51.1"
+composeCompiler = "1.5.14"
detekt = "1.23.6"
@@ -80,6 +80,14 @@ androidx-navigation-ui = "androidx.navigation:navigation-ui-ktx:2.7.7"
androidx-work = "androidx.work:work-runtime-ktx:2.9.0"
+compose-bom = "androidx.compose:compose-bom:2023.06.01"
+compose-material = { module = "androidx.compose.material3:material3" }
+compose-activity = "androidx.activity:activity-compose:1.8.2"
+compose-liveData = { module = "androidx.compose.runtime:runtime-livedata" }
+compose-constraintLayout = "androidx.constraintlayout:constraintlayout-compose:1.0.1"
+compose-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview" }
+compose-tooling-debug = { module = "androidx.compose.ui:ui-tooling" }
+
material = "com.google.android.material:material:1.4.0"
leakCannary = "com.squareup.leakcanary:leakcanary-android-startup:2.14"
@@ -149,3 +157,9 @@ androidx-test-espresso = [
"androidx-test-espresso-contrib",
"androidx-test-espresso-intents",
]
+compose = [
+ "compose-material",
+ "compose-activity",
+ "compose-liveData",
+ "compose-constraintLayout",
+]
From fa436717ed255f82ebc7d2034be001ae9b43cbf1 Mon Sep 17 00:00:00 2001
From: DeweyReed <32358486+DeweyReed@users.noreply.github.com>
Date: Fri, 9 Feb 2024 17:30:41 +0800
Subject: [PATCH 05/31] Replace Glide with Coil
---
app/build.gradle | 2 ++
.../java/io/github/deweyreed/timer/App.kt | 19 ++++++++++++++++++-
component-key/build.gradle | 3 ++-
.../key/behaviour/BehaviourLayout.kt | 10 ++++------
.../key/behaviour/EditableBehaviourLayout.kt | 10 ++++------
gradle/libs.versions.toml | 4 +++-
6 files changed, 33 insertions(+), 15 deletions(-)
diff --git a/app/build.gradle b/app/build.gradle
index 97f4b720..e390abe9 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -148,6 +148,8 @@ dependencies {
implementation libs.toggleButtonGroup
implementation libs.ultimateRingtonePicker
implementation libs.verticalStepperForm
+ implementation libs.coil
+ implementation libs.coil.gif
androidTestCompileOnly libs.taskerPlugin
}
diff --git a/app/src/main/java/io/github/deweyreed/timer/App.kt b/app/src/main/java/io/github/deweyreed/timer/App.kt
index 8d4e2ad6..cb719d43 100644
--- a/app/src/main/java/io/github/deweyreed/timer/App.kt
+++ b/app/src/main/java/io/github/deweyreed/timer/App.kt
@@ -11,6 +11,10 @@ import androidx.core.content.edit
import androidx.hilt.work.HiltWorkerFactory
import androidx.preference.PreferenceManager
import androidx.work.DelegatingWorkerFactory
+import coil.ImageLoader
+import coil.ImageLoaderFactory
+import coil.decode.GifDecoder
+import coil.decode.ImageDecoderDecoder
import com.github.deweyreed.tools.helper.hasPermissions
import dagger.Lazy
import dagger.hilt.android.HiltAndroidApp
@@ -36,7 +40,7 @@ import androidx.work.Configuration as WorkManagerConfiguration
import xyz.aprildown.timer.app.base.R as RBase
@HiltAndroidApp
-class App : Application(), WorkManagerConfiguration.Provider {
+class App : Application(), WorkManagerConfiguration.Provider, ImageLoaderFactory {
@Inject
lateinit var sharedPreferences: SharedPreferences
@@ -201,6 +205,19 @@ class App : Application(), WorkManagerConfiguration.Provider {
}
.build()
}
+
+ override fun newImageLoader(): ImageLoader {
+ return ImageLoader.Builder(this)
+ .components {
+ // https://coil-kt.github.io/coil/gifs/
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
+ add(ImageDecoderDecoder.Factory())
+ } else {
+ add(GifDecoder.Factory())
+ }
+ }
+ .build()
+ }
}
private const val PREF_FIRST_START = "pref_first_start_app"
diff --git a/component-key/build.gradle b/component-key/build.gradle
index 3f6b4de9..55575f24 100644
--- a/component-key/build.gradle
+++ b/component-key/build.gradle
@@ -28,6 +28,7 @@ dependencies {
implementation libs.hmsPicker
implementation libs.materialPopupMenu
implementation libs.scrollHmsPicker
- implementation libs.glide
+ implementation libs.coil
+ implementation libs.coil.compose
}
diff --git a/component-key/src/main/java/xyz/aprildown/timer/component/key/behaviour/BehaviourLayout.kt b/component-key/src/main/java/xyz/aprildown/timer/component/key/behaviour/BehaviourLayout.kt
index b59f0f6e..f44cfef2 100644
--- a/component-key/src/main/java/xyz/aprildown/timer/component/key/behaviour/BehaviourLayout.kt
+++ b/component-key/src/main/java/xyz/aprildown/timer/component/key/behaviour/BehaviourLayout.kt
@@ -10,8 +10,7 @@ import androidx.annotation.ColorInt
import androidx.appcompat.widget.TooltipCompat
import androidx.core.view.isEmpty
import androidx.core.view.isVisible
-import com.bumptech.glide.Glide
-import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions
+import coil.load
import com.github.deweyreed.tools.helper.setTextIfChanged
import com.google.android.material.chip.Chip
import xyz.aprildown.timer.component.key.R
@@ -69,10 +68,9 @@ class BehaviourLayout @JvmOverloads constructor(
} else {
LayoutEditableBehaviourImageBinding.bind(binding.layoutImage.getChildAt(0)).root
}
- Glide.with(imageView)
- .load(imageBehaviour.toImageAction().path)
- .transition(DrawableTransitionOptions.withCrossFade())
- .into(imageView)
+ imageView.load(imageBehaviour.toImageAction().path) {
+ crossfade(true)
+ }
} else {
binding.layoutImage.isVisible = false
binding.layoutImage.removeAllViews()
diff --git a/component-key/src/main/java/xyz/aprildown/timer/component/key/behaviour/EditableBehaviourLayout.kt b/component-key/src/main/java/xyz/aprildown/timer/component/key/behaviour/EditableBehaviourLayout.kt
index e0ed864a..6b0ca595 100644
--- a/component-key/src/main/java/xyz/aprildown/timer/component/key/behaviour/EditableBehaviourLayout.kt
+++ b/component-key/src/main/java/xyz/aprildown/timer/component/key/behaviour/EditableBehaviourLayout.kt
@@ -13,8 +13,7 @@ import androidx.appcompat.widget.TooltipCompat
import androidx.appcompat.widget.TooltipCompatFix
import androidx.core.view.isEmpty
import androidx.core.view.isVisible
-import com.bumptech.glide.Glide
-import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions
+import coil.load
import com.github.deweyreed.tools.helper.drawable
import com.github.deweyreed.tools.helper.setTextIfChanged
import com.github.deweyreed.tools.helper.toColorStateList
@@ -134,10 +133,9 @@ class EditableBehaviourLayout(
} else {
LayoutEditableBehaviourImageBinding.bind(binding.layoutImage.getChildAt(0)).root
}
- Glide.with(imageView)
- .load(imageAction.path)
- .transition(DrawableTransitionOptions.withCrossFade())
- .into(imageView)
+ imageView.load(imageAction.path) {
+ crossfade(true)
+ }
} else {
binding.layoutImage.removeAllViews()
}
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 5223cb17..3fc18c00 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -140,8 +140,10 @@ ultimateRingtonePicker = "com.github.DeweyReed:UltimateRingtonePicker:3.3.0"
verticalStepperForm = "com.ernestoyaquello.stepperform:vertical-stepper-form:2.5.0"
zip4j = "net.lingala.zip4j:zip4j:2.11.5"
taskerPlugin = "com.joaomgcd:taskerpluginlibrary:0.4.10"
-glide = "com.github.bumptech.glide:glide:4.16.0"
diskLruCache = "com.github.bumptech.glide:disklrucache:4.16.0"
+coil = "io.coil-kt:coil:2.5.0"
+coil-compose = "io.coil-kt:coil-compose:2.5.0"
+coil-gif = "io.coil-kt:coil-gif:2.5.0"
[bundles]
mockito-core = ["mockito-kotlin", "mockito-core"]
From 69d7633ea514036f3151e3c7bf996517c9614e63 Mon Sep 17 00:00:00 2001
From: DeweyReed <32358486+DeweyReed@users.noreply.github.com>
Date: Sun, 11 Feb 2024 12:03:52 +0800
Subject: [PATCH 06/31] Show the image preview
---
.../timer/app/timer/edit/EditActivity.kt | 13 +++-
.../timer/app/timer/edit/EditableStep.kt | 15 +++-
.../timer/app/timer/one/OneFragment.kt | 3 +
.../timer/app/timer/one/step/StepListView.kt | 22 +++++-
.../timer/app/timer/one/step/VisibleStep.kt | 11 ++-
.../dogReleaseRuntimeClasspath.txt | 1 +
.../googleReleaseRuntimeClasspath.txt | 1 +
.../otherReleaseRuntimeClasspath.txt | 1 +
component-key/build.gradle | 1 +
component-key/src/main/AndroidManifest.xml | 5 +-
.../component/key/ImagePreviewActivity.kt | 78 ++++++++++++++++++-
.../key/behaviour/BehaviourLayout.kt | 7 +-
.../key/behaviour/EditableBehaviourLayout.kt | 7 +-
component-key/src/main/res/values/themes.xml | 8 ++
gradle/libs.versions.toml | 1 +
15 files changed, 152 insertions(+), 22 deletions(-)
create mode 100644 component-key/src/main/res/values/themes.xml
diff --git a/app-timer-edit/src/main/java/xyz/aprildown/timer/app/timer/edit/EditActivity.kt b/app-timer-edit/src/main/java/xyz/aprildown/timer/app/timer/edit/EditActivity.kt
index e50e4193..83d8ca7c 100644
--- a/app-timer-edit/src/main/java/xyz/aprildown/timer/app/timer/edit/EditActivity.kt
+++ b/app-timer-edit/src/main/java/xyz/aprildown/timer/app/timer/edit/EditActivity.kt
@@ -55,6 +55,7 @@ import xyz.aprildown.timer.component.key.behaviour.EditableBehaviourLayout
import xyz.aprildown.timer.domain.entities.BehaviourEntity
import xyz.aprildown.timer.domain.entities.BehaviourType
import xyz.aprildown.timer.domain.entities.FolderEntity
+import xyz.aprildown.timer.domain.entities.ImageAction
import xyz.aprildown.timer.domain.entities.StepEntity
import xyz.aprildown.timer.domain.entities.StepType
import xyz.aprildown.timer.domain.entities.TimerEntity
@@ -101,14 +102,14 @@ class EditActivity :
private val pickImageLauncher = registerForActivityResult(
ActivityResultContracts.PickVisualMedia()
) { uri ->
- if (uri == null || uri == Uri.EMPTY) return@registerForActivityResult
val position = viewModel.imagePosition
+ viewModel.imagePosition = -1
+ if (uri == null || uri == Uri.EMPTY) return@registerForActivityResult
if (position == -1) return@registerForActivityResult
contentResolver.takePersistableUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION)
changeBehaviour(type = BehaviourType.IMAGE, position = position) {
it.toImageAction().copy(path = uri.toString()).toBehaviourEntity()
}
- viewModel.imagePosition = -1
}
override fun onCreate(savedInstanceState: Bundle?) {
@@ -778,7 +779,7 @@ class EditActivity :
BehaviourType.IMAGE -> {
addImageItems(
context = this@EditActivity,
- onPick = { onImageClick(position) },
+ onPick = { onImageAdding(position) },
)
}
else -> Unit
@@ -851,13 +852,17 @@ class EditActivity :
}
}
- override fun onImageClick(position: Int) {
+ override fun onImageAdding(position: Int) {
viewModel.imagePosition = position
pickImageLauncher.launch(
PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly)
)
}
+ override fun onImageCheck(position: Int, action: ImageAction) {
+ startActivity(appNavigator.getImagePreviewIntent(action.path))
+ }
+
private fun postUpdateTotalTime() {
if (shouldUpdateTotalTime) {
binding.viewEditStepInfo.run {
diff --git a/app-timer-edit/src/main/java/xyz/aprildown/timer/app/timer/edit/EditableStep.kt b/app-timer-edit/src/main/java/xyz/aprildown/timer/app/timer/edit/EditableStep.kt
index 8d0809c6..8c727439 100644
--- a/app-timer-edit/src/main/java/xyz/aprildown/timer/app/timer/edit/EditableStep.kt
+++ b/app-timer-edit/src/main/java/xyz/aprildown/timer/app/timer/edit/EditableStep.kt
@@ -22,6 +22,7 @@ import xyz.aprildown.timer.app.base.utils.setTime
import xyz.aprildown.timer.component.key.RoundTextView
import xyz.aprildown.timer.component.key.behaviour.EditableBehaviourLayout
import xyz.aprildown.timer.domain.entities.BehaviourEntity
+import xyz.aprildown.timer.domain.entities.ImageAction
import xyz.aprildown.timer.domain.entities.StepType
import xyz.aprildown.timer.app.base.R as RBase
@@ -58,7 +59,8 @@ class EditableStep(
position: Int
)
- fun onImageClick(position: Int)
+ fun onImageAdding(position: Int)
+ fun onImageCheck(position: Int, action: ImageAction)
fun onBehaviourAddedOrRemoved(position: Int, newBehaviours: List)
}
@@ -121,9 +123,14 @@ class EditableStep(
)
}
- override fun onImageClick() {
- super.onImageClick()
- handler.onImageClick(bindingAdapterPosition)
+ override fun onImageAdding() {
+ super.onImageAdding()
+ handler.onImageAdding(bindingAdapterPosition)
+ }
+
+ override fun onImageContentClick(action: ImageAction) {
+ super.onImageContentClick(action)
+ handler.onImageCheck(bindingAdapterPosition, action)
}
}
)
diff --git a/app-timer-one/src/main/java/xyz/aprildown/timer/app/timer/one/OneFragment.kt b/app-timer-one/src/main/java/xyz/aprildown/timer/app/timer/one/OneFragment.kt
index b9463336..dbaa4166 100644
--- a/app-timer-one/src/main/java/xyz/aprildown/timer/app/timer/one/OneFragment.kt
+++ b/app-timer-one/src/main/java/xyz/aprildown/timer/app/timer/one/OneFragment.kt
@@ -136,6 +136,9 @@ class OneFragment :
actionUpdateStepTime(index)
}
}
+ imageCheckListener = {
+ startActivity(appNavigator.getImagePreviewIntent(it.path))
+ }
}
viewModel.timer.observe(viewLifecycleOwner) {
diff --git a/app-timer-one/src/main/java/xyz/aprildown/timer/app/timer/one/step/StepListView.kt b/app-timer-one/src/main/java/xyz/aprildown/timer/app/timer/one/step/StepListView.kt
index edef0997..beed5abe 100644
--- a/app-timer-one/src/main/java/xyz/aprildown/timer/app/timer/one/step/StepListView.kt
+++ b/app-timer-one/src/main/java/xyz/aprildown/timer/app/timer/one/step/StepListView.kt
@@ -7,6 +7,7 @@ import androidx.recyclerview.widget.RecyclerView
import com.mikepenz.fastadapter.FastAdapter
import com.mikepenz.fastadapter.IItem
import com.mikepenz.fastadapter.adapters.ItemAdapter
+import xyz.aprildown.timer.domain.entities.ImageAction
import xyz.aprildown.timer.domain.entities.StepEntity
import xyz.aprildown.timer.domain.entities.TimerEntity
import xyz.aprildown.timer.presentation.stream.TimerIndex
@@ -41,6 +42,7 @@ class StepListView @JvmOverloads constructor(
private var currentIndex: TimerIndex = TimerIndex.Start
var listener: StepLongClickListener? = null
+ var imageCheckListener: ((ImageAction) -> Unit)? = null
init {
layoutManager = stepLayoutManager
@@ -128,7 +130,16 @@ class StepListView @JvmOverloads constructor(
endAdapter.clear()
timer.startStep?.let {
- startAdapter.add(VisibleStep(it, 0, indexesToIdentifier(0), this, this))
+ startAdapter.add(
+ VisibleStep(
+ step = it,
+ number = 0,
+ id = indexesToIdentifier(0),
+ currentPositionCallback = this,
+ stepLongClickListener = this,
+ imageCheckListener = imageCheckListener,
+ )
+ )
}
val stepItems = mutableListOf>()
@@ -141,7 +152,8 @@ class StepListView @JvmOverloads constructor(
number = i + 1,
id = indexesToIdentifier(i + 1),
currentPositionCallback = this,
- stepLongClickListener = this
+ stepLongClickListener = this,
+ imageCheckListener = imageCheckListener,
)
)
}
@@ -163,7 +175,8 @@ class StepListView @JvmOverloads constructor(
number = gi + 1,
id = indexesToIdentifier(i + 1, gi),
currentPositionCallback = this,
- stepLongClickListener = this
+ stepLongClickListener = this,
+ imageCheckListener = imageCheckListener,
)
}
)
@@ -181,7 +194,8 @@ class StepListView @JvmOverloads constructor(
number = stepCount + 1,
id = indexesToIdentifier(stepCount + 1),
currentPositionCallback = this,
- stepLongClickListener = this
+ stepLongClickListener = this,
+ imageCheckListener = imageCheckListener,
)
)
}
diff --git a/app-timer-one/src/main/java/xyz/aprildown/timer/app/timer/one/step/VisibleStep.kt b/app-timer-one/src/main/java/xyz/aprildown/timer/app/timer/one/step/VisibleStep.kt
index 891ec60e..b9c99bea 100644
--- a/app-timer-one/src/main/java/xyz/aprildown/timer/app/timer/one/step/VisibleStep.kt
+++ b/app-timer-one/src/main/java/xyz/aprildown/timer/app/timer/one/step/VisibleStep.kt
@@ -17,6 +17,7 @@ import xyz.aprildown.timer.app.base.data.PreferenceData.getTypeColor
import xyz.aprildown.timer.app.base.utils.produceTime
import xyz.aprildown.timer.app.timer.one.R
import xyz.aprildown.timer.app.timer.one.databinding.ItemStepStepBinding
+import xyz.aprildown.timer.domain.entities.ImageAction
import xyz.aprildown.timer.domain.entities.StepEntity
import xyz.aprildown.timer.app.base.R as RBase
@@ -25,12 +26,18 @@ internal class VisibleStep(
private val number: Int,
id: Long,
private val currentPositionCallback: CurrentPositionCallback,
- private val stepLongClickListener: OnStepLongClickListener
+ private val stepLongClickListener: OnStepLongClickListener,
+ private val imageCheckListener: ((ImageAction) -> Unit)?,
) : AbstractItem() {
override val layoutRes: Int = R.layout.item_step_step
override val type: Int = RBase.id.type_step_step
- override fun getViewHolder(v: View): ViewHolder = ViewHolder(v)
+ override fun getViewHolder(v: View): ViewHolder {
+ return ViewHolder(v).also {
+ it.binding.layoutBehaviour.onImageCheck = imageCheckListener
+ }
+ }
+
override var identifier: Long = id
override fun bindView(holder: ViewHolder, payloads: List) {
diff --git a/app/dependencies/dogReleaseRuntimeClasspath.txt b/app/dependencies/dogReleaseRuntimeClasspath.txt
index 5238380b..f294910e 100644
--- a/app/dependencies/dogReleaseRuntimeClasspath.txt
+++ b/app/dependencies/dogReleaseRuntimeClasspath.txt
@@ -136,6 +136,7 @@ com.squareup.okio:okio-jvm:3.9.0
com.squareup.okio:okio:3.9.0
javax.inject:javax.inject:1
me.zhanghai.android.materialprogressbar:library:1.4.2
+net.engawapg.lib:zoomable:1.6.0
nl.dionsegijn:konfetti:1.3.2
org.jetbrains.kotlin:kotlin-android-extensions-runtime:1.9.24
org.jetbrains.kotlin:kotlin-parcelize-runtime:1.9.24
diff --git a/app/dependencies/googleReleaseRuntimeClasspath.txt b/app/dependencies/googleReleaseRuntimeClasspath.txt
index fac88336..189eec96 100644
--- a/app/dependencies/googleReleaseRuntimeClasspath.txt
+++ b/app/dependencies/googleReleaseRuntimeClasspath.txt
@@ -182,6 +182,7 @@ com.squareup.okio:okio-jvm:3.9.0
com.squareup.okio:okio:3.9.0
javax.inject:javax.inject:1
me.zhanghai.android.materialprogressbar:library:1.4.2
+net.engawapg.lib:zoomable:1.6.0
net.lingala.zip4j:zip4j:2.11.5
nl.dionsegijn:konfetti:1.3.2
org.jetbrains.kotlin:kotlin-android-extensions-runtime:1.9.24
diff --git a/app/dependencies/otherReleaseRuntimeClasspath.txt b/app/dependencies/otherReleaseRuntimeClasspath.txt
index 5238380b..f294910e 100644
--- a/app/dependencies/otherReleaseRuntimeClasspath.txt
+++ b/app/dependencies/otherReleaseRuntimeClasspath.txt
@@ -136,6 +136,7 @@ com.squareup.okio:okio-jvm:3.9.0
com.squareup.okio:okio:3.9.0
javax.inject:javax.inject:1
me.zhanghai.android.materialprogressbar:library:1.4.2
+net.engawapg.lib:zoomable:1.6.0
nl.dionsegijn:konfetti:1.3.2
org.jetbrains.kotlin:kotlin-android-extensions-runtime:1.9.24
org.jetbrains.kotlin:kotlin-parcelize-runtime:1.9.24
diff --git a/component-key/build.gradle b/component-key/build.gradle
index 55575f24..1ee36226 100644
--- a/component-key/build.gradle
+++ b/component-key/build.gradle
@@ -30,5 +30,6 @@ dependencies {
implementation libs.scrollHmsPicker
implementation libs.coil
implementation libs.coil.compose
+ implementation libs.zoomable
}
diff --git a/component-key/src/main/AndroidManifest.xml b/component-key/src/main/AndroidManifest.xml
index de90252b..fbc4491d 100644
--- a/component-key/src/main/AndroidManifest.xml
+++ b/component-key/src/main/AndroidManifest.xml
@@ -1,6 +1,9 @@
-
+
diff --git a/component-key/src/main/java/xyz/aprildown/timer/component/key/ImagePreviewActivity.kt b/component-key/src/main/java/xyz/aprildown/timer/component/key/ImagePreviewActivity.kt
index 2da5c4c9..a31b7084 100644
--- a/component-key/src/main/java/xyz/aprildown/timer/component/key/ImagePreviewActivity.kt
+++ b/component-key/src/main/java/xyz/aprildown/timer/component/key/ImagePreviewActivity.kt
@@ -3,11 +3,34 @@ package xyz.aprildown.timer.component.key
import android.content.Context
import android.content.Intent
import android.os.Bundle
+import androidx.activity.ComponentActivity
+import androidx.activity.SystemBarStyle
import androidx.activity.compose.setContent
-import androidx.compose.material3.Text
-import xyz.aprildown.timer.app.base.ui.BaseActivity
+import androidx.activity.enableEdgeToEdge
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.background
+import androidx.compose.foundation.gestures.detectTapGestures
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.padding
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.input.pointer.pointerInput
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.unit.dp
+import coil.compose.rememberAsyncImagePainter
+import coil.request.ImageRequest
+import net.engawapg.lib.zoomable.rememberZoomState
+import net.engawapg.lib.zoomable.zoomable
+import android.graphics.Color as AndroidColor
+import androidx.compose.ui.graphics.Color.Companion as ComposeColor
+
+class ImagePreviewActivity : ComponentActivity() {
-class ImagePreviewActivity : BaseActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val path = intent?.getStringExtra(EXTRA_PATH)
@@ -15,8 +38,30 @@ class ImagePreviewActivity : BaseActivity() {
finish()
return
}
+
+ enableEdgeToEdge(
+ statusBarStyle = SystemBarStyle.dark(AndroidColor.TRANSPARENT),
+ navigationBarStyle = SystemBarStyle.dark(
+ AndroidColor.argb(0x80, 0x1b, 0x1b, 0x1b)
+ ) // DefaultDarkScrim
+ )
+
setContent {
- Text(text = path)
+ var data: String? by rememberSaveable { mutableStateOf(path) }
+
+ LaunchedEffect(Unit) {
+ addOnNewIntentListener { intent ->
+ intent?.getStringExtra(EXTRA_PATH)?.takeIf { it.isNotBlank() }?.let {
+ data = it
+ }
+ }
+ }
+
+ ImagePreview(
+ data = data,
+ onDismiss = ::finish,
+ modifier = Modifier.fillMaxSize(),
+ )
}
}
@@ -28,3 +73,28 @@ class ImagePreviewActivity : BaseActivity() {
}
}
}
+
+@Composable
+private fun ImagePreview(
+ data: Any?,
+ onDismiss: () -> Unit,
+ modifier: Modifier = Modifier,
+) {
+ Image(
+ painter = rememberAsyncImagePainter(
+ model = ImageRequest.Builder(LocalContext.current)
+ .data(data)
+ .crossfade(true)
+ .build(),
+ ),
+ contentDescription = null,
+ modifier = modifier
+ .fillMaxSize()
+ .background(ComposeColor.Black.copy(alpha = 0.5f))
+ .pointerInput(onDismiss) {
+ detectTapGestures(onTap = { onDismiss() })
+ }
+ .padding(16.dp)
+ .zoomable(zoomState = rememberZoomState()),
+ )
+}
diff --git a/component-key/src/main/java/xyz/aprildown/timer/component/key/behaviour/BehaviourLayout.kt b/component-key/src/main/java/xyz/aprildown/timer/component/key/behaviour/BehaviourLayout.kt
index f44cfef2..5d9ec98f 100644
--- a/component-key/src/main/java/xyz/aprildown/timer/component/key/behaviour/BehaviourLayout.kt
+++ b/component-key/src/main/java/xyz/aprildown/timer/component/key/behaviour/BehaviourLayout.kt
@@ -18,6 +18,7 @@ import xyz.aprildown.timer.component.key.databinding.LayoutBehaviourBinding
import xyz.aprildown.timer.component.key.databinding.LayoutEditableBehaviourImageBinding
import xyz.aprildown.timer.domain.entities.BehaviourEntity
import xyz.aprildown.timer.domain.entities.BehaviourType
+import xyz.aprildown.timer.domain.entities.ImageAction
import xyz.aprildown.timer.domain.entities.toImageAction
class BehaviourLayout @JvmOverloads constructor(
@@ -29,6 +30,8 @@ class BehaviourLayout @JvmOverloads constructor(
private val currentBehaviours = mutableListOf()
+ var onImageCheck: ((ImageAction) -> Unit)? = null
+
init {
orientation = VERTICAL
}
@@ -68,7 +71,9 @@ class BehaviourLayout @JvmOverloads constructor(
} else {
LayoutEditableBehaviourImageBinding.bind(binding.layoutImage.getChildAt(0)).root
}
- imageView.load(imageBehaviour.toImageAction().path) {
+ val action = imageBehaviour.toImageAction()
+ imageView.setOnClickListener { onImageCheck?.invoke(action) }
+ imageView.load(action.path) {
crossfade(true)
}
} else {
diff --git a/component-key/src/main/java/xyz/aprildown/timer/component/key/behaviour/EditableBehaviourLayout.kt b/component-key/src/main/java/xyz/aprildown/timer/component/key/behaviour/EditableBehaviourLayout.kt
index 6b0ca595..995eb47b 100644
--- a/component-key/src/main/java/xyz/aprildown/timer/component/key/behaviour/EditableBehaviourLayout.kt
+++ b/component-key/src/main/java/xyz/aprildown/timer/component/key/behaviour/EditableBehaviourLayout.kt
@@ -26,6 +26,7 @@ import xyz.aprildown.timer.component.key.databinding.LayoutEditableBehaviourBind
import xyz.aprildown.timer.component.key.databinding.LayoutEditableBehaviourImageBinding
import xyz.aprildown.timer.domain.entities.BehaviourEntity
import xyz.aprildown.timer.domain.entities.BehaviourType
+import xyz.aprildown.timer.domain.entities.ImageAction
import xyz.aprildown.timer.domain.entities.toImageAction
class EditableBehaviourLayout(
@@ -41,7 +42,8 @@ class EditableBehaviourLayout(
current: BehaviourEntity
)
- fun onImageClick(): Unit = Unit
+ fun onImageAdding(): Unit = Unit
+ fun onImageContentClick(action: ImageAction): Unit = Unit
}
private val binding =
@@ -82,7 +84,7 @@ class EditableBehaviourLayout(
}
callback = {
if (type == BehaviourType.IMAGE) {
- listener?.onImageClick()
+ listener?.onImageAdding()
} else {
addStubBehaviour(BehaviourEntity(type), true)
}
@@ -133,6 +135,7 @@ class EditableBehaviourLayout(
} else {
LayoutEditableBehaviourImageBinding.bind(binding.layoutImage.getChildAt(0)).root
}
+ imageView.setOnClickListener { listener?.onImageContentClick(imageAction) }
imageView.load(imageAction.path) {
crossfade(true)
}
diff --git a/component-key/src/main/res/values/themes.xml b/component-key/src/main/res/values/themes.xml
new file mode 100644
index 00000000..332ee82f
--- /dev/null
+++ b/component-key/src/main/res/values/themes.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 3fc18c00..bb75cd8a 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -144,6 +144,7 @@ diskLruCache = "com.github.bumptech.glide:disklrucache:4.16.0"
coil = "io.coil-kt:coil:2.5.0"
coil-compose = "io.coil-kt:coil-compose:2.5.0"
coil-gif = "io.coil-kt:coil-gif:2.5.0"
+zoomable = "net.engawapg.lib:zoomable:1.6.0"
[bundles]
mockito-core = ["mockito-kotlin", "mockito-core"]
From ba83670f2a052d5258d18d652667ba7ec654a9aa Mon Sep 17 00:00:00 2001
From: DeweyReed <32358486+DeweyReed@users.noreply.github.com>
Date: Tue, 20 Feb 2024 08:30:45 +0800
Subject: [PATCH 07/31] Copy images into the app's internal storage
---
.../SchedulerRepositoryImplTest.kt | 13 +-
.../repositories/TimerRepositoryImplTest.kt | 13 +-
.../data/repositories/TimerRepositoryImpl.kt | 162 +++++++++++++++++-
3 files changed, 171 insertions(+), 17 deletions(-)
diff --git a/data/src/androidTest/java/xyz/aprildown/timer/data/repositories/SchedulerRepositoryImplTest.kt b/data/src/androidTest/java/xyz/aprildown/timer/data/repositories/SchedulerRepositoryImplTest.kt
index 51b7303c..9d5cb396 100644
--- a/data/src/androidTest/java/xyz/aprildown/timer/data/repositories/SchedulerRepositoryImplTest.kt
+++ b/data/src/androidTest/java/xyz/aprildown/timer/data/repositories/SchedulerRepositoryImplTest.kt
@@ -1,5 +1,6 @@
package xyz.aprildown.timer.data.repositories
+import android.content.Context
import androidx.test.core.app.ApplicationProvider
import kotlinx.coroutines.test.runTest
import org.junit.After
@@ -24,13 +25,13 @@ import xyz.aprildown.timer.domain.repositories.TimerRepository
class SchedulerRepositoryImplTest {
- private val database = MachineDatabase.createInMemoryDatabase(
- ApplicationProvider.getApplicationContext()
- )
+ private val context = ApplicationProvider.getApplicationContext()
+ private val database = MachineDatabase.createInMemoryDatabase(context)
private val timerRepository: TimerRepository = TimerRepositoryImpl(
- database.timerDao(),
- TimerMapper(StepMapper(StepOnlyMapper(BehaviourMapper())), TimerMoreMapper()),
- TimerInfoMapper()
+ context = context,
+ timerDao = database.timerDao(),
+ timerMapper = TimerMapper(StepMapper(StepOnlyMapper(BehaviourMapper())), TimerMoreMapper()),
+ timerInfoMapper = TimerInfoMapper(),
)
private var timerId: Int = TimerEntity.NULL_ID
private val schedulerRepository: SchedulerRepository =
diff --git a/data/src/androidTest/java/xyz/aprildown/timer/data/repositories/TimerRepositoryImplTest.kt b/data/src/androidTest/java/xyz/aprildown/timer/data/repositories/TimerRepositoryImplTest.kt
index ade395e0..097c3937 100644
--- a/data/src/androidTest/java/xyz/aprildown/timer/data/repositories/TimerRepositoryImplTest.kt
+++ b/data/src/androidTest/java/xyz/aprildown/timer/data/repositories/TimerRepositoryImplTest.kt
@@ -1,5 +1,6 @@
package xyz.aprildown.timer.data.repositories
+import android.content.Context
import androidx.test.core.app.ApplicationProvider
import kotlinx.coroutines.test.runTest
import org.junit.After
@@ -20,13 +21,13 @@ import xyz.aprildown.timer.domain.repositories.TimerRepository
class TimerRepositoryImplTest {
- private val database = MachineDatabase.createInMemoryDatabase(
- ApplicationProvider.getApplicationContext()
- )
+ private val context = ApplicationProvider.getApplicationContext()
+ private val database = MachineDatabase.createInMemoryDatabase(context)
private val timerRepository: TimerRepository = TimerRepositoryImpl(
- database.timerDao(),
- TimerMapper(StepMapper(StepOnlyMapper(BehaviourMapper())), TimerMoreMapper()),
- TimerInfoMapper()
+ context = context,
+ timerDao = database.timerDao(),
+ timerMapper = TimerMapper(StepMapper(StepOnlyMapper(BehaviourMapper())), TimerMoreMapper()),
+ timerInfoMapper = TimerInfoMapper(),
)
@After
diff --git a/data/src/main/java/xyz/aprildown/timer/data/repositories/TimerRepositoryImpl.kt b/data/src/main/java/xyz/aprildown/timer/data/repositories/TimerRepositoryImpl.kt
index 2cfb6444..09d07b47 100644
--- a/data/src/main/java/xyz/aprildown/timer/data/repositories/TimerRepositoryImpl.kt
+++ b/data/src/main/java/xyz/aprildown/timer/data/repositories/TimerRepositoryImpl.kt
@@ -1,44 +1,115 @@
package xyz.aprildown.timer.data.repositories
+import android.content.ContentResolver
+import android.content.Context
+import android.webkit.MimeTypeMap
+import androidx.core.net.toFile
+import androidx.core.net.toUri
import dagger.Reusable
+import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import xyz.aprildown.timer.data.db.TimerDao
import xyz.aprildown.timer.data.mappers.TimerInfoMapper
import xyz.aprildown.timer.data.mappers.TimerMapper
import xyz.aprildown.timer.data.mappers.fromWithMapper
+import xyz.aprildown.timer.domain.entities.BehaviourType
+import xyz.aprildown.timer.domain.entities.ImageAction
+import xyz.aprildown.timer.domain.entities.StepEntity
import xyz.aprildown.timer.domain.entities.TimerEntity
import xyz.aprildown.timer.domain.entities.TimerInfo
+import xyz.aprildown.timer.domain.entities.toImageAction
import xyz.aprildown.timer.domain.repositories.TimerRepository
+import java.io.File
+import java.util.UUID
import javax.inject.Inject
@Reusable
internal class TimerRepositoryImpl @Inject constructor(
+ @ApplicationContext context: Context,
private val timerDao: TimerDao,
private val timerMapper: TimerMapper,
- private val timerInfoMapper: TimerInfoMapper
+ private val timerInfoMapper: TimerInfoMapper,
) : TimerRepository {
+ private val imageSaver: TimerImageSaver by lazy { TimerImageSaver(context) }
override suspend fun items(): List {
- return timerDao.getTimers().map { timerMapper.mapFrom(it) }
+ return timerDao.getTimers().map { timerMapper.mapFrom(it).withRealImagePath() }
}
override suspend fun item(id: Int): TimerEntity? {
return timerDao.getTimer(id).let {
- return@let if (it == null) null else timerMapper.mapFrom(it)
+ return@let if (it == null) null else timerMapper.mapFrom(it).withRealImagePath()
+ }
+ }
+
+ private fun TimerEntity.withRealImagePath(): TimerEntity {
+ return transformImageActions {
+ it.copy(path = imageSaver.getFullPath(id, it.path))
}
}
override suspend fun add(item: TimerEntity): Int {
- return timerDao.addTimer(timerMapper.mapTo(item)).toInt()
+ val tempTimerFolder = imageSaver.getTimerFolder(item.id)
+ val timerId = timerDao.addTimer(timerMapper.mapTo(item.saveImages())).toInt()
+ if (tempTimerFolder.exists()) {
+ val newTimerFolder = imageSaver.getTimerFolder(timerId)
+ tempTimerFolder.copyRecursively(newTimerFolder)
+ tempTimerFolder.deleteRecursively()
+ }
+ return timerId
}
override suspend fun save(item: TimerEntity): Boolean {
- return timerDao.updateTimer(timerMapper.mapTo(item)) == 1
+ try {
+ imageSaver.trackSavedImages()
+ return timerDao.updateTimer(timerMapper.mapTo(item.saveImages())) == 1
+ } finally {
+ imageSaver.deleteUnusedImages(item.id)
+ }
+ }
+
+ private fun TimerEntity.saveImages(): TimerEntity {
+ return transformImageActions {
+ it.copy(path = imageSaver.save(id, it.path))
+ }
+ }
+
+ private fun TimerEntity.transformImageActions(
+ transform: (ImageAction) -> ImageAction
+ ): TimerEntity {
+ fun StepEntity.transformStep(): StepEntity {
+ return when (this) {
+ is StepEntity.Step -> {
+ if (behaviour.any { it.type == BehaviourType.IMAGE }) {
+ copy(
+ behaviour = behaviour.map {
+ if (it.type == BehaviourType.IMAGE) {
+ transform(it.toImageAction()).toBehaviourEntity()
+ } else {
+ it
+ }
+ }
+ )
+ } else {
+ this
+ }
+ }
+ is StepEntity.Group -> {
+ copy(steps = steps.map { it.transformStep() })
+ }
+ }
+ }
+ return copy(
+ steps = steps.map { it.transformStep() },
+ startStep = startStep?.transformStep() as? StepEntity.Step,
+ endStep = endStep?.transformStep() as? StepEntity.Step,
+ )
}
override suspend fun delete(id: Int) {
timerDao.deleteTimer(id)
+ imageSaver.deleteAll(id)
}
override suspend fun getTimerInfoByTimerId(timerId: Int): TimerInfo? {
@@ -61,3 +132,84 @@ internal class TimerRepositoryImpl @Inject constructor(
timerDao.moveFolderTimersToAnother(originalFolderId, targetFolderId)
}
}
+
+private class TimerImageSaver(private val context: Context) {
+ private val imageFolder = File(context.filesDir, FOLDER)
+
+ private var savedFilenames = mutableListOf()
+
+ fun getTimerFolder(timerId: Int): File {
+ return File(imageFolder, timerId.toString())
+ }
+
+ fun getFullPath(timerId: Int, path: String): String {
+ return File(getTimerFolder(timerId), path).toUri().toString()
+ }
+
+ /**
+ * @return The saved image filename
+ */
+ fun save(timerId: Int, path: String): String {
+ val uri = path.toUri()
+ val timerFolder = getTimerFolder(timerId)
+ if (uri.scheme == ContentResolver.SCHEME_FILE) {
+ val imageFile = uri.toFile()
+ if (imageFile.parentFile?.canonicalPath == timerFolder.canonicalPath) {
+ return imageFile.name.also {
+ savedFilenames += it
+ }
+ }
+ }
+
+ if (!imageFolder.exists()) imageFolder.mkdirs()
+ if (!timerFolder.exists()) timerFolder.mkdirs()
+
+ return when (uri.scheme) {
+ ContentResolver.SCHEME_CONTENT -> {
+ val extension = MimeTypeMap.getSingleton()
+ .getExtensionFromMimeType(context.contentResolver.getType(uri)) ?: "jpg"
+ val imageFile = File(timerFolder, "${UUID.randomUUID()}.$extension")
+ if (imageFile.exists()) {
+ imageFile.delete()
+ imageFile.createNewFile()
+ }
+ context.contentResolver.openInputStream(uri)?.use { inputStream ->
+ imageFile.outputStream().use { outputStream ->
+ inputStream.copyTo(outputStream)
+ }
+ }
+ require(imageFile.exists() && imageFile.length() > 0L)
+ imageFile.name.also {
+ savedFilenames += it
+ }
+ }
+ else -> error("Unsupported path $path")
+ }
+ }
+
+ fun trackSavedImages() {
+ savedFilenames.clear()
+ }
+
+ fun deleteUnusedImages(timerId: Int) {
+ val timerFolder = getTimerFolder(timerId)
+ if (!timerFolder.exists()) return
+ timerFolder.listFiles()?.toList()?.forEach { file ->
+ if (file.name !in savedFilenames) {
+ file.delete()
+ }
+ }
+ savedFilenames.clear()
+ }
+
+ fun deleteAll(timerId: Int) {
+ val timerFolder = getTimerFolder(timerId)
+ if (timerFolder.exists()) {
+ timerFolder.deleteRecursively()
+ }
+ }
+
+ companion object {
+ private const val FOLDER = "timer_images"
+ }
+}
From ccfa3c209884a359343a880b0f53e62bf1e878ed Mon Sep 17 00:00:00 2001
From: DeweyReed <32358486+DeweyReed@users.noreply.github.com>
Date: Tue, 20 Feb 2024 08:59:09 +0800
Subject: [PATCH 08/31] Reduce the height of images
---
.../src/main/res/layout/layout_editable_behaviour_image.xml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/component-key/src/main/res/layout/layout_editable_behaviour_image.xml b/component-key/src/main/res/layout/layout_editable_behaviour_image.xml
index 2abe05ab..8054a503 100644
--- a/component-key/src/main/res/layout/layout_editable_behaviour_image.xml
+++ b/component-key/src/main/res/layout/layout_editable_behaviour_image.xml
@@ -2,6 +2,6 @@
From 563e4e46f9d147a8df702718b6477e740f8e1858 Mon Sep 17 00:00:00 2001
From: DeweyReed <32358486+DeweyReed@users.noreply.github.com>
Date: Mon, 4 Mar 2024 08:57:08 +0800
Subject: [PATCH 09/31] Enable Jetpack Compose in the backup module
---
app-backup/build.gradle | 9 +++++++++
component-key/build.gradle | 2 --
gradle/libs.versions.toml | 2 --
3 files changed, 9 insertions(+), 4 deletions(-)
diff --git a/app-backup/build.gradle b/app-backup/build.gradle
index 19b73d25..fa860409 100644
--- a/app-backup/build.gradle
+++ b/app-backup/build.gradle
@@ -7,6 +7,12 @@ plugins {
android {
namespace 'xyz.aprildown.timer.app.backup'
+ buildFeatures {
+ compose true
+ }
+ composeOptions {
+ kotlinCompilerExtensionVersion = libs.versions.composeCompiler.get()
+ }
}
dependencies {
@@ -15,6 +21,9 @@ dependencies {
implementation libs.androidx.preference
+ implementation platform(libs.compose.bom)
+ implementation libs.bundles.compose
+
implementation libs.hilt.android
kapt libs.hilt.compiler
diff --git a/component-key/build.gradle b/component-key/build.gradle
index 1ee36226..69e4c085 100644
--- a/component-key/build.gradle
+++ b/component-key/build.gradle
@@ -21,8 +21,6 @@ dependencies {
implementation platform(libs.compose.bom)
implementation libs.bundles.compose
- implementation libs.compose.tooling.preview
- debugImplementation libs.compose.tooling.debug
implementation libs.flexbox
implementation libs.hmsPicker
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index bb75cd8a..4184d4b5 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -85,8 +85,6 @@ compose-material = { module = "androidx.compose.material3:material3" }
compose-activity = "androidx.activity:activity-compose:1.8.2"
compose-liveData = { module = "androidx.compose.runtime:runtime-livedata" }
compose-constraintLayout = "androidx.constraintlayout:constraintlayout-compose:1.0.1"
-compose-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview" }
-compose-tooling-debug = { module = "androidx.compose.ui:ui-tooling" }
material = "com.google.android.material:material:1.4.0"
From 16bf2612ef5cdd9174558341475a42ec12da6305 Mon Sep 17 00:00:00 2001
From: DeweyReed <32358486+DeweyReed@users.noreply.github.com>
Date: Sat, 9 Mar 2024 13:19:41 +0800
Subject: [PATCH 10/31] Create Export2Fragment
---
.../timer/app/backup/Export2Fragment.kt | 312 ++++++++++++++++++
.../timer/app/backup/Export2ViewModel.kt | 109 ++++++
app/src/main/res/navigation/nav_graph.xml | 5 +-
gradle/libs.versions.toml | 2 +
4 files changed, 425 insertions(+), 3 deletions(-)
create mode 100644 app-backup/src/main/java/xyz/aprildown/timer/app/backup/Export2Fragment.kt
create mode 100644 app-backup/src/main/java/xyz/aprildown/timer/app/backup/Export2ViewModel.kt
diff --git a/app-backup/src/main/java/xyz/aprildown/timer/app/backup/Export2Fragment.kt b/app-backup/src/main/java/xyz/aprildown/timer/app/backup/Export2Fragment.kt
new file mode 100644
index 00000000..4b079e4c
--- /dev/null
+++ b/app-backup/src/main/java/xyz/aprildown/timer/app/backup/Export2Fragment.kt
@@ -0,0 +1,312 @@
+package xyz.aprildown.timer.app.backup
+
+import android.app.Activity
+import android.content.Context
+import android.content.Intent
+import android.net.Uri
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.activity.result.contract.ActivityResultContracts
+import androidx.annotation.DrawableRes
+import androidx.annotation.StringRes
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.automirrored.rounded.NavigateNext
+import androidx.compose.material.icons.rounded.Description
+import androidx.compose.material3.AlertDialog
+import androidx.compose.material3.Button
+import androidx.compose.material3.CircularProgressIndicator
+import androidx.compose.material3.Icon
+import androidx.compose.material3.ListItem
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Surface
+import androidx.compose.material3.Switch
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextButton
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.collectAsState
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.ComposeView
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.platform.ViewCompositionStrategy
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.window.Dialog
+import androidx.fragment.app.Fragment
+import androidx.fragment.app.viewModels
+import com.github.deweyreed.tools.helper.requireCallback
+import dagger.hilt.android.AndroidEntryPoint
+import xyz.aprildown.timer.app.base.data.PreferenceData.lastBackupUri
+import xyz.aprildown.timer.app.base.ui.MainCallback
+import xyz.aprildown.timer.domain.utils.AppTracker
+import java.text.SimpleDateFormat
+import java.util.Date
+import java.util.Locale
+import javax.inject.Inject
+import xyz.aprildown.timer.app.base.R as RBase
+
+@AndroidEntryPoint
+class Export2Fragment : Fragment() {
+
+ private val viewModel: Export2ViewModel by viewModels()
+
+ private lateinit var mainCallback: MainCallback.ActivityCallback
+
+ @Inject
+ lateinit var appTracker: AppTracker
+
+ private val launcher =
+ registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
+ handleActivityResult(resultCode = it.resultCode, data = it.data)
+ }
+
+ override fun onAttach(context: Context) {
+ super.onAttach(context)
+ mainCallback = requireCallback()
+ }
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View {
+ return ComposeView(requireContext()).apply {
+ setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
+ }
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+ (view as ComposeView).setContent {
+ Export2(
+ screen = viewModel.screen.collectAsState().value,
+ onLocationChange = ::onLocationChange,
+ modifier = Modifier.fillMaxSize(),
+ )
+ }
+ }
+
+ private fun onLocationChange() {
+ val date = Date()
+ val timeString = SimpleDateFormat("yyyy-MM-dd-kk-mm", Locale.getDefault()).format(date)
+ val initialFilename = "timer-machine-$timeString.json"
+ SafIntentSafeBelt(
+ context = requireContext(),
+ appTracker = appTracker,
+ viewForSnackbar = mainCallback.snackbarView
+ ).drive(
+ launcher = launcher,
+ intent = Intent(Intent.ACTION_CREATE_DOCUMENT)
+ .addCategory(Intent.CATEGORY_OPENABLE)
+ .setType("*/*")
+ .putExtra(Intent.EXTRA_TITLE, initialFilename)
+ )
+ }
+
+ private fun handleActivityResult(resultCode: Int, data: Intent?) {
+ if (resultCode == Activity.RESULT_OK) {
+ val uri = data?.data ?: return
+ if (uri.toString().isBlank() || uri == Uri.EMPTY) return
+ val context = requireContext()
+ context.lastBackupUri = uri
+ viewModel.setSaveLocation(uri.toString())
+ }
+ }
+}
+
+@Composable
+private fun Export2(
+ screen: Export2ViewModel.Screen,
+ onLocationChange: () -> Unit,
+ modifier: Modifier = Modifier,
+) {
+ MaterialTheme {
+ Column(
+ modifier = modifier
+ .verticalScroll(rememberScrollState())
+ .padding(16.dp),
+ verticalArrangement = Arrangement.spacedBy(16.dp),
+ ) {
+ Header(text = stringResource(id = RBase.string.export_path_title))
+
+ SaveLocation(
+ location = screen.saveLocation,
+ modifier = Modifier.clickable(onClick = onLocationChange),
+ )
+
+ Header(text = stringResource(id = RBase.string.backup_app_data))
+
+ DataEntry(
+ iconRes = RBase.drawable.ic_timer,
+ nameRes = RBase.string.main_action_timers,
+ include = screen.includeTimers,
+ onIncludedChange = screen.onTimersChange,
+ )
+ DataEntry(
+ iconRes = RBase.drawable.ic_scheduler,
+ nameRes = RBase.string.main_action_schedulers,
+ include = screen.includeSchedulers,
+ onIncludedChange = screen.onSchedulersChange,
+ )
+ DataEntry(
+ iconRes = RBase.drawable.ic_stat,
+ nameRes = RBase.string.main_action_record,
+ include = screen.includeRecords,
+ onIncludedChange = screen.onRecordsChange,
+ )
+ DataEntry(
+ iconRes = RBase.drawable.ic_settings,
+ nameRes = RBase.string.main_action_settings,
+ include = screen.includeSettings,
+ onIncludedChange = screen.onSettingsChange,
+ )
+
+ Button(
+ onClick = screen.onExport,
+ modifier = Modifier.align(Alignment.CenterHorizontally),
+ enabled = !screen.saveLocation.isNullOrBlank() &&
+ (screen.includeTimers || screen.includeSettings) &&
+ !screen.exporting,
+ ) {
+ Text(text = stringResource(id = RBase.string.export_action))
+ }
+ }
+
+ if (screen.exportError != null) {
+ ExportError(
+ error = screen.exportError,
+ consume = screen.consumeExportError,
+ )
+ }
+
+ if (screen.exporting) {
+ ExportingDialog()
+ }
+ }
+}
+
+@Composable
+private fun Header(
+ text: String,
+ modifier: Modifier = Modifier,
+) {
+ Text(
+ text = text,
+ modifier = modifier.padding(horizontal = 12.dp, vertical = 12.dp),
+ style = MaterialTheme.typography.titleSmall,
+ )
+}
+
+@Composable
+private fun SaveLocation(
+ location: String?,
+ modifier: Modifier = Modifier,
+) {
+ if (!location.isNullOrBlank()) {
+ ListItem(
+ headlineContent = {
+ Text(text = location)
+ },
+ modifier = modifier,
+ overlineContent = {
+ Text(text = stringResource(id = RBase.string.export_select_location))
+ },
+ leadingContent = {
+ Icon(imageVector = Icons.Rounded.Description, contentDescription = null)
+ },
+ trailingContent = {
+ Icon(
+ imageVector = Icons.AutoMirrored.Rounded.NavigateNext,
+ contentDescription = null,
+ )
+ },
+ )
+ } else {
+ ListItem(
+ headlineContent = {
+ Text(text = stringResource(id = RBase.string.export_select_location))
+ },
+ modifier = modifier,
+ leadingContent = {
+ Icon(imageVector = Icons.Rounded.Description, contentDescription = null)
+ },
+ trailingContent = {
+ Icon(
+ imageVector = Icons.AutoMirrored.Rounded.NavigateNext,
+ contentDescription = null,
+ )
+ },
+ )
+ }
+}
+
+@Composable
+private fun DataEntry(
+ @DrawableRes iconRes: Int,
+ @StringRes nameRes: Int,
+ include: Boolean,
+ onIncludedChange: (Boolean) -> Unit,
+ modifier: Modifier = Modifier,
+) {
+ ListItem(
+ headlineContent = {
+ Text(text = stringResource(id = nameRes))
+ },
+ modifier = modifier.clickable { onIncludedChange(!include) },
+ leadingContent = {
+ Icon(painter = painterResource(id = iconRes), contentDescription = null)
+ },
+ trailingContent = {
+ Switch(checked = include, onCheckedChange = onIncludedChange)
+ },
+ )
+}
+
+@Composable
+private fun ExportError(
+ error: Throwable,
+ consume: () -> Unit,
+) {
+ AlertDialog(
+ onDismissRequest = consume,
+ confirmButton = {
+ TextButton(onClick = consume) {
+ Text(text = stringResource(id = RBase.string.ok))
+ }
+ },
+ text = {
+ val context = LocalContext.current
+ Text(
+ text = buildString {
+ append(context.getString(RBase.string.export_error))
+
+ (error.localizedMessage ?: error.message)
+ ?.takeIf { it.isNotBlank() }
+ ?.let { message ->
+ append("\n\n")
+ append(message)
+ }
+ },
+ )
+ }
+ )
+}
+
+@Composable
+private fun ExportingDialog() {
+ Dialog(onDismissRequest = {}) {
+ Surface(shape = MaterialTheme.shapes.medium) {
+ CircularProgressIndicator(modifier = Modifier.padding(24.dp))
+ }
+ }
+}
diff --git a/app-backup/src/main/java/xyz/aprildown/timer/app/backup/Export2ViewModel.kt b/app-backup/src/main/java/xyz/aprildown/timer/app/backup/Export2ViewModel.kt
new file mode 100644
index 00000000..fd2142ac
--- /dev/null
+++ b/app-backup/src/main/java/xyz/aprildown/timer/app/backup/Export2ViewModel.kt
@@ -0,0 +1,109 @@
+package xyz.aprildown.timer.app.backup
+
+import androidx.compose.runtime.Immutable
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.update
+import kotlinx.coroutines.launch
+import javax.inject.Inject
+
+@HiltViewModel
+internal class Export2ViewModel @Inject constructor() : ViewModel() {
+ @Immutable
+ data class Screen(
+ val saveLocation: String? = null,
+
+ val includeTimers: Boolean = true,
+ val onTimersChange: (Boolean) -> Unit,
+
+ val includeRecords: Boolean = true,
+ val onRecordsChange: (Boolean) -> Unit,
+
+ val includeSchedulers: Boolean = true,
+ val onSchedulersChange: (Boolean) -> Unit,
+
+ val includeSettings: Boolean = true,
+ val onSettingsChange: (Boolean) -> Unit,
+
+ val onExport: () -> Unit,
+ val exporting: Boolean = false,
+
+ val exportError: Throwable? = null,
+ val consumeExportError: () -> Unit,
+ )
+
+ private val _screen: MutableStateFlow = MutableStateFlow(
+ Screen(
+ onTimersChange = ::onTimersChange,
+ onRecordsChange = ::onRecordsChange,
+ onSchedulersChange = ::onSchedulersChange,
+ onSettingsChange = ::onSettingsChange,
+ onExport = ::onExport,
+ consumeExportError = ::consumeExportError,
+ )
+ )
+ val screen: StateFlow = _screen.asStateFlow()
+
+ fun setSaveLocation(location: String) {
+ _screen.update { it.copy(saveLocation = location) }
+ }
+
+ private fun onTimersChange(include: Boolean) {
+ _screen.update {
+ it.copy(
+ includeTimers = include,
+ includeRecords = include,
+ includeSchedulers = include,
+ )
+ }
+ }
+
+ private fun onRecordsChange(include: Boolean) {
+ _screen.update {
+ it.copy(
+ includeTimers = if (include) true else it.includeTimers,
+ includeRecords = include,
+ )
+ }
+ }
+
+ private fun onSchedulersChange(include: Boolean) {
+ _screen.update {
+ it.copy(
+ includeTimers = if (include) true else it.includeTimers,
+ includeSchedulers = include,
+ )
+ }
+ }
+
+ private fun onSettingsChange(include: Boolean) {
+ _screen.update {
+ it.copy(includeSettings = include)
+ }
+ }
+
+ private fun onExport() {
+ val screen = _screen.value
+ if (screen.saveLocation.isNullOrBlank()) return
+ if (!screen.includeTimers && !screen.includeSettings) return
+ if (screen.exporting) return
+ viewModelScope.launch {
+ _screen.update { it.copy(exporting = true) }
+ try {
+ delay(5500)
+ _screen.update { it.copy(exportError = Throwable("Hey")) }
+ } finally {
+ _screen.update { it.copy(exporting = false) }
+ }
+ }
+ }
+
+ private fun consumeExportError() {
+ _screen.update { it.copy(exportError = null) }
+ }
+}
diff --git a/app/src/main/res/navigation/nav_graph.xml b/app/src/main/res/navigation/nav_graph.xml
index fbb17252..e8621021 100644
--- a/app/src/main/res/navigation/nav_graph.xml
+++ b/app/src/main/res/navigation/nav_graph.xml
@@ -61,9 +61,8 @@
+ android:name="xyz.aprildown.timer.app.backup.Export2Fragment"
+ android:label="@string/export_data_title" />
Date: Sun, 7 Apr 2024 09:37:28 +0800
Subject: [PATCH 11/31] Upgrade the compose dependencies
---
gradle/libs.versions.toml | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 9f6ced14..f1e107b8 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -140,10 +140,10 @@ verticalStepperForm = "com.ernestoyaquello.stepperform:vertical-stepper-form:2.5
zip4j = "net.lingala.zip4j:zip4j:2.11.5"
taskerPlugin = "com.joaomgcd:taskerpluginlibrary:0.4.10"
diskLruCache = "com.github.bumptech.glide:disklrucache:4.16.0"
-coil = "io.coil-kt:coil:2.5.0"
-coil-compose = "io.coil-kt:coil-compose:2.5.0"
-coil-gif = "io.coil-kt:coil-gif:2.5.0"
-zoomable = "net.engawapg.lib:zoomable:1.6.0"
+coil = "io.coil-kt:coil:2.6.0"
+coil-compose = "io.coil-kt:coil-compose:2.6.0"
+coil-gif = "io.coil-kt:coil-gif:2.6.0"
+zoomable = "net.engawapg.lib:zoomable:1.6.1"
[bundles]
mockito-core = ["mockito-kotlin", "mockito-core"]
From 45b1b222e7e7b7b408731a41dbf438b177a95be6 Mon Sep 17 00:00:00 2001
From: DeweyReed <32358486+DeweyReed@users.noreply.github.com>
Date: Sun, 7 Apr 2024 12:08:44 +0800
Subject: [PATCH 12/31] Improve the stability of the composables
---
.../timer/app/backup/Export2Fragment.kt | 94 ++++++++++---------
.../timer/app/backup/Export2ViewModel.kt | 8 +-
2 files changed, 56 insertions(+), 46 deletions(-)
diff --git a/app-backup/src/main/java/xyz/aprildown/timer/app/backup/Export2Fragment.kt b/app-backup/src/main/java/xyz/aprildown/timer/app/backup/Export2Fragment.kt
index 4b079e4c..82c8ac53 100644
--- a/app-backup/src/main/java/xyz/aprildown/timer/app/backup/Export2Fragment.kt
+++ b/app-backup/src/main/java/xyz/aprildown/timer/app/backup/Export2Fragment.kt
@@ -182,9 +182,9 @@ private fun Export2(
}
}
- if (screen.exportError != null) {
+ if (screen.exportErrorMessage != null) {
ExportError(
- error = screen.exportError,
+ message = screen.exportErrorMessage,
consume = screen.consumeExportError,
)
}
@@ -212,44 +212,54 @@ private fun SaveLocation(
location: String?,
modifier: Modifier = Modifier,
) {
- if (!location.isNullOrBlank()) {
- ListItem(
- headlineContent = {
- Text(text = location)
- },
- modifier = modifier,
- overlineContent = {
- Text(text = stringResource(id = RBase.string.export_select_location))
- },
- leadingContent = {
- Icon(imageVector = Icons.Rounded.Description, contentDescription = null)
- },
- trailingContent = {
- Icon(
- imageVector = Icons.AutoMirrored.Rounded.NavigateNext,
- contentDescription = null,
- )
- },
- )
+ if (location.isNullOrBlank()) {
+ SaveLocationEmpty(modifier = modifier)
} else {
- ListItem(
- headlineContent = {
- Text(text = stringResource(id = RBase.string.export_select_location))
- },
- modifier = modifier,
- leadingContent = {
- Icon(imageVector = Icons.Rounded.Description, contentDescription = null)
- },
- trailingContent = {
- Icon(
- imageVector = Icons.AutoMirrored.Rounded.NavigateNext,
- contentDescription = null,
- )
- },
- )
+ SaveLocationPath(location = location, modifier = modifier)
}
}
+@Composable
+private fun SaveLocationPath(location: String, modifier: Modifier) {
+ ListItem(
+ headlineContent = {
+ Text(text = location)
+ },
+ modifier = modifier,
+ overlineContent = {
+ Text(text = stringResource(id = RBase.string.export_select_location))
+ },
+ leadingContent = {
+ Icon(imageVector = Icons.Rounded.Description, contentDescription = null)
+ },
+ trailingContent = {
+ Icon(
+ imageVector = Icons.AutoMirrored.Rounded.NavigateNext,
+ contentDescription = null,
+ )
+ },
+ )
+}
+
+@Composable
+private fun SaveLocationEmpty(modifier: Modifier) {
+ ListItem(
+ headlineContent = {
+ Text(text = stringResource(id = RBase.string.export_select_location))
+ },
+ modifier = modifier,
+ leadingContent = {
+ Icon(imageVector = Icons.Rounded.Description, contentDescription = null)
+ },
+ trailingContent = {
+ Icon(
+ imageVector = Icons.AutoMirrored.Rounded.NavigateNext,
+ contentDescription = null,
+ )
+ },
+ )
+}
+
@Composable
private fun DataEntry(
@DrawableRes iconRes: Int,
@@ -274,7 +284,7 @@ private fun DataEntry(
@Composable
private fun ExportError(
- error: Throwable,
+ message: String,
consume: () -> Unit,
) {
AlertDialog(
@@ -290,12 +300,10 @@ private fun ExportError(
text = buildString {
append(context.getString(RBase.string.export_error))
- (error.localizedMessage ?: error.message)
- ?.takeIf { it.isNotBlank() }
- ?.let { message ->
- append("\n\n")
- append(message)
- }
+ if (message.isNotBlank()) {
+ append("\n\n")
+ append(message)
+ }
},
)
}
diff --git a/app-backup/src/main/java/xyz/aprildown/timer/app/backup/Export2ViewModel.kt b/app-backup/src/main/java/xyz/aprildown/timer/app/backup/Export2ViewModel.kt
index fd2142ac..7ef7ce3f 100644
--- a/app-backup/src/main/java/xyz/aprildown/timer/app/backup/Export2ViewModel.kt
+++ b/app-backup/src/main/java/xyz/aprildown/timer/app/backup/Export2ViewModel.kt
@@ -33,7 +33,7 @@ internal class Export2ViewModel @Inject constructor() : ViewModel() {
val onExport: () -> Unit,
val exporting: Boolean = false,
- val exportError: Throwable? = null,
+ val exportErrorMessage: String? = null,
val consumeExportError: () -> Unit,
)
@@ -96,7 +96,9 @@ internal class Export2ViewModel @Inject constructor() : ViewModel() {
_screen.update { it.copy(exporting = true) }
try {
delay(5500)
- _screen.update { it.copy(exportError = Throwable("Hey")) }
+ _screen.update {
+ it.copy(exportErrorMessage = "Hey")
+ }
} finally {
_screen.update { it.copy(exporting = false) }
}
@@ -104,6 +106,6 @@ internal class Export2ViewModel @Inject constructor() : ViewModel() {
}
private fun consumeExportError() {
- _screen.update { it.copy(exportError = null) }
+ _screen.update { it.copy(exportErrorMessage = null) }
}
}
From 392eeadd8ff61f628262e55a461049c54298014a Mon Sep 17 00:00:00 2001
From: DeweyReed <32358486+DeweyReed@users.noreply.github.com>
Date: Sun, 14 Apr 2024 10:37:25 +0800
Subject: [PATCH 13/31] Create Import2Fragment
---
.../timer/app/backup/BackupComposables.kt | 243 ++++++++++++++++++
.../timer/app/backup/BaseBackupViewModel.kt | 116 +++++++++
.../timer/app/backup/Export2Fragment.kt | 241 ++---------------
.../timer/app/backup/Export2ViewModel.kt | 107 +-------
.../timer/app/backup/Import2Fragment.kt | 150 +++++++++++
.../timer/app/backup/Import2ViewModel.kt | 36 +++
app/src/main/res/navigation/nav_graph.xml | 5 +-
7 files changed, 572 insertions(+), 326 deletions(-)
create mode 100644 app-backup/src/main/java/xyz/aprildown/timer/app/backup/BackupComposables.kt
create mode 100644 app-backup/src/main/java/xyz/aprildown/timer/app/backup/BaseBackupViewModel.kt
create mode 100644 app-backup/src/main/java/xyz/aprildown/timer/app/backup/Import2Fragment.kt
create mode 100644 app-backup/src/main/java/xyz/aprildown/timer/app/backup/Import2ViewModel.kt
diff --git a/app-backup/src/main/java/xyz/aprildown/timer/app/backup/BackupComposables.kt b/app-backup/src/main/java/xyz/aprildown/timer/app/backup/BackupComposables.kt
new file mode 100644
index 00000000..81f4a151
--- /dev/null
+++ b/app-backup/src/main/java/xyz/aprildown/timer/app/backup/BackupComposables.kt
@@ -0,0 +1,243 @@
+package xyz.aprildown.timer.app.backup
+
+import androidx.annotation.DrawableRes
+import androidx.annotation.StringRes
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.automirrored.rounded.NavigateNext
+import androidx.compose.material.icons.rounded.Description
+import androidx.compose.material3.AlertDialog
+import androidx.compose.material3.Button
+import androidx.compose.material3.CircularProgressIndicator
+import androidx.compose.material3.Icon
+import androidx.compose.material3.ListItem
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Surface
+import androidx.compose.material3.Switch
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextButton
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.window.Dialog
+import xyz.aprildown.timer.app.base.R as RBase
+
+@Composable
+internal fun Backup(
+ screen: BaseBackupViewModel.Screen,
+ contentLocationTitle: String,
+ contentLocationButtonText: String,
+ onChangeContentLocation: () -> Unit,
+ backupButtonText: String,
+ backupErrorHint: String,
+ modifier: Modifier = Modifier,
+ extraOptions: @Composable () -> Unit = {},
+) {
+ MaterialTheme {
+ Column(
+ modifier = modifier
+ .verticalScroll(rememberScrollState())
+ .padding(16.dp),
+ verticalArrangement = Arrangement.spacedBy(16.dp),
+ ) {
+ Header(text = contentLocationTitle)
+
+ ContentLocation(
+ title = contentLocationButtonText,
+ location = screen.contentName ?: screen.contentLocation,
+ modifier = Modifier.clickable(onClick = onChangeContentLocation),
+ )
+
+ Header(text = stringResource(id = RBase.string.backup_app_data))
+
+ extraOptions()
+
+ DataEntry(
+ iconRes = RBase.drawable.ic_timer,
+ nameRes = RBase.string.main_action_timers,
+ include = screen.includeTimers,
+ onIncludedChange = screen.onTimersChange,
+ )
+ DataEntry(
+ iconRes = RBase.drawable.ic_scheduler,
+ nameRes = RBase.string.main_action_schedulers,
+ include = screen.includeSchedulers,
+ onIncludedChange = screen.onSchedulersChange,
+ )
+ DataEntry(
+ iconRes = RBase.drawable.ic_stat,
+ nameRes = RBase.string.main_action_record,
+ include = screen.includeRecords,
+ onIncludedChange = screen.onRecordsChange,
+ )
+ DataEntry(
+ iconRes = RBase.drawable.ic_settings,
+ nameRes = RBase.string.main_action_settings,
+ include = screen.includeSettings,
+ onIncludedChange = screen.onSettingsChange,
+ )
+
+ Button(
+ onClick = screen.onBackup,
+ modifier = Modifier.align(Alignment.CenterHorizontally),
+ enabled = !screen.contentLocation.isNullOrBlank() &&
+ (screen.includeTimers || screen.includeSettings) &&
+ !screen.backupOngoing,
+ ) {
+ Text(text = backupButtonText)
+ }
+ }
+
+ if (screen.backupErrorMessage != null) {
+ BackupError(
+ hint = backupErrorHint,
+ message = screen.backupErrorMessage,
+ consume = screen.consumeBackupError,
+ )
+ }
+
+ if (screen.backupOngoing) {
+ BackupOngoingDialog()
+ }
+ }
+}
+
+@Composable
+private fun Header(
+ text: String,
+ modifier: Modifier = Modifier,
+) {
+ Text(
+ text = text,
+ modifier = modifier.padding(horizontal = 12.dp, vertical = 12.dp),
+ style = MaterialTheme.typography.titleSmall,
+ )
+}
+
+@Composable
+private fun ContentLocation(
+ title: String,
+ location: String?,
+ modifier: Modifier = Modifier,
+) {
+ if (location.isNullOrBlank()) {
+ ContentLocationEmpty(title = title, modifier = modifier)
+ } else {
+ ContentLocationPath(title = title, location = location, modifier = modifier)
+ }
+}
+
+@Composable
+private fun ContentLocationPath(
+ title: String,
+ location: String,
+ modifier: Modifier
+) {
+ ListItem(
+ headlineContent = {
+ Text(text = location)
+ },
+ modifier = modifier,
+ overlineContent = {
+ Text(text = title)
+ },
+ leadingContent = {
+ Icon(imageVector = Icons.Rounded.Description, contentDescription = null)
+ },
+ trailingContent = {
+ Icon(
+ imageVector = Icons.AutoMirrored.Rounded.NavigateNext,
+ contentDescription = null,
+ )
+ },
+ )
+}
+
+@Composable
+private fun ContentLocationEmpty(
+ title: String,
+ modifier: Modifier
+) {
+ ListItem(
+ headlineContent = {
+ Text(text = title)
+ },
+ modifier = modifier,
+ leadingContent = {
+ Icon(imageVector = Icons.Rounded.Description, contentDescription = null)
+ },
+ trailingContent = {
+ Icon(
+ imageVector = Icons.AutoMirrored.Rounded.NavigateNext,
+ contentDescription = null,
+ )
+ },
+ )
+}
+
+@Composable
+private fun DataEntry(
+ @DrawableRes iconRes: Int,
+ @StringRes nameRes: Int,
+ include: Boolean,
+ onIncludedChange: (Boolean) -> Unit,
+ modifier: Modifier = Modifier,
+) {
+ ListItem(
+ headlineContent = {
+ Text(text = stringResource(id = nameRes))
+ },
+ modifier = modifier.clickable { onIncludedChange(!include) },
+ leadingContent = {
+ Icon(painter = painterResource(id = iconRes), contentDescription = null)
+ },
+ trailingContent = {
+ Switch(checked = include, onCheckedChange = onIncludedChange)
+ },
+ )
+}
+
+@Composable
+private fun BackupError(
+ hint: String,
+ message: String,
+ consume: () -> Unit,
+) {
+ AlertDialog(
+ onDismissRequest = consume,
+ confirmButton = {
+ TextButton(onClick = consume) {
+ Text(text = stringResource(id = RBase.string.ok))
+ }
+ },
+ text = {
+ Text(
+ text = buildString {
+ append(hint)
+
+ if (message.isNotBlank()) {
+ append("\n\n")
+ append(message)
+ }
+ },
+ )
+ }
+ )
+}
+
+@Composable
+private fun BackupOngoingDialog() {
+ Dialog(onDismissRequest = {}) {
+ Surface(shape = MaterialTheme.shapes.medium) {
+ CircularProgressIndicator(modifier = Modifier.padding(24.dp))
+ }
+ }
+}
diff --git a/app-backup/src/main/java/xyz/aprildown/timer/app/backup/BaseBackupViewModel.kt b/app-backup/src/main/java/xyz/aprildown/timer/app/backup/BaseBackupViewModel.kt
new file mode 100644
index 00000000..24000019
--- /dev/null
+++ b/app-backup/src/main/java/xyz/aprildown/timer/app/backup/BaseBackupViewModel.kt
@@ -0,0 +1,116 @@
+package xyz.aprildown.timer.app.backup
+
+import androidx.compose.runtime.Immutable
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.update
+import kotlinx.coroutines.launch
+import xyz.aprildown.timer.domain.usecases.Fruit
+
+internal abstract class BaseBackupViewModel : ViewModel() {
+ @Immutable
+ data class Screen(
+ val contentLocation: String? = null,
+ val contentName: String? = null,
+
+ val includeTimers: Boolean = true,
+ val onTimersChange: (Boolean) -> Unit,
+
+ val includeRecords: Boolean = true,
+ val onRecordsChange: (Boolean) -> Unit,
+
+ val includeSchedulers: Boolean = true,
+ val onSchedulersChange: (Boolean) -> Unit,
+
+ val includeSettings: Boolean = true,
+ val onSettingsChange: (Boolean) -> Unit,
+
+ val onBackup: () -> Unit,
+ val backupOngoing: Boolean = false,
+
+ val backupErrorMessage: String? = null,
+ val consumeBackupError: () -> Unit,
+ )
+
+ private val _screen: MutableStateFlow = MutableStateFlow(
+ Screen(
+ onTimersChange = ::onTimersChange,
+ onRecordsChange = ::onRecordsChange,
+ onSchedulersChange = ::onSchedulersChange,
+ onSettingsChange = ::onSettingsChange,
+ onBackup = ::onBackup,
+ consumeBackupError = ::consumeBackupError,
+ )
+ )
+ val screen: StateFlow = _screen.asStateFlow()
+
+ fun changeContentLocation(location: String, name: String?) {
+ _screen.update { it.copy(contentLocation = location, contentName = name) }
+ }
+
+ private fun onTimersChange(include: Boolean) {
+ _screen.update {
+ it.copy(
+ includeTimers = include,
+ includeRecords = include,
+ includeSchedulers = include,
+ )
+ }
+ }
+
+ private fun onRecordsChange(include: Boolean) {
+ _screen.update {
+ it.copy(
+ includeTimers = if (include) true else it.includeTimers,
+ includeRecords = include,
+ )
+ }
+ }
+
+ private fun onSchedulersChange(include: Boolean) {
+ _screen.update {
+ it.copy(
+ includeTimers = if (include) true else it.includeTimers,
+ includeSchedulers = include,
+ )
+ }
+ }
+
+ private fun onSettingsChange(include: Boolean) {
+ _screen.update {
+ it.copy(includeSettings = include)
+ }
+ }
+
+ abstract suspend fun backup(screen: Screen): Fruit
+
+ private fun onBackup() {
+ val screen = _screen.value
+ if (screen.contentLocation.isNullOrBlank()) return
+ if (!screen.includeTimers && !screen.includeSettings) return
+ if (screen.backupOngoing) return
+
+ viewModelScope.launch {
+ _screen.update { it.copy(backupOngoing = true) }
+ try {
+ when (val fruit = backup(screen)) {
+ is Fruit.Ripe -> Unit
+ is Fruit.Rotten -> {
+ _screen.update {
+ it.copy(backupErrorMessage = fruit.exception.message)
+ }
+ }
+ }
+ } finally {
+ _screen.update { it.copy(backupOngoing = false) }
+ }
+ }
+ }
+
+ private fun consumeBackupError() {
+ _screen.update { it.copy(backupErrorMessage = null) }
+ }
+}
diff --git a/app-backup/src/main/java/xyz/aprildown/timer/app/backup/Export2Fragment.kt b/app-backup/src/main/java/xyz/aprildown/timer/app/backup/Export2Fragment.kt
index 82c8ac53..9aac75c7 100644
--- a/app-backup/src/main/java/xyz/aprildown/timer/app/backup/Export2Fragment.kt
+++ b/app-backup/src/main/java/xyz/aprildown/timer/app/backup/Export2Fragment.kt
@@ -9,39 +9,14 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.activity.result.contract.ActivityResultContracts
-import androidx.annotation.DrawableRes
-import androidx.annotation.StringRes
-import androidx.compose.foundation.clickable
-import androidx.compose.foundation.layout.Arrangement
-import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.rememberScrollState
-import androidx.compose.foundation.verticalScroll
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.automirrored.rounded.NavigateNext
-import androidx.compose.material.icons.rounded.Description
-import androidx.compose.material3.AlertDialog
-import androidx.compose.material3.Button
-import androidx.compose.material3.CircularProgressIndicator
-import androidx.compose.material3.Icon
-import androidx.compose.material3.ListItem
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.Surface
-import androidx.compose.material3.Switch
-import androidx.compose.material3.Text
-import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
-import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.ComposeView
-import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.ViewCompositionStrategy
-import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.unit.dp
-import androidx.compose.ui.window.Dialog
+import androidx.documentfile.provider.DocumentFile
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import com.github.deweyreed.tools.helper.requireCallback
@@ -88,7 +63,7 @@ class Export2Fragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
(view as ComposeView).setContent {
- Export2(
+ Export(
screen = viewModel.screen.collectAsState().value,
onLocationChange = ::onLocationChange,
modifier = Modifier.fillMaxSize(),
@@ -114,207 +89,31 @@ class Export2Fragment : Fragment() {
}
private fun handleActivityResult(resultCode: Int, data: Intent?) {
- if (resultCode == Activity.RESULT_OK) {
- val uri = data?.data ?: return
- if (uri.toString().isBlank() || uri == Uri.EMPTY) return
- val context = requireContext()
- context.lastBackupUri = uri
- viewModel.setSaveLocation(uri.toString())
- }
+ if (resultCode != Activity.RESULT_OK) return
+ val uri = data?.data ?: return
+ if (uri.toString().isBlank() || uri == Uri.EMPTY) return
+ val context = requireContext()
+ context.lastBackupUri = uri
+ viewModel.changeContentLocation(
+ location = uri.toString(),
+ name = DocumentFile.fromSingleUri(context, uri)?.name,
+ )
}
}
@Composable
-private fun Export2(
- screen: Export2ViewModel.Screen,
+private fun Export(
+ screen: BaseBackupViewModel.Screen,
onLocationChange: () -> Unit,
modifier: Modifier = Modifier,
) {
- MaterialTheme {
- Column(
- modifier = modifier
- .verticalScroll(rememberScrollState())
- .padding(16.dp),
- verticalArrangement = Arrangement.spacedBy(16.dp),
- ) {
- Header(text = stringResource(id = RBase.string.export_path_title))
-
- SaveLocation(
- location = screen.saveLocation,
- modifier = Modifier.clickable(onClick = onLocationChange),
- )
-
- Header(text = stringResource(id = RBase.string.backup_app_data))
-
- DataEntry(
- iconRes = RBase.drawable.ic_timer,
- nameRes = RBase.string.main_action_timers,
- include = screen.includeTimers,
- onIncludedChange = screen.onTimersChange,
- )
- DataEntry(
- iconRes = RBase.drawable.ic_scheduler,
- nameRes = RBase.string.main_action_schedulers,
- include = screen.includeSchedulers,
- onIncludedChange = screen.onSchedulersChange,
- )
- DataEntry(
- iconRes = RBase.drawable.ic_stat,
- nameRes = RBase.string.main_action_record,
- include = screen.includeRecords,
- onIncludedChange = screen.onRecordsChange,
- )
- DataEntry(
- iconRes = RBase.drawable.ic_settings,
- nameRes = RBase.string.main_action_settings,
- include = screen.includeSettings,
- onIncludedChange = screen.onSettingsChange,
- )
-
- Button(
- onClick = screen.onExport,
- modifier = Modifier.align(Alignment.CenterHorizontally),
- enabled = !screen.saveLocation.isNullOrBlank() &&
- (screen.includeTimers || screen.includeSettings) &&
- !screen.exporting,
- ) {
- Text(text = stringResource(id = RBase.string.export_action))
- }
- }
-
- if (screen.exportErrorMessage != null) {
- ExportError(
- message = screen.exportErrorMessage,
- consume = screen.consumeExportError,
- )
- }
-
- if (screen.exporting) {
- ExportingDialog()
- }
- }
-}
-
-@Composable
-private fun Header(
- text: String,
- modifier: Modifier = Modifier,
-) {
- Text(
- text = text,
- modifier = modifier.padding(horizontal = 12.dp, vertical = 12.dp),
- style = MaterialTheme.typography.titleSmall,
- )
-}
-
-@Composable
-private fun SaveLocation(
- location: String?,
- modifier: Modifier = Modifier,
-) {
- if (location.isNullOrBlank()) {
- SaveLocationEmpty(modifier = modifier)
- } else {
- SaveLocationPath(location = location, modifier = modifier)
- }
-}
-
-@Composable
-private fun SaveLocationPath(location: String, modifier: Modifier) {
- ListItem(
- headlineContent = {
- Text(text = location)
- },
+ Backup(
+ screen = screen,
+ contentLocationTitle = stringResource(id = RBase.string.export_path_title),
+ contentLocationButtonText = stringResource(id = RBase.string.export_select_location),
+ onChangeContentLocation = onLocationChange,
+ backupButtonText = stringResource(id = RBase.string.export_action),
+ backupErrorHint = stringResource(id = RBase.string.export_error),
modifier = modifier,
- overlineContent = {
- Text(text = stringResource(id = RBase.string.export_select_location))
- },
- leadingContent = {
- Icon(imageVector = Icons.Rounded.Description, contentDescription = null)
- },
- trailingContent = {
- Icon(
- imageVector = Icons.AutoMirrored.Rounded.NavigateNext,
- contentDescription = null,
- )
- },
)
}
-
-@Composable
-private fun SaveLocationEmpty(modifier: Modifier) {
- ListItem(
- headlineContent = {
- Text(text = stringResource(id = RBase.string.export_select_location))
- },
- modifier = modifier,
- leadingContent = {
- Icon(imageVector = Icons.Rounded.Description, contentDescription = null)
- },
- trailingContent = {
- Icon(
- imageVector = Icons.AutoMirrored.Rounded.NavigateNext,
- contentDescription = null,
- )
- },
- )
-}
-
-@Composable
-private fun DataEntry(
- @DrawableRes iconRes: Int,
- @StringRes nameRes: Int,
- include: Boolean,
- onIncludedChange: (Boolean) -> Unit,
- modifier: Modifier = Modifier,
-) {
- ListItem(
- headlineContent = {
- Text(text = stringResource(id = nameRes))
- },
- modifier = modifier.clickable { onIncludedChange(!include) },
- leadingContent = {
- Icon(painter = painterResource(id = iconRes), contentDescription = null)
- },
- trailingContent = {
- Switch(checked = include, onCheckedChange = onIncludedChange)
- },
- )
-}
-
-@Composable
-private fun ExportError(
- message: String,
- consume: () -> Unit,
-) {
- AlertDialog(
- onDismissRequest = consume,
- confirmButton = {
- TextButton(onClick = consume) {
- Text(text = stringResource(id = RBase.string.ok))
- }
- },
- text = {
- val context = LocalContext.current
- Text(
- text = buildString {
- append(context.getString(RBase.string.export_error))
-
- if (message.isNotBlank()) {
- append("\n\n")
- append(message)
- }
- },
- )
- }
- )
-}
-
-@Composable
-private fun ExportingDialog() {
- Dialog(onDismissRequest = {}) {
- Surface(shape = MaterialTheme.shapes.medium) {
- CircularProgressIndicator(modifier = Modifier.padding(24.dp))
- }
- }
-}
diff --git a/app-backup/src/main/java/xyz/aprildown/timer/app/backup/Export2ViewModel.kt b/app-backup/src/main/java/xyz/aprildown/timer/app/backup/Export2ViewModel.kt
index 7ef7ce3f..ba242d44 100644
--- a/app-backup/src/main/java/xyz/aprildown/timer/app/backup/Export2ViewModel.kt
+++ b/app-backup/src/main/java/xyz/aprildown/timer/app/backup/Export2ViewModel.kt
@@ -1,111 +1,14 @@
package xyz.aprildown.timer.app.backup
-import androidx.compose.runtime.Immutable
-import androidx.lifecycle.ViewModel
-import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.delay
-import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.StateFlow
-import kotlinx.coroutines.flow.asStateFlow
-import kotlinx.coroutines.flow.update
-import kotlinx.coroutines.launch
+import xyz.aprildown.timer.domain.usecases.Fruit
import javax.inject.Inject
@HiltViewModel
-internal class Export2ViewModel @Inject constructor() : ViewModel() {
- @Immutable
- data class Screen(
- val saveLocation: String? = null,
-
- val includeTimers: Boolean = true,
- val onTimersChange: (Boolean) -> Unit,
-
- val includeRecords: Boolean = true,
- val onRecordsChange: (Boolean) -> Unit,
-
- val includeSchedulers: Boolean = true,
- val onSchedulersChange: (Boolean) -> Unit,
-
- val includeSettings: Boolean = true,
- val onSettingsChange: (Boolean) -> Unit,
-
- val onExport: () -> Unit,
- val exporting: Boolean = false,
-
- val exportErrorMessage: String? = null,
- val consumeExportError: () -> Unit,
- )
-
- private val _screen: MutableStateFlow = MutableStateFlow(
- Screen(
- onTimersChange = ::onTimersChange,
- onRecordsChange = ::onRecordsChange,
- onSchedulersChange = ::onSchedulersChange,
- onSettingsChange = ::onSettingsChange,
- onExport = ::onExport,
- consumeExportError = ::consumeExportError,
- )
- )
- val screen: StateFlow = _screen.asStateFlow()
-
- fun setSaveLocation(location: String) {
- _screen.update { it.copy(saveLocation = location) }
- }
-
- private fun onTimersChange(include: Boolean) {
- _screen.update {
- it.copy(
- includeTimers = include,
- includeRecords = include,
- includeSchedulers = include,
- )
- }
- }
-
- private fun onRecordsChange(include: Boolean) {
- _screen.update {
- it.copy(
- includeTimers = if (include) true else it.includeTimers,
- includeRecords = include,
- )
- }
- }
-
- private fun onSchedulersChange(include: Boolean) {
- _screen.update {
- it.copy(
- includeTimers = if (include) true else it.includeTimers,
- includeSchedulers = include,
- )
- }
- }
-
- private fun onSettingsChange(include: Boolean) {
- _screen.update {
- it.copy(includeSettings = include)
- }
- }
-
- private fun onExport() {
- val screen = _screen.value
- if (screen.saveLocation.isNullOrBlank()) return
- if (!screen.includeTimers && !screen.includeSettings) return
- if (screen.exporting) return
- viewModelScope.launch {
- _screen.update { it.copy(exporting = true) }
- try {
- delay(5500)
- _screen.update {
- it.copy(exportErrorMessage = "Hey")
- }
- } finally {
- _screen.update { it.copy(exporting = false) }
- }
- }
- }
-
- private fun consumeExportError() {
- _screen.update { it.copy(exportErrorMessage = null) }
+internal class Export2ViewModel @Inject constructor() : BaseBackupViewModel() {
+ override suspend fun backup(screen: Screen): Fruit {
+ delay(5000)
+ return Fruit.Rotten("Hey")
}
}
diff --git a/app-backup/src/main/java/xyz/aprildown/timer/app/backup/Import2Fragment.kt b/app-backup/src/main/java/xyz/aprildown/timer/app/backup/Import2Fragment.kt
new file mode 100644
index 00000000..835f8202
--- /dev/null
+++ b/app-backup/src/main/java/xyz/aprildown/timer/app/backup/Import2Fragment.kt
@@ -0,0 +1,150 @@
+package xyz.aprildown.timer.app.backup
+
+import android.app.Activity
+import android.content.Context
+import android.content.Intent
+import android.net.Uri
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.activity.result.contract.ActivityResultContracts
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.material3.Icon
+import androidx.compose.material3.ListItem
+import androidx.compose.material3.Switch
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.collectAsState
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.ComposeView
+import androidx.compose.ui.platform.ViewCompositionStrategy
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import androidx.documentfile.provider.DocumentFile
+import androidx.fragment.app.Fragment
+import androidx.fragment.app.viewModels
+import com.github.deweyreed.tools.helper.requireCallback
+import dagger.hilt.android.AndroidEntryPoint
+import xyz.aprildown.timer.app.base.data.PreferenceData.lastBackupUri
+import xyz.aprildown.timer.app.base.ui.MainCallback
+import xyz.aprildown.timer.domain.utils.AppTracker
+import javax.inject.Inject
+import xyz.aprildown.timer.app.base.R as RBase
+
+@AndroidEntryPoint
+class Import2Fragment : Fragment() {
+
+ private val viewModel: Import2ViewModel by viewModels()
+
+ private lateinit var mainCallback: MainCallback.ActivityCallback
+
+ @Inject
+ lateinit var appTracker: AppTracker
+
+ private val launcher =
+ registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
+ handleActivityResult(resultCode = it.resultCode, data = it.data)
+ }
+
+ override fun onAttach(context: Context) {
+ super.onAttach(context)
+ mainCallback = requireCallback()
+ }
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View {
+ return ComposeView(requireContext()).apply {
+ setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
+ }
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+ (view as ComposeView).setContent {
+ Import(
+ screen = viewModel.screen.collectAsState().value,
+ importScreen = viewModel.importScreen.collectAsState().value,
+ onLocationChange = ::onLocationChange,
+ modifier = Modifier.fillMaxSize(),
+ )
+ }
+ }
+
+ private fun onLocationChange() {
+ SafIntentSafeBelt(
+ context = requireContext(),
+ appTracker = appTracker,
+ viewForSnackbar = mainCallback.snackbarView
+ ).drive(
+ launcher = launcher,
+ intent = Intent(Intent.ACTION_OPEN_DOCUMENT)
+ .addCategory(Intent.CATEGORY_OPENABLE)
+ .setType("*/*")
+ )
+ }
+
+ private fun handleActivityResult(resultCode: Int, data: Intent?) {
+ if (resultCode != Activity.RESULT_OK) return
+ val uri = data?.data ?: return
+ if (uri.toString().isBlank() || uri == Uri.EMPTY) return
+ val context = requireContext()
+ context.lastBackupUri = uri
+ viewModel.changeContentLocation(
+ location = uri.toString(),
+ name = DocumentFile.fromSingleUri(context, uri)?.name,
+ )
+ }
+}
+
+@Composable
+private fun Import(
+ screen: BaseBackupViewModel.Screen,
+ importScreen: Import2ViewModel.ImportScreen,
+ onLocationChange: () -> Unit,
+ modifier: Modifier = Modifier,
+) {
+ Backup(
+ screen = screen,
+ contentLocationTitle = stringResource(id = RBase.string.import_path_title),
+ contentLocationButtonText = stringResource(id = RBase.string.import_select_location),
+ onChangeContentLocation = onLocationChange,
+ backupButtonText = stringResource(id = RBase.string.import_action),
+ backupErrorHint = stringResource(id = RBase.string.import_error),
+ modifier = modifier,
+ extraOptions = {
+ WipeContent(
+ wipe = importScreen.wipe,
+ onWipeChanged = importScreen.onWipeChanged,
+ modifier = Modifier.fillMaxSize(),
+ )
+ },
+ )
+}
+
+@Composable
+private fun WipeContent(
+ wipe: Boolean,
+ onWipeChanged: (Boolean) -> Unit,
+ modifier: Modifier = Modifier
+) {
+ ListItem(
+ headlineContent = {
+ Text(text = stringResource(id = RBase.string.import_wipe_first))
+ },
+ modifier = modifier.clickable { onWipeChanged(!wipe) },
+ leadingContent = {
+ Icon(
+ painter = painterResource(id = RBase.drawable.ic_delete),
+ contentDescription = null,
+ )
+ },
+ trailingContent = {
+ Switch(checked = wipe, onCheckedChange = onWipeChanged)
+ },
+ )
+}
diff --git a/app-backup/src/main/java/xyz/aprildown/timer/app/backup/Import2ViewModel.kt b/app-backup/src/main/java/xyz/aprildown/timer/app/backup/Import2ViewModel.kt
new file mode 100644
index 00000000..311dc810
--- /dev/null
+++ b/app-backup/src/main/java/xyz/aprildown/timer/app/backup/Import2ViewModel.kt
@@ -0,0 +1,36 @@
+package xyz.aprildown.timer.app.backup
+
+import androidx.compose.runtime.Immutable
+import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.update
+import xyz.aprildown.timer.domain.usecases.Fruit
+import javax.inject.Inject
+
+@HiltViewModel
+internal class Import2ViewModel @Inject constructor() : BaseBackupViewModel() {
+ @Immutable
+ data class ImportScreen(
+ val wipe: Boolean = false,
+ val onWipeChanged: (Boolean) -> Unit,
+ )
+
+ private val _importScreen: MutableStateFlow = MutableStateFlow(
+ ImportScreen(
+ onWipeChanged = ::onWipeChanged,
+ )
+ )
+ val importScreen: StateFlow = _importScreen.asStateFlow()
+
+ private fun onWipeChanged(wipe: Boolean) {
+ _importScreen.update { it.copy(wipe = wipe) }
+ }
+
+ override suspend fun backup(screen: Screen): Fruit {
+ delay(5000)
+ return Fruit.Rotten("Hey")
+ }
+}
diff --git a/app/src/main/res/navigation/nav_graph.xml b/app/src/main/res/navigation/nav_graph.xml
index e8621021..d2ce3717 100644
--- a/app/src/main/res/navigation/nav_graph.xml
+++ b/app/src/main/res/navigation/nav_graph.xml
@@ -66,9 +66,8 @@
+ android:name="xyz.aprildown.timer.app.backup.Import2Fragment"
+ android:label="@string/import_data_title" />
Date: Wed, 17 Apr 2024 11:09:28 +0800
Subject: [PATCH 14/31] Export and import the data
---
.../timer/app/backup/BackupComposables.kt | 22 +++--
.../timer/app/backup/BaseBackupViewModel.kt | 41 ++++----
.../timer/app/backup/Export2Fragment.kt | 38 ++++++-
.../timer/app/backup/Export2ViewModel.kt | 55 ++++++++++-
.../timer/app/backup/Import2Fragment.kt | 98 +++++++++++++++++--
.../timer/app/backup/Import2ViewModel.kt | 33 ++++++-
domain/build.gradle | 2 +
7 files changed, 240 insertions(+), 49 deletions(-)
diff --git a/app-backup/src/main/java/xyz/aprildown/timer/app/backup/BackupComposables.kt b/app-backup/src/main/java/xyz/aprildown/timer/app/backup/BackupComposables.kt
index 81f4a151..6eaa287c 100644
--- a/app-backup/src/main/java/xyz/aprildown/timer/app/backup/BackupComposables.kt
+++ b/app-backup/src/main/java/xyz/aprildown/timer/app/backup/BackupComposables.kt
@@ -7,6 +7,7 @@ import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.selection.toggleable
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.rounded.NavigateNext
@@ -26,13 +27,15 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.semantics.Role
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
+import xyz.aprildown.timer.domain.usecases.Fruit
import xyz.aprildown.timer.app.base.R as RBase
@Composable
internal fun Backup(
- screen: BaseBackupViewModel.Screen,
+ screen: BaseBackupViewModel.Screen<*>,
contentLocationTitle: String,
contentLocationButtonText: String,
onChangeContentLocation: () -> Unit,
@@ -52,7 +55,7 @@ internal fun Backup(
ContentLocation(
title = contentLocationButtonText,
- location = screen.contentName ?: screen.contentLocation,
+ location = screen.contentName,
modifier = Modifier.clickable(onClick = onChangeContentLocation),
)
@@ -88,7 +91,7 @@ internal fun Backup(
Button(
onClick = screen.onBackup,
modifier = Modifier.align(Alignment.CenterHorizontally),
- enabled = !screen.contentLocation.isNullOrBlank() &&
+ enabled = screen.content != null &&
(screen.includeTimers || screen.includeSettings) &&
!screen.backupOngoing,
) {
@@ -96,10 +99,10 @@ internal fun Backup(
}
}
- if (screen.backupErrorMessage != null) {
+ if (screen.backupResult?.fruit is Fruit.Rotten) {
BackupError(
hint = backupErrorHint,
- message = screen.backupErrorMessage,
+ message = (screen.backupResult.fruit as Fruit.Rotten).exception.message.toString(),
consume = screen.consumeBackupError,
)
}
@@ -195,12 +198,17 @@ private fun DataEntry(
headlineContent = {
Text(text = stringResource(id = nameRes))
},
- modifier = modifier.clickable { onIncludedChange(!include) },
+ modifier = modifier
+ .toggleable(
+ value = include,
+ role = Role.Switch,
+ onValueChange = onIncludedChange
+ ),
leadingContent = {
Icon(painter = painterResource(id = iconRes), contentDescription = null)
},
trailingContent = {
- Switch(checked = include, onCheckedChange = onIncludedChange)
+ Switch(checked = include, onCheckedChange = null)
},
)
}
diff --git a/app-backup/src/main/java/xyz/aprildown/timer/app/backup/BaseBackupViewModel.kt b/app-backup/src/main/java/xyz/aprildown/timer/app/backup/BaseBackupViewModel.kt
index 24000019..e820bcea 100644
--- a/app-backup/src/main/java/xyz/aprildown/timer/app/backup/BaseBackupViewModel.kt
+++ b/app-backup/src/main/java/xyz/aprildown/timer/app/backup/BaseBackupViewModel.kt
@@ -10,10 +10,11 @@ import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import xyz.aprildown.timer.domain.usecases.Fruit
-internal abstract class BaseBackupViewModel : ViewModel() {
+internal abstract class BaseBackupViewModel :
+ ViewModel() {
@Immutable
- data class Screen(
- val contentLocation: String? = null,
+ data class Screen(
+ val content: ContentType? = null,
val contentName: String? = null,
val includeTimers: Boolean = true,
@@ -31,11 +32,17 @@ internal abstract class BaseBackupViewModel : ViewModel() {
val onBackup: () -> Unit,
val backupOngoing: Boolean = false,
- val backupErrorMessage: String? = null,
+ val backupResult: Result? = null,
val consumeBackupError: () -> Unit,
- )
+ ) {
+ @Immutable
+ interface Content
+
+ @Immutable
+ data class Result(val fruit: Fruit)
+ }
- private val _screen: MutableStateFlow = MutableStateFlow(
+ private val _screen: MutableStateFlow> = MutableStateFlow(
Screen(
onTimersChange = ::onTimersChange,
onRecordsChange = ::onRecordsChange,
@@ -45,10 +52,10 @@ internal abstract class BaseBackupViewModel : ViewModel() {
consumeBackupError = ::consumeBackupError,
)
)
- val screen: StateFlow = _screen.asStateFlow()
+ val screen: StateFlow> = _screen.asStateFlow()
- fun changeContentLocation(location: String, name: String?) {
- _screen.update { it.copy(contentLocation = location, contentName = name) }
+ fun changeContent(content: ContentType, name: String?) {
+ _screen.update { it.copy(content = content, contentName = name) }
}
private fun onTimersChange(include: Boolean) {
@@ -85,24 +92,20 @@ internal abstract class BaseBackupViewModel : ViewModel() {
}
}
- abstract suspend fun backup(screen: Screen): Fruit
+ abstract suspend fun backup(screen: Screen): Fruit
private fun onBackup() {
val screen = _screen.value
- if (screen.contentLocation.isNullOrBlank()) return
+ if (screen.content == null) return
if (!screen.includeTimers && !screen.includeSettings) return
if (screen.backupOngoing) return
viewModelScope.launch {
_screen.update { it.copy(backupOngoing = true) }
try {
- when (val fruit = backup(screen)) {
- is Fruit.Ripe -> Unit
- is Fruit.Rotten -> {
- _screen.update {
- it.copy(backupErrorMessage = fruit.exception.message)
- }
- }
+ val result = Screen.Result(backup(screen))
+ _screen.update {
+ it.copy(backupResult = result)
}
} finally {
_screen.update { it.copy(backupOngoing = false) }
@@ -111,6 +114,6 @@ internal abstract class BaseBackupViewModel : ViewModel() {
}
private fun consumeBackupError() {
- _screen.update { it.copy(backupErrorMessage = null) }
+ _screen.update { it.copy(backupResult = null) }
}
}
diff --git a/app-backup/src/main/java/xyz/aprildown/timer/app/backup/Export2Fragment.kt b/app-backup/src/main/java/xyz/aprildown/timer/app/backup/Export2Fragment.kt
index 9aac75c7..5acd6f7c 100644
--- a/app-backup/src/main/java/xyz/aprildown/timer/app/backup/Export2Fragment.kt
+++ b/app-backup/src/main/java/xyz/aprildown/timer/app/backup/Export2Fragment.kt
@@ -11,7 +11,9 @@ import android.view.ViewGroup
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.platform.ViewCompositionStrategy
@@ -19,10 +21,14 @@ import androidx.compose.ui.res.stringResource
import androidx.documentfile.provider.DocumentFile
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
+import androidx.navigation.fragment.findNavController
+import com.github.deweyreed.tools.anko.longSnackbar
import com.github.deweyreed.tools.helper.requireCallback
import dagger.hilt.android.AndroidEntryPoint
+import okio.sink
import xyz.aprildown.timer.app.base.data.PreferenceData.lastBackupUri
import xyz.aprildown.timer.app.base.ui.MainCallback
+import xyz.aprildown.timer.domain.usecases.Fruit
import xyz.aprildown.timer.domain.utils.AppTracker
import java.text.SimpleDateFormat
import java.util.Date
@@ -62,9 +68,21 @@ class Export2Fragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
+ val navController = findNavController()
(view as ComposeView).setContent {
+ val screen by viewModel.screen.collectAsState()
+ LaunchedEffect(screen.backupResult) {
+ val result = screen.backupResult
+ if (result?.fruit is Fruit.Ripe) {
+ mainCallback.snackbarView.longSnackbar(RBase.string.export_done)
+ navController.popBackStack(
+ destinationId = RBase.id.dest_backup_restore,
+ inclusive = false,
+ )
+ }
+ }
Export(
- screen = viewModel.screen.collectAsState().value,
+ screen = screen,
onLocationChange = ::onLocationChange,
modifier = Modifier.fillMaxSize(),
)
@@ -94,16 +112,26 @@ class Export2Fragment : Fragment() {
if (uri.toString().isBlank() || uri == Uri.EMPTY) return
val context = requireContext()
context.lastBackupUri = uri
- viewModel.changeContentLocation(
- location = uri.toString(),
- name = DocumentFile.fromSingleUri(context, uri)?.name,
+ val contentResolver = context.contentResolver
+ val documentFile = DocumentFile.fromSingleUri(context, uri)
+ viewModel.changeContent(
+ content = Export2ViewModel.WritableContent(
+ getSink = {
+ checkNotNull(contentResolver.openOutputStream(uri)).sink()
+ },
+ delete = {
+ // ContentResolver.delete doesn't work
+ documentFile?.delete()
+ },
+ ),
+ name = documentFile?.name?.takeIf { it.isNotBlank() } ?: uri.toString(),
)
}
}
@Composable
private fun Export(
- screen: BaseBackupViewModel.Screen,
+ screen: BaseBackupViewModel.Screen<*>,
onLocationChange: () -> Unit,
modifier: Modifier = Modifier,
) {
diff --git a/app-backup/src/main/java/xyz/aprildown/timer/app/backup/Export2ViewModel.kt b/app-backup/src/main/java/xyz/aprildown/timer/app/backup/Export2ViewModel.kt
index ba242d44..4b193e80 100644
--- a/app-backup/src/main/java/xyz/aprildown/timer/app/backup/Export2ViewModel.kt
+++ b/app-backup/src/main/java/xyz/aprildown/timer/app/backup/Export2ViewModel.kt
@@ -1,14 +1,59 @@
package xyz.aprildown.timer.app.backup
+import androidx.lifecycle.SavedStateHandle
import dagger.hilt.android.lifecycle.HiltViewModel
-import kotlinx.coroutines.delay
+import okio.Sink
+import okio.buffer
import xyz.aprildown.timer.domain.usecases.Fruit
+import xyz.aprildown.timer.domain.usecases.data.ExportAppData
+import java.io.IOException
import javax.inject.Inject
@HiltViewModel
-internal class Export2ViewModel @Inject constructor() : BaseBackupViewModel() {
- override suspend fun backup(screen: Screen): Fruit {
- delay(5000)
- return Fruit.Rotten("Hey")
+internal class Export2ViewModel @Inject constructor(
+ private val savedStateHandle: SavedStateHandle,
+ private val exportAppData: ExportAppData,
+) : BaseBackupViewModel() {
+ class WritableContent(
+ val getSink: () -> Sink,
+ val delete: () -> Unit,
+ ) : Screen.Content
+
+ override suspend fun backup(screen: Screen): Fruit {
+ return try {
+ val string = exportAppData(
+ ExportAppData.Params(
+ exportTimers = screen.includeTimers,
+ exportTimerStamps = screen.includeRecords,
+ exportSchedulers = screen.includeSchedulers,
+ exportPreferences = screen.includeSettings,
+ )
+ )
+ checkNotNull(screen.content).getSink().use {
+ it.buffer().run {
+ writeUtf8(string)
+ flush()
+ }
+ }
+ savedStateHandle[KEY_HAS_EXPORTED] = true
+ Fruit.Ripe(Unit)
+ } catch (e: IOException) {
+ Fruit.Rotten(e)
+ }
+ }
+
+ override fun onCleared() {
+ super.onCleared()
+ if (savedStateHandle.get(KEY_HAS_EXPORTED) != true) {
+ try {
+ screen.value.content?.delete?.invoke()
+ } catch (_: Exception) {
+ // Ignore
+ }
+ }
+ }
+
+ companion object {
+ private const val KEY_HAS_EXPORTED = "has_exported"
}
}
diff --git a/app-backup/src/main/java/xyz/aprildown/timer/app/backup/Import2Fragment.kt b/app-backup/src/main/java/xyz/aprildown/timer/app/backup/Import2Fragment.kt
index 835f8202..59e0f6b3 100644
--- a/app-backup/src/main/java/xyz/aprildown/timer/app/backup/Import2Fragment.kt
+++ b/app-backup/src/main/java/xyz/aprildown/timer/app/backup/Import2Fragment.kt
@@ -9,26 +9,37 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.activity.result.contract.ActivityResultContracts
-import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.selection.toggleable
+import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Icon
import androidx.compose.material3.ListItem
import androidx.compose.material3.Switch
import androidx.compose.material3.Text
+import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.platform.ViewCompositionStrategy
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.semantics.Role
import androidx.documentfile.provider.DocumentFile
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import com.github.deweyreed.tools.helper.requireCallback
+import com.github.deweyreed.tools.helper.restartWithFading
import dagger.hilt.android.AndroidEntryPoint
+import okio.source
import xyz.aprildown.timer.app.base.data.PreferenceData.lastBackupUri
+import xyz.aprildown.timer.app.base.ui.AppNavigator
import xyz.aprildown.timer.app.base.ui.MainCallback
+import xyz.aprildown.timer.domain.usecases.Fruit
import xyz.aprildown.timer.domain.utils.AppTracker
import javax.inject.Inject
import xyz.aprildown.timer.app.base.R as RBase
@@ -40,6 +51,9 @@ class Import2Fragment : Fragment() {
private lateinit var mainCallback: MainCallback.ActivityCallback
+ @Inject
+ lateinit var appNavigator: AppNavigator
+
@Inject
lateinit var appTracker: AppTracker
@@ -66,12 +80,21 @@ class Import2Fragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
(view as ComposeView).setContent {
+ val screen by viewModel.screen.collectAsState()
Import(
- screen = viewModel.screen.collectAsState().value,
+ screen = screen,
importScreen = viewModel.importScreen.collectAsState().value,
onLocationChange = ::onLocationChange,
modifier = Modifier.fillMaxSize(),
)
+
+ if (screen.backupResult?.fruit is Fruit.Ripe) {
+ RestartDialog(
+ onRestart = {
+ requireActivity().restartWithFading(appNavigator.getMainIntent())
+ },
+ )
+ }
}
}
@@ -94,16 +117,22 @@ class Import2Fragment : Fragment() {
if (uri.toString().isBlank() || uri == Uri.EMPTY) return
val context = requireContext()
context.lastBackupUri = uri
- viewModel.changeContentLocation(
- location = uri.toString(),
- name = DocumentFile.fromSingleUri(context, uri)?.name,
+ val contentResolver = context.contentResolver
+ val documentFile = DocumentFile.fromSingleUri(context, uri)
+ viewModel.changeContent(
+ content = Import2ViewModel.ReadableContent(
+ getSource = {
+ checkNotNull(contentResolver.openInputStream(uri)).source()
+ }
+ ),
+ name = documentFile?.name?.takeIf { it.isNotBlank() },
)
}
}
@Composable
private fun Import(
- screen: BaseBackupViewModel.Screen,
+ screen: BaseBackupViewModel.Screen<*>,
importScreen: Import2ViewModel.ImportScreen,
onLocationChange: () -> Unit,
modifier: Modifier = Modifier,
@@ -132,11 +161,27 @@ private fun WipeContent(
onWipeChanged: (Boolean) -> Unit,
modifier: Modifier = Modifier
) {
+ var showAlert by remember { mutableStateOf(false) }
+ val onWipeChangedAlert: (Boolean) -> Unit = remember(onWipeChanged) {
+ { wipe: Boolean ->
+ if (wipe) {
+ showAlert = true
+ } else {
+ onWipeChanged(false)
+ }
+ }
+ }
+
ListItem(
headlineContent = {
Text(text = stringResource(id = RBase.string.import_wipe_first))
},
- modifier = modifier.clickable { onWipeChanged(!wipe) },
+ modifier = modifier
+ .toggleable(
+ value = wipe,
+ role = Role.Switch,
+ onValueChange = onWipeChangedAlert,
+ ),
leadingContent = {
Icon(
painter = painterResource(id = RBase.drawable.ic_delete),
@@ -144,7 +189,44 @@ private fun WipeContent(
)
},
trailingContent = {
- Switch(checked = wipe, onCheckedChange = onWipeChanged)
+ Switch(checked = wipe, onCheckedChange = null)
+ },
+ )
+
+ if (showAlert) {
+ AlertDialog(
+ onDismissRequest = { showAlert = false },
+ confirmButton = {
+ TextButton(
+ onClick = {
+ onWipeChanged(true)
+ showAlert = false
+ },
+ ) {
+ Text(text = stringResource(id = RBase.string.ok))
+ }
+ },
+ text = {
+ Text(text = stringResource(id = RBase.string.import_wipe_warning))
+ },
+ )
+ }
+}
+
+@Composable
+private fun RestartDialog(onRestart: () -> Unit) {
+ AlertDialog(
+ onDismissRequest = {},
+ confirmButton = {
+ TextButton(onClick = onRestart) {
+ Text(text = stringResource(id = RBase.string.import_restart))
+ }
+ },
+ title = {
+ Text(text = stringResource(id = RBase.string.import_done))
+ },
+ text = {
+ Text(text = stringResource(id = RBase.string.import_restart_content))
},
)
}
diff --git a/app-backup/src/main/java/xyz/aprildown/timer/app/backup/Import2ViewModel.kt b/app-backup/src/main/java/xyz/aprildown/timer/app/backup/Import2ViewModel.kt
index 311dc810..4dfe6593 100644
--- a/app-backup/src/main/java/xyz/aprildown/timer/app/backup/Import2ViewModel.kt
+++ b/app-backup/src/main/java/xyz/aprildown/timer/app/backup/Import2ViewModel.kt
@@ -2,22 +2,31 @@ package xyz.aprildown.timer.app.backup
import androidx.compose.runtime.Immutable
import dagger.hilt.android.lifecycle.HiltViewModel
-import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
+import okio.Source
+import okio.buffer
import xyz.aprildown.timer.domain.usecases.Fruit
+import xyz.aprildown.timer.domain.usecases.data.ImportAppData
+import xyz.aprildown.timer.domain.usecases.data.NotifyDataChanged
+import java.io.IOException
import javax.inject.Inject
@HiltViewModel
-internal class Import2ViewModel @Inject constructor() : BaseBackupViewModel() {
+internal class Import2ViewModel @Inject constructor(
+ private val importAppData: ImportAppData,
+ private val notifyDataChanged: NotifyDataChanged,
+) : BaseBackupViewModel() {
@Immutable
data class ImportScreen(
val wipe: Boolean = false,
val onWipeChanged: (Boolean) -> Unit,
)
+ class ReadableContent(val getSource: () -> Source) : Screen.Content
+
private val _importScreen: MutableStateFlow = MutableStateFlow(
ImportScreen(
onWipeChanged = ::onWipeChanged,
@@ -29,8 +38,22 @@ internal class Import2ViewModel @Inject constructor() : BaseBackupViewModel() {
_importScreen.update { it.copy(wipe = wipe) }
}
- override suspend fun backup(screen: Screen): Fruit {
- delay(5000)
- return Fruit.Rotten("Hey")
+ override suspend fun backup(screen: Screen): Fruit {
+ return try {
+ importAppData(
+ ImportAppData.Params(
+ data = checkNotNull(screen.content).getSource().use { it.buffer().readUtf8() },
+ wipeFirst = _importScreen.value.wipe,
+ importTimers = screen.includeTimers,
+ importTimerStamps = screen.includeRecords,
+ importSchedulers = screen.includeSchedulers,
+ importPreferences = screen.includeSettings,
+ )
+ )
+ notifyDataChanged()
+ Fruit.Ripe(Unit)
+ } catch (e: IOException) {
+ Fruit.Rotten(e)
+ }
}
}
diff --git a/domain/build.gradle b/domain/build.gradle
index 7e24e99d..afe988cc 100644
--- a/domain/build.gradle
+++ b/domain/build.gradle
@@ -43,4 +43,6 @@ dependencies {
implementation libs.hilt.android
kapt libs.hilt.compiler
+ api libs.okio
+
}
From 23600ad749529bf0fbe1b0b8fbcc470603d516a1 Mon Sep 17 00:00:00 2001
From: DeweyReed <32358486+DeweyReed@users.noreply.github.com>
Date: Fri, 19 Apr 2024 09:10:35 +0800
Subject: [PATCH 15/31] Rename ImageAction.path to data
---
.../xyz/aprildown/timer/app/timer/edit/EditActivity.kt | 4 ++--
.../xyz/aprildown/timer/app/timer/one/OneFragment.kt | 2 +-
.../timer/component/key/behaviour/BehaviourLayout.kt | 2 +-
.../component/key/behaviour/EditableBehaviourLayout.kt | 2 +-
.../timer/data/repositories/TimerRepositoryImpl.kt | 4 ++--
.../aprildown/timer/domain/entities/BehaviourEntity.kt | 10 +++++++---
6 files changed, 14 insertions(+), 10 deletions(-)
diff --git a/app-timer-edit/src/main/java/xyz/aprildown/timer/app/timer/edit/EditActivity.kt b/app-timer-edit/src/main/java/xyz/aprildown/timer/app/timer/edit/EditActivity.kt
index 83d8ca7c..78b83360 100644
--- a/app-timer-edit/src/main/java/xyz/aprildown/timer/app/timer/edit/EditActivity.kt
+++ b/app-timer-edit/src/main/java/xyz/aprildown/timer/app/timer/edit/EditActivity.kt
@@ -108,7 +108,7 @@ class EditActivity :
if (position == -1) return@registerForActivityResult
contentResolver.takePersistableUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION)
changeBehaviour(type = BehaviourType.IMAGE, position = position) {
- it.toImageAction().copy(path = uri.toString()).toBehaviourEntity()
+ it.toImageAction().copy(data = uri.toString()).toBehaviourEntity()
}
}
@@ -860,7 +860,7 @@ class EditActivity :
}
override fun onImageCheck(position: Int, action: ImageAction) {
- startActivity(appNavigator.getImagePreviewIntent(action.path))
+ startActivity(appNavigator.getImagePreviewIntent(action.data))
}
private fun postUpdateTotalTime() {
diff --git a/app-timer-one/src/main/java/xyz/aprildown/timer/app/timer/one/OneFragment.kt b/app-timer-one/src/main/java/xyz/aprildown/timer/app/timer/one/OneFragment.kt
index dbaa4166..76dcdcf3 100644
--- a/app-timer-one/src/main/java/xyz/aprildown/timer/app/timer/one/OneFragment.kt
+++ b/app-timer-one/src/main/java/xyz/aprildown/timer/app/timer/one/OneFragment.kt
@@ -137,7 +137,7 @@ class OneFragment :
}
}
imageCheckListener = {
- startActivity(appNavigator.getImagePreviewIntent(it.path))
+ startActivity(appNavigator.getImagePreviewIntent(it.data))
}
}
diff --git a/component-key/src/main/java/xyz/aprildown/timer/component/key/behaviour/BehaviourLayout.kt b/component-key/src/main/java/xyz/aprildown/timer/component/key/behaviour/BehaviourLayout.kt
index 5d9ec98f..687aa7bc 100644
--- a/component-key/src/main/java/xyz/aprildown/timer/component/key/behaviour/BehaviourLayout.kt
+++ b/component-key/src/main/java/xyz/aprildown/timer/component/key/behaviour/BehaviourLayout.kt
@@ -73,7 +73,7 @@ class BehaviourLayout @JvmOverloads constructor(
}
val action = imageBehaviour.toImageAction()
imageView.setOnClickListener { onImageCheck?.invoke(action) }
- imageView.load(action.path) {
+ imageView.load(action.data) {
crossfade(true)
}
} else {
diff --git a/component-key/src/main/java/xyz/aprildown/timer/component/key/behaviour/EditableBehaviourLayout.kt b/component-key/src/main/java/xyz/aprildown/timer/component/key/behaviour/EditableBehaviourLayout.kt
index 995eb47b..730a5609 100644
--- a/component-key/src/main/java/xyz/aprildown/timer/component/key/behaviour/EditableBehaviourLayout.kt
+++ b/component-key/src/main/java/xyz/aprildown/timer/component/key/behaviour/EditableBehaviourLayout.kt
@@ -136,7 +136,7 @@ class EditableBehaviourLayout(
LayoutEditableBehaviourImageBinding.bind(binding.layoutImage.getChildAt(0)).root
}
imageView.setOnClickListener { listener?.onImageContentClick(imageAction) }
- imageView.load(imageAction.path) {
+ imageView.load(imageAction.data) {
crossfade(true)
}
} else {
diff --git a/data/src/main/java/xyz/aprildown/timer/data/repositories/TimerRepositoryImpl.kt b/data/src/main/java/xyz/aprildown/timer/data/repositories/TimerRepositoryImpl.kt
index 09d07b47..987e3c34 100644
--- a/data/src/main/java/xyz/aprildown/timer/data/repositories/TimerRepositoryImpl.kt
+++ b/data/src/main/java/xyz/aprildown/timer/data/repositories/TimerRepositoryImpl.kt
@@ -45,7 +45,7 @@ internal class TimerRepositoryImpl @Inject constructor(
private fun TimerEntity.withRealImagePath(): TimerEntity {
return transformImageActions {
- it.copy(path = imageSaver.getFullPath(id, it.path))
+ it.copy(data = imageSaver.getFullPath(id, it.data))
}
}
@@ -71,7 +71,7 @@ internal class TimerRepositoryImpl @Inject constructor(
private fun TimerEntity.saveImages(): TimerEntity {
return transformImageActions {
- it.copy(path = imageSaver.save(id, it.path))
+ it.copy(data = imageSaver.save(id, it.data))
}
}
diff --git a/domain/src/main/java/xyz/aprildown/timer/domain/entities/BehaviourEntity.kt b/domain/src/main/java/xyz/aprildown/timer/domain/entities/BehaviourEntity.kt
index 7f41ad47..57708fa5 100644
--- a/domain/src/main/java/xyz/aprildown/timer/domain/entities/BehaviourEntity.kt
+++ b/domain/src/main/java/xyz/aprildown/timer/domain/entities/BehaviourEntity.kt
@@ -377,15 +377,19 @@ fun BehaviourEntity.toFlashlightAction(): FlashlightAction {
// region Image
-data class ImageAction(val path: String) : Action {
+/**
+ * @data It can be a relative path when being stored in the database, a canonical path when showing,
+ * or an Uri string when editing
+ */
+data class ImageAction(val data: String) : Action {
override fun toBehaviourEntity(): BehaviourEntity {
- return BehaviourEntity(type = BehaviourType.IMAGE, str1 = path)
+ return BehaviourEntity(type = BehaviourType.IMAGE, str1 = data)
}
}
fun BehaviourEntity.toImageAction(): ImageAction {
require(type == BehaviourType.IMAGE)
- return ImageAction(path = str1)
+ return ImageAction(data = str1)
}
// endregion Image
From d2de885a934ec8c97dd4e52e2a11922f1f6691f3 Mon Sep 17 00:00:00 2001
From: DeweyReed <32358486+DeweyReed@users.noreply.github.com>
Date: Tue, 23 Apr 2024 13:47:43 +0800
Subject: [PATCH 16/31] Convert the image data to different types
---
.../data/repositories/TimerRepositoryImpl.kt | 199 ++++++++++++------
.../timer/domain/entities/BehaviourEntity.kt | 6 +-
.../timer/domain/entities/TimerEntity.kt | 22 +-
.../domain/repositories/TimerRepository.kt | 5 +
.../domain/utils/Base64BitmapConverter.kt | 36 ++++
.../aprildown/timer/domain/utils/FileUtils.kt | 15 ++
6 files changed, 211 insertions(+), 72 deletions(-)
create mode 100644 domain/src/main/java/xyz/aprildown/timer/domain/utils/Base64BitmapConverter.kt
create mode 100644 domain/src/main/java/xyz/aprildown/timer/domain/utils/FileUtils.kt
diff --git a/data/src/main/java/xyz/aprildown/timer/data/repositories/TimerRepositoryImpl.kt b/data/src/main/java/xyz/aprildown/timer/data/repositories/TimerRepositoryImpl.kt
index 987e3c34..a797423b 100644
--- a/data/src/main/java/xyz/aprildown/timer/data/repositories/TimerRepositoryImpl.kt
+++ b/data/src/main/java/xyz/aprildown/timer/data/repositories/TimerRepositoryImpl.kt
@@ -1,9 +1,8 @@
package xyz.aprildown.timer.data.repositories
-import android.content.ContentResolver
import android.content.Context
+import android.graphics.BitmapFactory
import android.webkit.MimeTypeMap
-import androidx.core.net.toFile
import androidx.core.net.toUri
import dagger.Reusable
import dagger.hilt.android.qualifiers.ApplicationContext
@@ -15,11 +14,17 @@ import xyz.aprildown.timer.data.mappers.TimerMapper
import xyz.aprildown.timer.data.mappers.fromWithMapper
import xyz.aprildown.timer.domain.entities.BehaviourType
import xyz.aprildown.timer.domain.entities.ImageAction
+import xyz.aprildown.timer.domain.entities.ResourceContentType
import xyz.aprildown.timer.domain.entities.StepEntity
import xyz.aprildown.timer.domain.entities.TimerEntity
import xyz.aprildown.timer.domain.entities.TimerInfo
+import xyz.aprildown.timer.domain.entities.inferResourcesContentType
import xyz.aprildown.timer.domain.entities.toImageAction
import xyz.aprildown.timer.domain.repositories.TimerRepository
+import xyz.aprildown.timer.domain.utils.Base64BitmapConverter.decodeBitmapByteArrayFromBase64
+import xyz.aprildown.timer.domain.utils.Base64BitmapConverter.encodeToBase64
+import xyz.aprildown.timer.domain.utils.ensureDirExistence
+import xyz.aprildown.timer.domain.utils.ensureNewFile
import java.io.File
import java.util.UUID
import javax.inject.Inject
@@ -31,29 +36,38 @@ internal class TimerRepositoryImpl @Inject constructor(
private val timerMapper: TimerMapper,
private val timerInfoMapper: TimerInfoMapper,
) : TimerRepository {
- private val imageSaver: TimerImageSaver by lazy { TimerImageSaver(context) }
+ private val imageManager by lazy { TimerImageManager(context) }
override suspend fun items(): List {
- return timerDao.getTimers().map { timerMapper.mapFrom(it).withRealImagePath() }
+ return timerDao.getTimers().map { timerMapper.mapFrom(it).withImageCanonicalPath() }
}
override suspend fun item(id: Int): TimerEntity? {
return timerDao.getTimer(id).let {
- return@let if (it == null) null else timerMapper.mapFrom(it).withRealImagePath()
+ return@let if (it == null) null else timerMapper.mapFrom(it).withImageCanonicalPath()
}
}
- private fun TimerEntity.withRealImagePath(): TimerEntity {
- return transformImageActions {
- it.copy(data = imageSaver.getFullPath(id, it.data))
+ private fun TimerEntity.withImageCanonicalPath(): TimerEntity {
+ return transformImageActions { data ->
+ ImageAction(data = data.convertImageDataToCanonicalPath(id))
}
}
override suspend fun add(item: TimerEntity): Int {
- val tempTimerFolder = imageSaver.getTimerFolder(item.id)
- val timerId = timerDao.addTimer(timerMapper.mapTo(item.saveImages())).toInt()
+ val tempTimerFolder = imageManager.getTimerFolder(item.id)
+ val timerId = timerDao.addTimer(
+ timerMapper.mapTo(
+ item.transformImageActions {
+ ImageAction(
+ data = imageManager.save(timerId = item.id, data = it)
+ .convertImageCanonicalPathToDataType(ResourceContentType.RelativePath)
+ )
+ }
+ )
+ ).toInt()
if (tempTimerFolder.exists()) {
- val newTimerFolder = imageSaver.getTimerFolder(timerId)
+ val newTimerFolder = imageManager.getTimerFolder(timerId)
tempTimerFolder.copyRecursively(newTimerFolder)
tempTimerFolder.deleteRecursively()
}
@@ -61,22 +75,19 @@ internal class TimerRepositoryImpl @Inject constructor(
}
override suspend fun save(item: TimerEntity): Boolean {
- try {
- imageSaver.trackSavedImages()
- return timerDao.updateTimer(timerMapper.mapTo(item.saveImages())) == 1
- } finally {
- imageSaver.deleteUnusedImages(item.id)
- }
- }
-
- private fun TimerEntity.saveImages(): TimerEntity {
- return transformImageActions {
- it.copy(data = imageSaver.save(id, it.data))
+ val savedItem = imageManager.trackSaving(timerId = item.id) {
+ item.transformImageActions {
+ ImageAction(
+ data = save(data = it)
+ .convertImageCanonicalPathToDataType(ResourceContentType.RelativePath)
+ )
+ }
}
+ return timerDao.updateTimer(timerMapper.mapTo(savedItem)) == 1
}
private fun TimerEntity.transformImageActions(
- transform: (ImageAction) -> ImageAction
+ transform: (data: String) -> ImageAction
): TimerEntity {
fun StepEntity.transformStep(): StepEntity {
return when (this) {
@@ -85,7 +96,7 @@ internal class TimerRepositoryImpl @Inject constructor(
copy(
behaviour = behaviour.map {
if (it.type == BehaviourType.IMAGE) {
- transform(it.toImageAction()).toBehaviourEntity()
+ transform(it.toImageAction().data).toBehaviourEntity()
} else {
it
}
@@ -109,7 +120,7 @@ internal class TimerRepositoryImpl @Inject constructor(
override suspend fun delete(id: Int) {
timerDao.deleteTimer(id)
- imageSaver.deleteAll(id)
+ imageManager.delete(id)
}
override suspend fun getTimerInfoByTimerId(timerId: Int): TimerInfo? {
@@ -131,78 +142,132 @@ internal class TimerRepositoryImpl @Inject constructor(
override suspend fun moveFolderTimersToAnother(originalFolderId: Long, targetFolderId: Long) {
timerDao.moveFolderTimersToAnother(originalFolderId, targetFolderId)
}
+
+ override suspend fun changeContentType(
+ timers: List,
+ type: ResourceContentType,
+ ): List {
+ return timers.map { timer ->
+ timer.transformImageActions { data ->
+ ImageAction(
+ data = data.convertImageDataToCanonicalPath(timerId = timer.id)
+ .convertImageCanonicalPathToDataType(type = type),
+ )
+ }
+ }
+ }
+
+ private fun String.convertImageDataToCanonicalPath(timerId: Int): String {
+ return imageManager.save(timerId = timerId, data = this)
+ }
+
+ private fun String.convertImageCanonicalPathToDataType(type: ResourceContentType): String {
+ return when (type) {
+ ResourceContentType.CanonicalPath -> this
+ ResourceContentType.RelativePath -> File(this).name
+ ResourceContentType.Uri -> error("Unsupported")
+ ResourceContentType.Base64 -> {
+ BitmapFactory.decodeFile(this).encodeToBase64(quality = 90)
+ }
+ }
+ }
}
-private class TimerImageSaver(private val context: Context) {
- private val imageFolder = File(context.filesDir, FOLDER)
+private class TimerImageManager(private val context: Context) {
+ interface TrackingScope {
+ /**
+ * [TimerImageManager.save]
+ */
+ fun save(data: String): String
+ }
- private var savedFilenames = mutableListOf()
+ private val imageFolder = File(context.filesDir, FOLDER)
fun getTimerFolder(timerId: Int): File {
return File(imageFolder, timerId.toString())
}
- fun getFullPath(timerId: Int, path: String): String {
- return File(getTimerFolder(timerId), path).toUri().toString()
- }
-
/**
- * @return The saved image filename
+ * @return [ResourceContentType.RelativePath], [File.getCanonicalPath]
*/
- fun save(timerId: Int, path: String): String {
- val uri = path.toUri()
+ fun save(timerId: Int, data: String): String {
+ imageFolder.ensureDirExistence()
+
val timerFolder = getTimerFolder(timerId)
- if (uri.scheme == ContentResolver.SCHEME_FILE) {
- val imageFile = uri.toFile()
- if (imageFile.parentFile?.canonicalPath == timerFolder.canonicalPath) {
- return imageFile.name.also {
- savedFilenames += it
+ val imageFile: File
+ when (data.inferResourcesContentType()) {
+ ResourceContentType.CanonicalPath -> {
+ val existingFile = File(data)
+ if (existingFile.parentFile?.canonicalPath == timerFolder.canonicalPath) {
+ imageFile = existingFile
+ } else {
+ imageFile = File(
+ imageFolder.ensureDirExistence(),
+ "${UUID.randomUUID()}.${existingFile.extension}"
+ ).ensureNewFile()
+ imageFile.inputStream().use { inputStream ->
+ existingFile.outputStream().use { outputStream ->
+ inputStream.copyTo(outputStream)
+ }
+ }
}
}
- }
-
- if (!imageFolder.exists()) imageFolder.mkdirs()
- if (!timerFolder.exists()) timerFolder.mkdirs()
-
- return when (uri.scheme) {
- ContentResolver.SCHEME_CONTENT -> {
+ ResourceContentType.RelativePath -> {
+ imageFile = File(timerFolder.ensureDirExistence(), data)
+ }
+ ResourceContentType.Uri -> {
+ val uri = data.toUri()
val extension = MimeTypeMap.getSingleton()
.getExtensionFromMimeType(context.contentResolver.getType(uri)) ?: "jpg"
- val imageFile = File(timerFolder, "${UUID.randomUUID()}.$extension")
- if (imageFile.exists()) {
- imageFile.delete()
- imageFile.createNewFile()
- }
+ imageFile =
+ File(timerFolder.ensureDirExistence(), "${UUID.randomUUID()}.$extension")
+ .ensureNewFile()
context.contentResolver.openInputStream(uri)?.use { inputStream ->
imageFile.outputStream().use { outputStream ->
inputStream.copyTo(outputStream)
}
}
require(imageFile.exists() && imageFile.length() > 0L)
- imageFile.name.also {
- savedFilenames += it
+ imageFile.canonicalPath
+ }
+ ResourceContentType.Base64 -> {
+ imageFile = File(timerFolder.ensureDirExistence(), "${UUID.randomUUID()}.webp")
+ .ensureNewFile()
+ val byteArray = data.decodeBitmapByteArrayFromBase64()
+ imageFile.outputStream().use {
+ it.write(byteArray)
+ it.flush()
}
}
- else -> error("Unsupported path $path")
}
+ require(imageFile.exists() && imageFile.length() > 0L)
+ return imageFile.canonicalPath
}
- fun trackSavedImages() {
- savedFilenames.clear()
- }
-
- fun deleteUnusedImages(timerId: Int) {
- val timerFolder = getTimerFolder(timerId)
- if (!timerFolder.exists()) return
- timerFolder.listFiles()?.toList()?.forEach { file ->
- if (file.name !in savedFilenames) {
- file.delete()
+ fun trackSaving(timerId: Int, block: TrackingScope.() -> T): T {
+ val saved = mutableListOf()
+ val scope = object : TrackingScope {
+ override fun save(data: String): String {
+ val savedData = save(timerId = timerId, data = data)
+ saved += savedData
+ return savedData
+ }
+ }
+ return try {
+ block(scope)
+ } finally {
+ val timerFolder = getTimerFolder(timerId)
+ if (timerFolder.exists()) {
+ timerFolder.listFiles()?.toList()?.forEach { file ->
+ if (file.canonicalPath !in saved) {
+ file.delete()
+ }
+ }
}
}
- savedFilenames.clear()
}
- fun deleteAll(timerId: Int) {
+ fun delete(timerId: Int) {
val timerFolder = getTimerFolder(timerId)
if (timerFolder.exists()) {
timerFolder.deleteRecursively()
diff --git a/domain/src/main/java/xyz/aprildown/timer/domain/entities/BehaviourEntity.kt b/domain/src/main/java/xyz/aprildown/timer/domain/entities/BehaviourEntity.kt
index 57708fa5..00809d1e 100644
--- a/domain/src/main/java/xyz/aprildown/timer/domain/entities/BehaviourEntity.kt
+++ b/domain/src/main/java/xyz/aprildown/timer/domain/entities/BehaviourEntity.kt
@@ -377,11 +377,9 @@ fun BehaviourEntity.toFlashlightAction(): FlashlightAction {
// region Image
-/**
- * @data It can be a relative path when being stored in the database, a canonical path when showing,
- * or an Uri string when editing
- */
data class ImageAction(val data: String) : Action {
+ val type: ResourceContentType = data.inferResourcesContentType()
+
override fun toBehaviourEntity(): BehaviourEntity {
return BehaviourEntity(type = BehaviourType.IMAGE, str1 = data)
}
diff --git a/domain/src/main/java/xyz/aprildown/timer/domain/entities/TimerEntity.kt b/domain/src/main/java/xyz/aprildown/timer/domain/entities/TimerEntity.kt
index 9a28b430..9769a57a 100644
--- a/domain/src/main/java/xyz/aprildown/timer/domain/entities/TimerEntity.kt
+++ b/domain/src/main/java/xyz/aprildown/timer/domain/entities/TimerEntity.kt
@@ -1,5 +1,9 @@
package xyz.aprildown.timer.domain.entities
+import android.content.ContentResolver
+import okio.Path
+import xyz.aprildown.timer.domain.utils.Base64BitmapConverter
+
/**
* Whenever you add a new more item, you need to check
* TestData, TimerMoreMapper, MappersTest, OneFragment, EditActivity
@@ -20,12 +24,28 @@ data class TimerEntity(
val more: TimerMoreEntity = TimerMoreEntity(),
val folderId: Long = FolderEntity.FOLDER_DEFAULT
) {
+ val isNull get() = id == NULL_ID
+
companion object {
const val NEW_ID = 0
const val NULL_ID = 0
}
+}
- val isNull get() = id == NULL_ID
+enum class ResourceContentType {
+ CanonicalPath,
+ RelativePath,
+ Uri,
+ Base64;
+}
+
+fun String.inferResourcesContentType(): ResourceContentType {
+ return when {
+ startsWith(Path.DIRECTORY_SEPARATOR) -> ResourceContentType.CanonicalPath
+ startsWith(ContentResolver.SCHEME_CONTENT) -> ResourceContentType.Uri
+ startsWith(Base64BitmapConverter.PREFIX) -> ResourceContentType.Base64
+ else -> ResourceContentType.RelativePath
+ }
}
/**
diff --git a/domain/src/main/java/xyz/aprildown/timer/domain/repositories/TimerRepository.kt b/domain/src/main/java/xyz/aprildown/timer/domain/repositories/TimerRepository.kt
index 39fd01c6..0cb3c8e6 100644
--- a/domain/src/main/java/xyz/aprildown/timer/domain/repositories/TimerRepository.kt
+++ b/domain/src/main/java/xyz/aprildown/timer/domain/repositories/TimerRepository.kt
@@ -1,6 +1,7 @@
package xyz.aprildown.timer.domain.repositories
import kotlinx.coroutines.flow.Flow
+import xyz.aprildown.timer.domain.entities.ResourceContentType
import xyz.aprildown.timer.domain.entities.TimerEntity
import xyz.aprildown.timer.domain.entities.TimerInfo
@@ -15,4 +16,8 @@ interface TimerRepository {
suspend fun getTimerInfo(folderId: Long): List
suspend fun changeTimerFolder(timerId: Int, folderId: Long)
suspend fun moveFolderTimersToAnother(originalFolderId: Long, targetFolderId: Long)
+ suspend fun changeContentType(
+ timers: List,
+ type: ResourceContentType,
+ ): List
}
diff --git a/domain/src/main/java/xyz/aprildown/timer/domain/utils/Base64BitmapConverter.kt b/domain/src/main/java/xyz/aprildown/timer/domain/utils/Base64BitmapConverter.kt
new file mode 100644
index 00000000..cd770c68
--- /dev/null
+++ b/domain/src/main/java/xyz/aprildown/timer/domain/utils/Base64BitmapConverter.kt
@@ -0,0 +1,36 @@
+package xyz.aprildown.timer.domain.utils
+
+import android.graphics.Bitmap
+import android.os.Build
+import okio.ByteString.Companion.decodeBase64
+import okio.ByteString.Companion.toByteString
+import java.io.ByteArrayOutputStream
+
+object Base64BitmapConverter {
+ const val PREFIX = "base64,"
+
+ fun Bitmap.encodeToBase64(quality: Int = 100): String {
+ val outputStream = ByteArrayOutputStream()
+ compress(
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
+ Bitmap.CompressFormat.WEBP_LOSSY
+ } else {
+ @Suppress("DEPRECATION")
+ Bitmap.CompressFormat.WEBP
+ },
+ quality,
+ outputStream
+ )
+ val byteArray = outputStream.toByteArray()
+ return "$PREFIX${byteArray.toByteString().base64()}"
+ }
+
+ fun String.decodeBitmapByteArrayFromBase64(): ByteArray {
+ return checkNotNull(substringAfter(PREFIX).decodeBase64()).toByteArray()
+ }
+
+ // fun String.decodeBitmapFromBase64(): Bitmap {
+ // val data = decodeBitmapByteArrayFromBase64()
+ // return BitmapFactory.decodeByteArray(data, 0, data.size)
+ // }
+}
diff --git a/domain/src/main/java/xyz/aprildown/timer/domain/utils/FileUtils.kt b/domain/src/main/java/xyz/aprildown/timer/domain/utils/FileUtils.kt
new file mode 100644
index 00000000..e88d5370
--- /dev/null
+++ b/domain/src/main/java/xyz/aprildown/timer/domain/utils/FileUtils.kt
@@ -0,0 +1,15 @@
+package xyz.aprildown.timer.domain.utils
+
+import java.io.File
+
+fun File.ensureDirExistence(): File {
+ if (!exists()) mkdirs()
+ return this
+}
+
+fun File.ensureNewFile(): File {
+ parentFile?.ensureDirExistence()
+ if (exists()) delete()
+ createNewFile()
+ return this
+}
From 8422eb876cd99bbfafcb74c9aeec72608b8732be Mon Sep 17 00:00:00 2001
From: DeweyReed <32358486+DeweyReed@users.noreply.github.com>
Date: Tue, 23 Apr 2024 14:07:47 +0800
Subject: [PATCH 17/31] Fix the wrong base64 string's prefix
---
.../xyz/aprildown/timer/domain/utils/Base64BitmapConverter.kt | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/domain/src/main/java/xyz/aprildown/timer/domain/utils/Base64BitmapConverter.kt b/domain/src/main/java/xyz/aprildown/timer/domain/utils/Base64BitmapConverter.kt
index cd770c68..1b49952c 100644
--- a/domain/src/main/java/xyz/aprildown/timer/domain/utils/Base64BitmapConverter.kt
+++ b/domain/src/main/java/xyz/aprildown/timer/domain/utils/Base64BitmapConverter.kt
@@ -7,7 +7,7 @@ import okio.ByteString.Companion.toByteString
import java.io.ByteArrayOutputStream
object Base64BitmapConverter {
- const val PREFIX = "base64,"
+ const val PREFIX = "data:image/png;base64,"
fun Bitmap.encodeToBase64(quality: Int = 100): String {
val outputStream = ByteArrayOutputStream()
From 1338c6a9935f0d3fb219815c47da5b3255887cf6 Mon Sep 17 00:00:00 2001
From: DeweyReed <32358486+DeweyReed@users.noreply.github.com>
Date: Sat, 27 Apr 2024 11:59:06 +0800
Subject: [PATCH 18/31] Stabilize ImagePreview
---
.../timer/component/key/ImagePreviewActivity.kt | 17 +++++++----------
1 file changed, 7 insertions(+), 10 deletions(-)
diff --git a/component-key/src/main/java/xyz/aprildown/timer/component/key/ImagePreviewActivity.kt b/component-key/src/main/java/xyz/aprildown/timer/component/key/ImagePreviewActivity.kt
index a31b7084..7f02b95a 100644
--- a/component-key/src/main/java/xyz/aprildown/timer/component/key/ImagePreviewActivity.kt
+++ b/component-key/src/main/java/xyz/aprildown/timer/component/key/ImagePreviewActivity.kt
@@ -7,7 +7,6 @@ import androidx.activity.ComponentActivity
import androidx.activity.SystemBarStyle
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
-import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.layout.fillMaxSize
@@ -22,7 +21,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
-import coil.compose.rememberAsyncImagePainter
+import coil.compose.AsyncImage
import coil.request.ImageRequest
import net.engawapg.lib.zoomable.rememberZoomState
import net.engawapg.lib.zoomable.zoomable
@@ -76,17 +75,15 @@ class ImagePreviewActivity : ComponentActivity() {
@Composable
private fun ImagePreview(
- data: Any?,
+ data: String?,
onDismiss: () -> Unit,
modifier: Modifier = Modifier,
) {
- Image(
- painter = rememberAsyncImagePainter(
- model = ImageRequest.Builder(LocalContext.current)
- .data(data)
- .crossfade(true)
- .build(),
- ),
+ AsyncImage(
+ model = ImageRequest.Builder(LocalContext.current)
+ .data(data)
+ .crossfade(true)
+ .build(),
contentDescription = null,
modifier = modifier
.fillMaxSize()
From 85bbbff18770d5fb38ebcbc25d080ba7db56c8ba Mon Sep 17 00:00:00 2001
From: DeweyReed <32358486+DeweyReed@users.noreply.github.com>
Date: Sat, 27 Apr 2024 12:03:35 +0800
Subject: [PATCH 19/31] Load ImageAction with Coil
---
.../java/io/github/deweyreed/timer/App.kt | 2 ++
.../timer/component/key/ImageActionMapper.kt | 23 +++++++++++++++++++
.../key/behaviour/BehaviourLayout.kt | 2 +-
.../key/behaviour/EditableBehaviourLayout.kt | 2 +-
4 files changed, 27 insertions(+), 2 deletions(-)
create mode 100644 component-key/src/main/java/xyz/aprildown/timer/component/key/ImageActionMapper.kt
diff --git a/app/src/main/java/io/github/deweyreed/timer/App.kt b/app/src/main/java/io/github/deweyreed/timer/App.kt
index cb719d43..31a347da 100644
--- a/app/src/main/java/io/github/deweyreed/timer/App.kt
+++ b/app/src/main/java/io/github/deweyreed/timer/App.kt
@@ -29,6 +29,7 @@ import xyz.aprildown.timer.app.base.data.PreferenceData.shouldPausePhoneCall
import xyz.aprildown.timer.app.base.data.PreferenceData.useVoiceContent2
import xyz.aprildown.timer.app.base.utils.AppThemeUtils
import xyz.aprildown.timer.app.base.utils.LogToFileTree
+import xyz.aprildown.timer.component.key.ImageActionMapper
import xyz.aprildown.timer.data.job.initJob
import xyz.aprildown.timer.domain.repositories.PreferencesRepository
import xyz.aprildown.timer.domain.usecases.home.TipManager
@@ -215,6 +216,7 @@ class App : Application(), WorkManagerConfiguration.Provider, ImageLoaderFactory
} else {
add(GifDecoder.Factory())
}
+ add(ImageActionMapper())
}
.build()
}
diff --git a/component-key/src/main/java/xyz/aprildown/timer/component/key/ImageActionMapper.kt b/component-key/src/main/java/xyz/aprildown/timer/component/key/ImageActionMapper.kt
new file mode 100644
index 00000000..54057918
--- /dev/null
+++ b/component-key/src/main/java/xyz/aprildown/timer/component/key/ImageActionMapper.kt
@@ -0,0 +1,23 @@
+package xyz.aprildown.timer.component.key
+
+import androidx.core.net.toUri
+import coil.map.Mapper
+import coil.request.Options
+import xyz.aprildown.timer.domain.entities.ImageAction
+import xyz.aprildown.timer.domain.entities.ResourceContentType
+import xyz.aprildown.timer.domain.utils.Base64BitmapConverter.decodeBitmapByteArrayFromBase64
+import java.io.File
+import java.nio.ByteBuffer
+
+class ImageActionMapper : Mapper {
+ override fun map(data: ImageAction, options: Options): Any? {
+ return when (data.type) {
+ ResourceContentType.CanonicalPath -> File(data.data)
+ ResourceContentType.RelativePath -> error("Relative path can't be loaded")
+ ResourceContentType.Uri -> data.data.toUri()
+ ResourceContentType.Base64 -> {
+ ByteBuffer.wrap(data.data.decodeBitmapByteArrayFromBase64())
+ }
+ }
+ }
+}
diff --git a/component-key/src/main/java/xyz/aprildown/timer/component/key/behaviour/BehaviourLayout.kt b/component-key/src/main/java/xyz/aprildown/timer/component/key/behaviour/BehaviourLayout.kt
index 687aa7bc..46f1e051 100644
--- a/component-key/src/main/java/xyz/aprildown/timer/component/key/behaviour/BehaviourLayout.kt
+++ b/component-key/src/main/java/xyz/aprildown/timer/component/key/behaviour/BehaviourLayout.kt
@@ -73,7 +73,7 @@ class BehaviourLayout @JvmOverloads constructor(
}
val action = imageBehaviour.toImageAction()
imageView.setOnClickListener { onImageCheck?.invoke(action) }
- imageView.load(action.data) {
+ imageView.load(action) {
crossfade(true)
}
} else {
diff --git a/component-key/src/main/java/xyz/aprildown/timer/component/key/behaviour/EditableBehaviourLayout.kt b/component-key/src/main/java/xyz/aprildown/timer/component/key/behaviour/EditableBehaviourLayout.kt
index 730a5609..629b016e 100644
--- a/component-key/src/main/java/xyz/aprildown/timer/component/key/behaviour/EditableBehaviourLayout.kt
+++ b/component-key/src/main/java/xyz/aprildown/timer/component/key/behaviour/EditableBehaviourLayout.kt
@@ -136,7 +136,7 @@ class EditableBehaviourLayout(
LayoutEditableBehaviourImageBinding.bind(binding.layoutImage.getChildAt(0)).root
}
imageView.setOnClickListener { listener?.onImageContentClick(imageAction) }
- imageView.load(imageAction.data) {
+ imageView.load(imageAction) {
crossfade(true)
}
} else {
From 1cc66a36191d60e54a10b9253a309bf516fc7065 Mon Sep 17 00:00:00 2001
From: DeweyReed <32358486+DeweyReed@users.noreply.github.com>
Date: Sat, 4 May 2024 09:50:30 +0800
Subject: [PATCH 20/31] Remove the support for Base64 because of its excessive
size
---
.../timer/component/key/ImageActionMapper.kt | 7 +-
.../data/repositories/TimerRepositoryImpl.kt | 141 +++++++++++-------
.../timer/domain/entities/TimerEntity.kt | 3 -
.../domain/utils/Base64BitmapConverter.kt | 36 -----
4 files changed, 90 insertions(+), 97 deletions(-)
delete mode 100644 domain/src/main/java/xyz/aprildown/timer/domain/utils/Base64BitmapConverter.kt
diff --git a/component-key/src/main/java/xyz/aprildown/timer/component/key/ImageActionMapper.kt b/component-key/src/main/java/xyz/aprildown/timer/component/key/ImageActionMapper.kt
index 54057918..72657ed6 100644
--- a/component-key/src/main/java/xyz/aprildown/timer/component/key/ImageActionMapper.kt
+++ b/component-key/src/main/java/xyz/aprildown/timer/component/key/ImageActionMapper.kt
@@ -5,19 +5,14 @@ import coil.map.Mapper
import coil.request.Options
import xyz.aprildown.timer.domain.entities.ImageAction
import xyz.aprildown.timer.domain.entities.ResourceContentType
-import xyz.aprildown.timer.domain.utils.Base64BitmapConverter.decodeBitmapByteArrayFromBase64
import java.io.File
-import java.nio.ByteBuffer
class ImageActionMapper : Mapper {
- override fun map(data: ImageAction, options: Options): Any? {
+ override fun map(data: ImageAction, options: Options): Any {
return when (data.type) {
ResourceContentType.CanonicalPath -> File(data.data)
ResourceContentType.RelativePath -> error("Relative path can't be loaded")
ResourceContentType.Uri -> data.data.toUri()
- ResourceContentType.Base64 -> {
- ByteBuffer.wrap(data.data.decodeBitmapByteArrayFromBase64())
- }
}
}
}
diff --git a/data/src/main/java/xyz/aprildown/timer/data/repositories/TimerRepositoryImpl.kt b/data/src/main/java/xyz/aprildown/timer/data/repositories/TimerRepositoryImpl.kt
index a797423b..b25396e1 100644
--- a/data/src/main/java/xyz/aprildown/timer/data/repositories/TimerRepositoryImpl.kt
+++ b/data/src/main/java/xyz/aprildown/timer/data/repositories/TimerRepositoryImpl.kt
@@ -1,7 +1,7 @@
package xyz.aprildown.timer.data.repositories
+import android.content.ContentResolver
import android.content.Context
-import android.graphics.BitmapFactory
import android.webkit.MimeTypeMap
import androidx.core.net.toUri
import dagger.Reusable
@@ -21,8 +21,6 @@ import xyz.aprildown.timer.domain.entities.TimerInfo
import xyz.aprildown.timer.domain.entities.inferResourcesContentType
import xyz.aprildown.timer.domain.entities.toImageAction
import xyz.aprildown.timer.domain.repositories.TimerRepository
-import xyz.aprildown.timer.domain.utils.Base64BitmapConverter.decodeBitmapByteArrayFromBase64
-import xyz.aprildown.timer.domain.utils.Base64BitmapConverter.encodeToBase64
import xyz.aprildown.timer.domain.utils.ensureDirExistence
import xyz.aprildown.timer.domain.utils.ensureNewFile
import java.io.File
@@ -39,7 +37,9 @@ internal class TimerRepositoryImpl @Inject constructor(
private val imageManager by lazy { TimerImageManager(context) }
override suspend fun items(): List {
- return timerDao.getTimers().map { timerMapper.mapFrom(it).withImageCanonicalPath() }
+ return timerDao.getTimers().map {
+ timerMapper.mapFrom(it).withImageCanonicalPath()
+ }
}
override suspend fun item(id: Int): TimerEntity? {
@@ -50,7 +50,13 @@ internal class TimerRepositoryImpl @Inject constructor(
private fun TimerEntity.withImageCanonicalPath(): TimerEntity {
return transformImageActions { data ->
- ImageAction(data = data.convertImageDataToCanonicalPath(id))
+ ImageAction(
+ data = data.convertImageData(
+ id = id,
+ imageManager = imageManager,
+ targetType = ResourceContentType.CanonicalPath,
+ ),
+ )
}
}
@@ -60,8 +66,13 @@ internal class TimerRepositoryImpl @Inject constructor(
timerMapper.mapTo(
item.transformImageActions {
ImageAction(
- data = imageManager.save(timerId = item.id, data = it)
- .convertImageCanonicalPathToDataType(ResourceContentType.RelativePath)
+ data = imageManager
+ .save(id = item.id, data = it)
+ .convertImageData(
+ id = item.id,
+ imageManager = imageManager,
+ targetType = ResourceContentType.RelativePath,
+ ),
)
}
)
@@ -75,11 +86,14 @@ internal class TimerRepositoryImpl @Inject constructor(
}
override suspend fun save(item: TimerEntity): Boolean {
- val savedItem = imageManager.trackSaving(timerId = item.id) {
+ val savedItem = imageManager.trackSaving(id = item.id) {
item.transformImageActions {
ImageAction(
- data = save(data = it)
- .convertImageCanonicalPathToDataType(ResourceContentType.RelativePath)
+ data = save(data = it).convertImageData(
+ id = item.id,
+ imageManager = imageManager,
+ targetType = ResourceContentType.RelativePath,
+ ),
)
}
}
@@ -150,30 +164,18 @@ internal class TimerRepositoryImpl @Inject constructor(
return timers.map { timer ->
timer.transformImageActions { data ->
ImageAction(
- data = data.convertImageDataToCanonicalPath(timerId = timer.id)
- .convertImageCanonicalPathToDataType(type = type),
+ data = data.convertImageData(
+ id = timer.id,
+ imageManager = imageManager,
+ targetType = type,
+ )
)
}
}
}
-
- private fun String.convertImageDataToCanonicalPath(timerId: Int): String {
- return imageManager.save(timerId = timerId, data = this)
- }
-
- private fun String.convertImageCanonicalPathToDataType(type: ResourceContentType): String {
- return when (type) {
- ResourceContentType.CanonicalPath -> this
- ResourceContentType.RelativePath -> File(this).name
- ResourceContentType.Uri -> error("Unsupported")
- ResourceContentType.Base64 -> {
- BitmapFactory.decodeFile(this).encodeToBase64(quality = 90)
- }
- }
- }
}
-private class TimerImageManager(private val context: Context) {
+private class TimerImageManager(context: Context) {
interface TrackingScope {
/**
* [TimerImageManager.save]
@@ -181,6 +183,8 @@ private class TimerImageManager(private val context: Context) {
fun save(data: String): String
}
+ val contentResolver: ContentResolver = context.contentResolver
+
private val imageFolder = File(context.filesDir, FOLDER)
fun getTimerFolder(timerId: Int): File {
@@ -190,10 +194,10 @@ private class TimerImageManager(private val context: Context) {
/**
* @return [ResourceContentType.RelativePath], [File.getCanonicalPath]
*/
- fun save(timerId: Int, data: String): String {
+ fun save(id: Int, data: String): String {
imageFolder.ensureDirExistence()
- val timerFolder = getTimerFolder(timerId)
+ val timerFolder = getTimerFolder(id)
val imageFile: File
when (data.inferResourcesContentType()) {
ResourceContentType.CanonicalPath -> {
@@ -202,8 +206,8 @@ private class TimerImageManager(private val context: Context) {
imageFile = existingFile
} else {
imageFile = File(
- imageFolder.ensureDirExistence(),
- "${UUID.randomUUID()}.${existingFile.extension}"
+ timerFolder.ensureDirExistence(),
+ "${generateImageName()}.${existingFile.extension}"
).ensureNewFile()
imageFile.inputStream().use { inputStream ->
existingFile.outputStream().use { outputStream ->
@@ -218,37 +222,32 @@ private class TimerImageManager(private val context: Context) {
ResourceContentType.Uri -> {
val uri = data.toUri()
val extension = MimeTypeMap.getSingleton()
- .getExtensionFromMimeType(context.contentResolver.getType(uri)) ?: "jpg"
- imageFile =
- File(timerFolder.ensureDirExistence(), "${UUID.randomUUID()}.$extension")
- .ensureNewFile()
- context.contentResolver.openInputStream(uri)?.use { inputStream ->
+ .getExtensionFromMimeType(contentResolver.getType(uri)) ?: "jpg"
+ imageFile = File(
+ timerFolder.ensureDirExistence(),
+ "${generateImageName()}.$extension"
+ ).ensureNewFile()
+ contentResolver.openInputStream(uri)?.use { inputStream ->
imageFile.outputStream().use { outputStream ->
inputStream.copyTo(outputStream)
}
}
- require(imageFile.exists() && imageFile.length() > 0L)
imageFile.canonicalPath
}
- ResourceContentType.Base64 -> {
- imageFile = File(timerFolder.ensureDirExistence(), "${UUID.randomUUID()}.webp")
- .ensureNewFile()
- val byteArray = data.decodeBitmapByteArrayFromBase64()
- imageFile.outputStream().use {
- it.write(byteArray)
- it.flush()
- }
- }
}
require(imageFile.exists() && imageFile.length() > 0L)
return imageFile.canonicalPath
}
- fun trackSaving(timerId: Int, block: TrackingScope.() -> T): T {
+ private fun generateImageName(): String {
+ return UUID.randomUUID().toString()
+ }
+
+ fun trackSaving(id: Int, block: TrackingScope.() -> T): T {
val saved = mutableListOf()
val scope = object : TrackingScope {
override fun save(data: String): String {
- val savedData = save(timerId = timerId, data = data)
+ val savedData = save(id = id, data = data)
saved += savedData
return savedData
}
@@ -256,7 +255,7 @@ private class TimerImageManager(private val context: Context) {
return try {
block(scope)
} finally {
- val timerFolder = getTimerFolder(timerId)
+ val timerFolder = getTimerFolder(id)
if (timerFolder.exists()) {
timerFolder.listFiles()?.toList()?.forEach { file ->
if (file.canonicalPath !in saved) {
@@ -267,8 +266,8 @@ private class TimerImageManager(private val context: Context) {
}
}
- fun delete(timerId: Int) {
- val timerFolder = getTimerFolder(timerId)
+ fun delete(id: Int) {
+ val timerFolder = getTimerFolder(id)
if (timerFolder.exists()) {
timerFolder.deleteRecursively()
}
@@ -278,3 +277,41 @@ private class TimerImageManager(private val context: Context) {
private const val FOLDER = "timer_images"
}
}
+
+private fun String.convertImageData(
+ id: Int = TimerEntity.NULL_ID,
+ imageManager: TimerImageManager,
+ targetType: ResourceContentType,
+): String {
+ fun String.save(): String {
+ return imageManager.save(id = id, data = this)
+ }
+
+ fun String.convert(targetType: ResourceContentType): String {
+ return convertImageData(id = id, imageManager = imageManager, targetType)
+ }
+
+ return when (inferResourcesContentType()) {
+ ResourceContentType.CanonicalPath -> {
+ when (targetType) {
+ ResourceContentType.CanonicalPath -> save()
+ ResourceContentType.RelativePath -> File(this).name
+ ResourceContentType.Uri -> error("Unable to convert CanonicalPath to Uri")
+ }
+ }
+ ResourceContentType.RelativePath -> {
+ when (targetType) {
+ ResourceContentType.CanonicalPath -> save()
+ ResourceContentType.RelativePath -> return this
+ ResourceContentType.Uri -> error("Unable to convert RelativePath to Uri")
+ }
+ }
+ ResourceContentType.Uri -> {
+ when (targetType) {
+ ResourceContentType.CanonicalPath -> save()
+ ResourceContentType.RelativePath -> save().convert(ResourceContentType.RelativePath)
+ ResourceContentType.Uri -> return this
+ }
+ }
+ }
+}
diff --git a/domain/src/main/java/xyz/aprildown/timer/domain/entities/TimerEntity.kt b/domain/src/main/java/xyz/aprildown/timer/domain/entities/TimerEntity.kt
index 9769a57a..e576b338 100644
--- a/domain/src/main/java/xyz/aprildown/timer/domain/entities/TimerEntity.kt
+++ b/domain/src/main/java/xyz/aprildown/timer/domain/entities/TimerEntity.kt
@@ -2,7 +2,6 @@ package xyz.aprildown.timer.domain.entities
import android.content.ContentResolver
import okio.Path
-import xyz.aprildown.timer.domain.utils.Base64BitmapConverter
/**
* Whenever you add a new more item, you need to check
@@ -36,14 +35,12 @@ enum class ResourceContentType {
CanonicalPath,
RelativePath,
Uri,
- Base64;
}
fun String.inferResourcesContentType(): ResourceContentType {
return when {
startsWith(Path.DIRECTORY_SEPARATOR) -> ResourceContentType.CanonicalPath
startsWith(ContentResolver.SCHEME_CONTENT) -> ResourceContentType.Uri
- startsWith(Base64BitmapConverter.PREFIX) -> ResourceContentType.Base64
else -> ResourceContentType.RelativePath
}
}
diff --git a/domain/src/main/java/xyz/aprildown/timer/domain/utils/Base64BitmapConverter.kt b/domain/src/main/java/xyz/aprildown/timer/domain/utils/Base64BitmapConverter.kt
deleted file mode 100644
index 1b49952c..00000000
--- a/domain/src/main/java/xyz/aprildown/timer/domain/utils/Base64BitmapConverter.kt
+++ /dev/null
@@ -1,36 +0,0 @@
-package xyz.aprildown.timer.domain.utils
-
-import android.graphics.Bitmap
-import android.os.Build
-import okio.ByteString.Companion.decodeBase64
-import okio.ByteString.Companion.toByteString
-import java.io.ByteArrayOutputStream
-
-object Base64BitmapConverter {
- const val PREFIX = "data:image/png;base64,"
-
- fun Bitmap.encodeToBase64(quality: Int = 100): String {
- val outputStream = ByteArrayOutputStream()
- compress(
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
- Bitmap.CompressFormat.WEBP_LOSSY
- } else {
- @Suppress("DEPRECATION")
- Bitmap.CompressFormat.WEBP
- },
- quality,
- outputStream
- )
- val byteArray = outputStream.toByteArray()
- return "$PREFIX${byteArray.toByteString().base64()}"
- }
-
- fun String.decodeBitmapByteArrayFromBase64(): ByteArray {
- return checkNotNull(substringAfter(PREFIX).decodeBase64()).toByteArray()
- }
-
- // fun String.decodeBitmapFromBase64(): Bitmap {
- // val data = decodeBitmapByteArrayFromBase64()
- // return BitmapFactory.decodeByteArray(data, 0, data.size)
- // }
-}
From 00ad47e5a674ed3e7d8778906dc3caed4f1b4680 Mon Sep 17 00:00:00 2001
From: DeweyReed <32358486+DeweyReed@users.noreply.github.com>
Date: Mon, 17 Jun 2024 21:27:50 +0800
Subject: [PATCH 21/31] Upgrade dependencies
---
.../dogReleaseRuntimeClasspath.txt | 59 +++++++++++++++++--
.../googleReleaseRuntimeClasspath.txt | 59 +++++++++++++++++--
.../otherReleaseRuntimeClasspath.txt | 59 +++++++++++++++++--
gradle/libs.versions.toml | 4 +-
4 files changed, 164 insertions(+), 17 deletions(-)
diff --git a/app/dependencies/dogReleaseRuntimeClasspath.txt b/app/dependencies/dogReleaseRuntimeClasspath.txt
index f294910e..e12a1530 100644
--- a/app/dependencies/dogReleaseRuntimeClasspath.txt
+++ b/app/dependencies/dogReleaseRuntimeClasspath.txt
@@ -16,8 +16,9 @@
:data
:domain
:presentation
-androidx.activity:activity-ktx:1.8.1
-androidx.activity:activity:1.8.1
+androidx.activity:activity-compose:1.9.0
+androidx.activity:activity-ktx:1.9.0
+androidx.activity:activity:1.9.0
androidx.annotation:annotation-experimental:1.4.0
androidx.annotation:annotation-jvm:1.8.0
androidx.annotation:annotation:1.8.0
@@ -25,12 +26,50 @@ androidx.appcompat:appcompat-resources:1.7.0
androidx.appcompat:appcompat:1.7.0
androidx.arch.core:core-common:2.2.0
androidx.arch.core:core-runtime:2.2.0
+androidx.autofill:autofill:1.0.0
androidx.browser:browser:1.8.0
androidx.cardview:cardview:1.0.0
androidx.collection:collection-jvm:1.4.0
androidx.collection:collection-ktx:1.4.0
androidx.collection:collection:1.4.0
+androidx.compose.animation:animation-android:1.6.8
+androidx.compose.animation:animation-core-android:1.6.8
+androidx.compose.animation:animation-core:1.6.8
+androidx.compose.animation:animation:1.6.8
+androidx.compose.foundation:foundation-android:1.6.8
+androidx.compose.foundation:foundation-layout-android:1.6.8
+androidx.compose.foundation:foundation-layout:1.6.8
+androidx.compose.foundation:foundation:1.6.8
+androidx.compose.material3:material3-android:1.2.1
+androidx.compose.material3:material3:1.2.1
+androidx.compose.material:material-android:1.6.8
+androidx.compose.material:material-icons-core-android:1.6.8
+androidx.compose.material:material-icons-core:1.6.8
+androidx.compose.material:material-icons-extended-android:1.6.8
+androidx.compose.material:material-icons-extended:1.6.8
+androidx.compose.material:material-ripple-android:1.6.8
+androidx.compose.material:material-ripple:1.6.8
+androidx.compose.material:material:1.6.8
+androidx.compose.runtime:runtime-android:1.6.8
+androidx.compose.runtime:runtime-livedata:1.6.8
+androidx.compose.runtime:runtime-saveable-android:1.6.8
+androidx.compose.runtime:runtime-saveable:1.6.8
+androidx.compose.runtime:runtime:1.6.8
+androidx.compose.ui:ui-android:1.6.8
+androidx.compose.ui:ui-geometry-android:1.6.8
+androidx.compose.ui:ui-geometry:1.6.8
+androidx.compose.ui:ui-graphics-android:1.6.8
+androidx.compose.ui:ui-graphics:1.6.8
+androidx.compose.ui:ui-text-android:1.6.8
+androidx.compose.ui:ui-text:1.6.8
+androidx.compose.ui:ui-unit-android:1.6.8
+androidx.compose.ui:ui-unit:1.6.8
+androidx.compose.ui:ui-util-android:1.6.8
+androidx.compose.ui:ui-util:1.6.8
+androidx.compose.ui:ui:1.6.8
+androidx.compose:compose-bom:2024.06.00
androidx.concurrent:concurrent-futures:1.1.0
+androidx.constraintlayout:constraintlayout-compose:1.0.1
androidx.constraintlayout:constraintlayout-core:1.0.4
androidx.constraintlayout:constraintlayout:2.1.4
androidx.coordinatorlayout:coordinatorlayout:1.1.0
@@ -45,12 +84,14 @@ androidx.drawerlayout:drawerlayout:1.1.1
androidx.dynamicanimation:dynamicanimation:1.0.0
androidx.emoji2:emoji2-views-helper:1.3.0
androidx.emoji2:emoji2:1.3.0
+androidx.exifinterface:exifinterface:1.3.7
androidx.fragment:fragment-ktx:1.8.0
androidx.fragment:fragment:1.8.0
androidx.hilt:hilt-common:1.2.0
androidx.hilt:hilt-work:1.2.0
androidx.interpolator:interpolator:1.0.0
androidx.legacy:legacy-support-core-utils:1.0.0
+androidx.lifecycle:lifecycle-common-java8:2.8.2
androidx.lifecycle:lifecycle-common-jvm:2.8.2
androidx.lifecycle:lifecycle-common:2.8.2
androidx.lifecycle:lifecycle-livedata-core-ktx:2.8.2
@@ -115,6 +156,7 @@ com.github.PhilJay:MPAndroidChart:v3.1.0
com.github.bumptech.glide:disklrucache:4.16.0
com.github.kizitonwose:CalendarView:1.0.4
com.github.zawadz88.materialpopupmenu:material-popup-menu:4.1.0
+com.google.accompanist:accompanist-drawablepainter:0.32.0
com.google.android.flexbox:flexbox:3.0.0
com.google.android.material:material:1.4.0
com.google.code.findbugs:jsr305:3.0.2
@@ -132,16 +174,23 @@ com.mikepenz:materialdrawer:8.4.5
com.mikepenz:materialize:1.2.1
com.nex3z:toggle-button-group:1.2.3
com.squareup.moshi:moshi:1.15.1
+com.squareup.okhttp3:okhttp:4.12.0
com.squareup.okio:okio-jvm:3.9.0
com.squareup.okio:okio:3.9.0
+io.coil-kt:coil-base:2.6.0
+io.coil-kt:coil-compose-base:2.6.0
+io.coil-kt:coil-compose:2.6.0
+io.coil-kt:coil-gif:2.6.0
+io.coil-kt:coil:2.6.0
javax.inject:javax.inject:1
me.zhanghai.android.materialprogressbar:library:1.4.2
-net.engawapg.lib:zoomable:1.6.0
+net.engawapg.lib:zoomable:1.6.1
nl.dionsegijn:konfetti:1.3.2
org.jetbrains.kotlin:kotlin-android-extensions-runtime:1.9.24
org.jetbrains.kotlin:kotlin-parcelize-runtime:1.9.24
-org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.8.22
-org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.8.22
+org.jetbrains.kotlin:kotlin-stdlib-common:1.9.24
+org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.9.0
+org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.9.0
org.jetbrains.kotlin:kotlin-stdlib:1.9.24
org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.1
org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.8.1
diff --git a/app/dependencies/googleReleaseRuntimeClasspath.txt b/app/dependencies/googleReleaseRuntimeClasspath.txt
index 189eec96..1fc58c2d 100644
--- a/app/dependencies/googleReleaseRuntimeClasspath.txt
+++ b/app/dependencies/googleReleaseRuntimeClasspath.txt
@@ -17,8 +17,9 @@
:domain
:flavor-google
:presentation
-androidx.activity:activity-ktx:1.8.1
-androidx.activity:activity:1.8.1
+androidx.activity:activity-compose:1.9.0
+androidx.activity:activity-ktx:1.9.0
+androidx.activity:activity:1.9.0
androidx.annotation:annotation-experimental:1.4.0
androidx.annotation:annotation-jvm:1.8.0
androidx.annotation:annotation:1.8.0
@@ -27,12 +28,50 @@ androidx.appcompat:appcompat:1.7.0
androidx.arch.core:core-common:2.2.0
androidx.arch.core:core-runtime:2.2.0
androidx.asynclayoutinflater:asynclayoutinflater:1.0.0
+androidx.autofill:autofill:1.0.0
androidx.browser:browser:1.8.0
androidx.cardview:cardview:1.0.0
androidx.collection:collection-jvm:1.4.0
androidx.collection:collection-ktx:1.4.0
androidx.collection:collection:1.4.0
+androidx.compose.animation:animation-android:1.6.8
+androidx.compose.animation:animation-core-android:1.6.8
+androidx.compose.animation:animation-core:1.6.8
+androidx.compose.animation:animation:1.6.8
+androidx.compose.foundation:foundation-android:1.6.8
+androidx.compose.foundation:foundation-layout-android:1.6.8
+androidx.compose.foundation:foundation-layout:1.6.8
+androidx.compose.foundation:foundation:1.6.8
+androidx.compose.material3:material3-android:1.2.1
+androidx.compose.material3:material3:1.2.1
+androidx.compose.material:material-android:1.6.8
+androidx.compose.material:material-icons-core-android:1.6.8
+androidx.compose.material:material-icons-core:1.6.8
+androidx.compose.material:material-icons-extended-android:1.6.8
+androidx.compose.material:material-icons-extended:1.6.8
+androidx.compose.material:material-ripple-android:1.6.8
+androidx.compose.material:material-ripple:1.6.8
+androidx.compose.material:material:1.6.8
+androidx.compose.runtime:runtime-android:1.6.8
+androidx.compose.runtime:runtime-livedata:1.6.8
+androidx.compose.runtime:runtime-saveable-android:1.6.8
+androidx.compose.runtime:runtime-saveable:1.6.8
+androidx.compose.runtime:runtime:1.6.8
+androidx.compose.ui:ui-android:1.6.8
+androidx.compose.ui:ui-geometry-android:1.6.8
+androidx.compose.ui:ui-geometry:1.6.8
+androidx.compose.ui:ui-graphics-android:1.6.8
+androidx.compose.ui:ui-graphics:1.6.8
+androidx.compose.ui:ui-text-android:1.6.8
+androidx.compose.ui:ui-text:1.6.8
+androidx.compose.ui:ui-unit-android:1.6.8
+androidx.compose.ui:ui-unit:1.6.8
+androidx.compose.ui:ui-util-android:1.6.8
+androidx.compose.ui:ui-util:1.6.8
+androidx.compose.ui:ui:1.6.8
+androidx.compose:compose-bom:2024.06.00
androidx.concurrent:concurrent-futures:1.1.0
+androidx.constraintlayout:constraintlayout-compose:1.0.1
androidx.constraintlayout:constraintlayout-core:1.0.4
androidx.constraintlayout:constraintlayout:2.1.4
androidx.coordinatorlayout:coordinatorlayout:1.1.0
@@ -49,6 +88,7 @@ androidx.drawerlayout:drawerlayout:1.1.1
androidx.dynamicanimation:dynamicanimation:1.0.0
androidx.emoji2:emoji2-views-helper:1.3.0
androidx.emoji2:emoji2:1.3.0
+androidx.exifinterface:exifinterface:1.3.7
androidx.fragment:fragment-ktx:1.8.0
androidx.fragment:fragment:1.8.0
androidx.hilt:hilt-common:1.2.0
@@ -59,6 +99,7 @@ androidx.interpolator:interpolator:1.0.0
androidx.legacy:legacy-support-core-ui:1.0.0
androidx.legacy:legacy-support-core-utils:1.0.0
androidx.legacy:legacy-support-v4:1.0.0
+androidx.lifecycle:lifecycle-common-java8:2.8.2
androidx.lifecycle:lifecycle-common-jvm:2.8.2
androidx.lifecycle:lifecycle-common:2.8.2
androidx.lifecycle:lifecycle-extensions:2.2.0
@@ -127,6 +168,7 @@ com.github.PhilJay:MPAndroidChart:v3.1.0
com.github.bumptech.glide:disklrucache:4.16.0
com.github.kizitonwose:CalendarView:1.0.4
com.github.zawadz88.materialpopupmenu:material-popup-menu:4.1.0
+com.google.accompanist:accompanist-drawablepainter:0.32.0
com.google.android.datatransport:transport-api:3.0.0
com.google.android.datatransport:transport-backend-cct:3.1.8
com.google.android.datatransport:transport-runtime:3.1.8
@@ -178,17 +220,24 @@ com.mikepenz:materialdrawer:8.4.5
com.mikepenz:materialize:1.2.1
com.nex3z:toggle-button-group:1.2.3
com.squareup.moshi:moshi:1.15.1
+com.squareup.okhttp3:okhttp:4.12.0
com.squareup.okio:okio-jvm:3.9.0
com.squareup.okio:okio:3.9.0
+io.coil-kt:coil-base:2.6.0
+io.coil-kt:coil-compose-base:2.6.0
+io.coil-kt:coil-compose:2.6.0
+io.coil-kt:coil-gif:2.6.0
+io.coil-kt:coil:2.6.0
javax.inject:javax.inject:1
me.zhanghai.android.materialprogressbar:library:1.4.2
-net.engawapg.lib:zoomable:1.6.0
+net.engawapg.lib:zoomable:1.6.1
net.lingala.zip4j:zip4j:2.11.5
nl.dionsegijn:konfetti:1.3.2
org.jetbrains.kotlin:kotlin-android-extensions-runtime:1.9.24
org.jetbrains.kotlin:kotlin-parcelize-runtime:1.9.24
-org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.8.22
-org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.8.22
+org.jetbrains.kotlin:kotlin-stdlib-common:1.9.24
+org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.9.0
+org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.9.0
org.jetbrains.kotlin:kotlin-stdlib:1.9.24
org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.1
org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.8.1
diff --git a/app/dependencies/otherReleaseRuntimeClasspath.txt b/app/dependencies/otherReleaseRuntimeClasspath.txt
index f294910e..e12a1530 100644
--- a/app/dependencies/otherReleaseRuntimeClasspath.txt
+++ b/app/dependencies/otherReleaseRuntimeClasspath.txt
@@ -16,8 +16,9 @@
:data
:domain
:presentation
-androidx.activity:activity-ktx:1.8.1
-androidx.activity:activity:1.8.1
+androidx.activity:activity-compose:1.9.0
+androidx.activity:activity-ktx:1.9.0
+androidx.activity:activity:1.9.0
androidx.annotation:annotation-experimental:1.4.0
androidx.annotation:annotation-jvm:1.8.0
androidx.annotation:annotation:1.8.0
@@ -25,12 +26,50 @@ androidx.appcompat:appcompat-resources:1.7.0
androidx.appcompat:appcompat:1.7.0
androidx.arch.core:core-common:2.2.0
androidx.arch.core:core-runtime:2.2.0
+androidx.autofill:autofill:1.0.0
androidx.browser:browser:1.8.0
androidx.cardview:cardview:1.0.0
androidx.collection:collection-jvm:1.4.0
androidx.collection:collection-ktx:1.4.0
androidx.collection:collection:1.4.0
+androidx.compose.animation:animation-android:1.6.8
+androidx.compose.animation:animation-core-android:1.6.8
+androidx.compose.animation:animation-core:1.6.8
+androidx.compose.animation:animation:1.6.8
+androidx.compose.foundation:foundation-android:1.6.8
+androidx.compose.foundation:foundation-layout-android:1.6.8
+androidx.compose.foundation:foundation-layout:1.6.8
+androidx.compose.foundation:foundation:1.6.8
+androidx.compose.material3:material3-android:1.2.1
+androidx.compose.material3:material3:1.2.1
+androidx.compose.material:material-android:1.6.8
+androidx.compose.material:material-icons-core-android:1.6.8
+androidx.compose.material:material-icons-core:1.6.8
+androidx.compose.material:material-icons-extended-android:1.6.8
+androidx.compose.material:material-icons-extended:1.6.8
+androidx.compose.material:material-ripple-android:1.6.8
+androidx.compose.material:material-ripple:1.6.8
+androidx.compose.material:material:1.6.8
+androidx.compose.runtime:runtime-android:1.6.8
+androidx.compose.runtime:runtime-livedata:1.6.8
+androidx.compose.runtime:runtime-saveable-android:1.6.8
+androidx.compose.runtime:runtime-saveable:1.6.8
+androidx.compose.runtime:runtime:1.6.8
+androidx.compose.ui:ui-android:1.6.8
+androidx.compose.ui:ui-geometry-android:1.6.8
+androidx.compose.ui:ui-geometry:1.6.8
+androidx.compose.ui:ui-graphics-android:1.6.8
+androidx.compose.ui:ui-graphics:1.6.8
+androidx.compose.ui:ui-text-android:1.6.8
+androidx.compose.ui:ui-text:1.6.8
+androidx.compose.ui:ui-unit-android:1.6.8
+androidx.compose.ui:ui-unit:1.6.8
+androidx.compose.ui:ui-util-android:1.6.8
+androidx.compose.ui:ui-util:1.6.8
+androidx.compose.ui:ui:1.6.8
+androidx.compose:compose-bom:2024.06.00
androidx.concurrent:concurrent-futures:1.1.0
+androidx.constraintlayout:constraintlayout-compose:1.0.1
androidx.constraintlayout:constraintlayout-core:1.0.4
androidx.constraintlayout:constraintlayout:2.1.4
androidx.coordinatorlayout:coordinatorlayout:1.1.0
@@ -45,12 +84,14 @@ androidx.drawerlayout:drawerlayout:1.1.1
androidx.dynamicanimation:dynamicanimation:1.0.0
androidx.emoji2:emoji2-views-helper:1.3.0
androidx.emoji2:emoji2:1.3.0
+androidx.exifinterface:exifinterface:1.3.7
androidx.fragment:fragment-ktx:1.8.0
androidx.fragment:fragment:1.8.0
androidx.hilt:hilt-common:1.2.0
androidx.hilt:hilt-work:1.2.0
androidx.interpolator:interpolator:1.0.0
androidx.legacy:legacy-support-core-utils:1.0.0
+androidx.lifecycle:lifecycle-common-java8:2.8.2
androidx.lifecycle:lifecycle-common-jvm:2.8.2
androidx.lifecycle:lifecycle-common:2.8.2
androidx.lifecycle:lifecycle-livedata-core-ktx:2.8.2
@@ -115,6 +156,7 @@ com.github.PhilJay:MPAndroidChart:v3.1.0
com.github.bumptech.glide:disklrucache:4.16.0
com.github.kizitonwose:CalendarView:1.0.4
com.github.zawadz88.materialpopupmenu:material-popup-menu:4.1.0
+com.google.accompanist:accompanist-drawablepainter:0.32.0
com.google.android.flexbox:flexbox:3.0.0
com.google.android.material:material:1.4.0
com.google.code.findbugs:jsr305:3.0.2
@@ -132,16 +174,23 @@ com.mikepenz:materialdrawer:8.4.5
com.mikepenz:materialize:1.2.1
com.nex3z:toggle-button-group:1.2.3
com.squareup.moshi:moshi:1.15.1
+com.squareup.okhttp3:okhttp:4.12.0
com.squareup.okio:okio-jvm:3.9.0
com.squareup.okio:okio:3.9.0
+io.coil-kt:coil-base:2.6.0
+io.coil-kt:coil-compose-base:2.6.0
+io.coil-kt:coil-compose:2.6.0
+io.coil-kt:coil-gif:2.6.0
+io.coil-kt:coil:2.6.0
javax.inject:javax.inject:1
me.zhanghai.android.materialprogressbar:library:1.4.2
-net.engawapg.lib:zoomable:1.6.0
+net.engawapg.lib:zoomable:1.6.1
nl.dionsegijn:konfetti:1.3.2
org.jetbrains.kotlin:kotlin-android-extensions-runtime:1.9.24
org.jetbrains.kotlin:kotlin-parcelize-runtime:1.9.24
-org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.8.22
-org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.8.22
+org.jetbrains.kotlin:kotlin-stdlib-common:1.9.24
+org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.9.0
+org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.9.0
org.jetbrains.kotlin:kotlin-stdlib:1.9.24
org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.1
org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.8.1
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index f1e107b8..4be5ebc4 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -80,10 +80,10 @@ androidx-navigation-ui = "androidx.navigation:navigation-ui-ktx:2.7.7"
androidx-work = "androidx.work:work-runtime-ktx:2.9.0"
-compose-bom = "androidx.compose:compose-bom:2023.06.01"
+compose-bom = "androidx.compose:compose-bom:2024.06.00"
compose-material = { module = "androidx.compose.material3:material3" }
compose-icons = { module = "androidx.compose.material:material-icons-extended" }
-compose-activity = "androidx.activity:activity-compose:1.8.2"
+compose-activity = "androidx.activity:activity-compose:1.9.0"
compose-liveData = { module = "androidx.compose.runtime:runtime-livedata" }
compose-constraintLayout = "androidx.constraintlayout:constraintlayout-compose:1.0.1"
From 9bf6d20a2bd7ed9f35420c8cfc05b79fcf7d82f8 Mon Sep 17 00:00:00 2001
From: DeweyReed <32358486+DeweyReed@users.noreply.github.com>
Date: Mon, 17 Jun 2024 21:45:59 +0800
Subject: [PATCH 22/31] Disable the timer image manager to simplify the feature
---
.../data/repositories/TimerRepositoryImpl.kt | 77 +++++++++----------
1 file changed, 38 insertions(+), 39 deletions(-)
diff --git a/data/src/main/java/xyz/aprildown/timer/data/repositories/TimerRepositoryImpl.kt b/data/src/main/java/xyz/aprildown/timer/data/repositories/TimerRepositoryImpl.kt
index b25396e1..89a2d54c 100644
--- a/data/src/main/java/xyz/aprildown/timer/data/repositories/TimerRepositoryImpl.kt
+++ b/data/src/main/java/xyz/aprildown/timer/data/repositories/TimerRepositoryImpl.kt
@@ -12,14 +12,11 @@ import xyz.aprildown.timer.data.db.TimerDao
import xyz.aprildown.timer.data.mappers.TimerInfoMapper
import xyz.aprildown.timer.data.mappers.TimerMapper
import xyz.aprildown.timer.data.mappers.fromWithMapper
-import xyz.aprildown.timer.domain.entities.BehaviourType
import xyz.aprildown.timer.domain.entities.ImageAction
import xyz.aprildown.timer.domain.entities.ResourceContentType
-import xyz.aprildown.timer.domain.entities.StepEntity
import xyz.aprildown.timer.domain.entities.TimerEntity
import xyz.aprildown.timer.domain.entities.TimerInfo
import xyz.aprildown.timer.domain.entities.inferResourcesContentType
-import xyz.aprildown.timer.domain.entities.toImageAction
import xyz.aprildown.timer.domain.repositories.TimerRepository
import xyz.aprildown.timer.domain.utils.ensureDirExistence
import xyz.aprildown.timer.domain.utils.ensureNewFile
@@ -49,15 +46,16 @@ internal class TimerRepositoryImpl @Inject constructor(
}
private fun TimerEntity.withImageCanonicalPath(): TimerEntity {
- return transformImageActions { data ->
- ImageAction(
- data = data.convertImageData(
- id = id,
- imageManager = imageManager,
- targetType = ResourceContentType.CanonicalPath,
- ),
- )
- }
+ return this
+ // return transformImageActions { data ->
+ // ImageAction(
+ // data = data.convertImageData(
+ // id = id,
+ // imageManager = imageManager,
+ // targetType = ResourceContentType.CanonicalPath,
+ // ),
+ // )
+ // }
}
override suspend fun add(item: TimerEntity): Int {
@@ -103,33 +101,34 @@ internal class TimerRepositoryImpl @Inject constructor(
private fun TimerEntity.transformImageActions(
transform: (data: String) -> ImageAction
): TimerEntity {
- fun StepEntity.transformStep(): StepEntity {
- return when (this) {
- is StepEntity.Step -> {
- if (behaviour.any { it.type == BehaviourType.IMAGE }) {
- copy(
- behaviour = behaviour.map {
- if (it.type == BehaviourType.IMAGE) {
- transform(it.toImageAction().data).toBehaviourEntity()
- } else {
- it
- }
- }
- )
- } else {
- this
- }
- }
- is StepEntity.Group -> {
- copy(steps = steps.map { it.transformStep() })
- }
- }
- }
- return copy(
- steps = steps.map { it.transformStep() },
- startStep = startStep?.transformStep() as? StepEntity.Step,
- endStep = endStep?.transformStep() as? StepEntity.Step,
- )
+ return this
+ // fun StepEntity.transformStep(): StepEntity {
+ // return when (this) {
+ // is StepEntity.Step -> {
+ // if (behaviour.any { it.type == BehaviourType.IMAGE }) {
+ // copy(
+ // behaviour = behaviour.map {
+ // if (it.type == BehaviourType.IMAGE) {
+ // transform(it.toImageAction().data).toBehaviourEntity()
+ // } else {
+ // it
+ // }
+ // }
+ // )
+ // } else {
+ // this
+ // }
+ // }
+ // is StepEntity.Group -> {
+ // copy(steps = steps.map { it.transformStep() })
+ // }
+ // }
+ // }
+ // return copy(
+ // steps = steps.map { it.transformStep() },
+ // startStep = startStep?.transformStep() as? StepEntity.Step,
+ // endStep = endStep?.transformStep() as? StepEntity.Step,
+ // )
}
override suspend fun delete(id: Int) {
From 1bd8ccfd7d1e0b2874a4db0a400e5fdd644d7b83 Mon Sep 17 00:00:00 2001
From: DeweyReed <32358486+DeweyReed@users.noreply.github.com>
Date: Tue, 18 Jun 2024 20:28:02 +0800
Subject: [PATCH 23/31] Create the theme for Jetpack Compose
---
.../timer/app/backup/BackupComposables.kt | 123 +++++++++---------
.../timer/app/backup/Export2Fragment.kt | 21 +--
.../timer/app/backup/Import2Fragment.kt | 35 ++---
app-base/build.gradle | 10 ++
.../aprildown/timer/app/base/ui/AppTheme.kt | 108 +++++++++++++++
.../dogReleaseRuntimeClasspath.txt | 4 +
.../googleReleaseRuntimeClasspath.txt | 4 +
.../otherReleaseRuntimeClasspath.txt | 4 +
gradle/libs.versions.toml | 1 +
9 files changed, 223 insertions(+), 87 deletions(-)
create mode 100644 app-base/src/main/java/xyz/aprildown/timer/app/base/ui/AppTheme.kt
diff --git a/app-backup/src/main/java/xyz/aprildown/timer/app/backup/BackupComposables.kt b/app-backup/src/main/java/xyz/aprildown/timer/app/backup/BackupComposables.kt
index 6eaa287c..9a6f7649 100644
--- a/app-backup/src/main/java/xyz/aprildown/timer/app/backup/BackupComposables.kt
+++ b/app-backup/src/main/java/xyz/aprildown/timer/app/backup/BackupComposables.kt
@@ -44,72 +44,70 @@ internal fun Backup(
modifier: Modifier = Modifier,
extraOptions: @Composable () -> Unit = {},
) {
- MaterialTheme {
- Column(
- modifier = modifier
- .verticalScroll(rememberScrollState())
- .padding(16.dp),
- verticalArrangement = Arrangement.spacedBy(16.dp),
+ Column(
+ modifier = modifier
+ .verticalScroll(rememberScrollState())
+ .padding(16.dp),
+ verticalArrangement = Arrangement.spacedBy(16.dp),
+ ) {
+ Header(text = contentLocationTitle)
+
+ ContentLocation(
+ title = contentLocationButtonText,
+ location = screen.contentName,
+ modifier = Modifier.clickable(onClick = onChangeContentLocation),
+ )
+
+ Header(text = stringResource(id = RBase.string.backup_app_data))
+
+ extraOptions()
+
+ DataEntry(
+ iconRes = RBase.drawable.ic_timer,
+ nameRes = RBase.string.main_action_timers,
+ include = screen.includeTimers,
+ onIncludedChange = screen.onTimersChange,
+ )
+ DataEntry(
+ iconRes = RBase.drawable.ic_scheduler,
+ nameRes = RBase.string.main_action_schedulers,
+ include = screen.includeSchedulers,
+ onIncludedChange = screen.onSchedulersChange,
+ )
+ DataEntry(
+ iconRes = RBase.drawable.ic_stat,
+ nameRes = RBase.string.main_action_record,
+ include = screen.includeRecords,
+ onIncludedChange = screen.onRecordsChange,
+ )
+ DataEntry(
+ iconRes = RBase.drawable.ic_settings,
+ nameRes = RBase.string.main_action_settings,
+ include = screen.includeSettings,
+ onIncludedChange = screen.onSettingsChange,
+ )
+
+ Button(
+ onClick = screen.onBackup,
+ modifier = Modifier.align(Alignment.CenterHorizontally),
+ enabled = screen.content != null &&
+ (screen.includeTimers || screen.includeSettings) &&
+ !screen.backupOngoing,
) {
- Header(text = contentLocationTitle)
-
- ContentLocation(
- title = contentLocationButtonText,
- location = screen.contentName,
- modifier = Modifier.clickable(onClick = onChangeContentLocation),
- )
-
- Header(text = stringResource(id = RBase.string.backup_app_data))
-
- extraOptions()
-
- DataEntry(
- iconRes = RBase.drawable.ic_timer,
- nameRes = RBase.string.main_action_timers,
- include = screen.includeTimers,
- onIncludedChange = screen.onTimersChange,
- )
- DataEntry(
- iconRes = RBase.drawable.ic_scheduler,
- nameRes = RBase.string.main_action_schedulers,
- include = screen.includeSchedulers,
- onIncludedChange = screen.onSchedulersChange,
- )
- DataEntry(
- iconRes = RBase.drawable.ic_stat,
- nameRes = RBase.string.main_action_record,
- include = screen.includeRecords,
- onIncludedChange = screen.onRecordsChange,
- )
- DataEntry(
- iconRes = RBase.drawable.ic_settings,
- nameRes = RBase.string.main_action_settings,
- include = screen.includeSettings,
- onIncludedChange = screen.onSettingsChange,
- )
-
- Button(
- onClick = screen.onBackup,
- modifier = Modifier.align(Alignment.CenterHorizontally),
- enabled = screen.content != null &&
- (screen.includeTimers || screen.includeSettings) &&
- !screen.backupOngoing,
- ) {
- Text(text = backupButtonText)
- }
+ Text(text = backupButtonText)
}
+ }
- if (screen.backupResult?.fruit is Fruit.Rotten) {
- BackupError(
- hint = backupErrorHint,
- message = (screen.backupResult.fruit as Fruit.Rotten).exception.message.toString(),
- consume = screen.consumeBackupError,
- )
- }
+ if (screen.backupResult?.fruit is Fruit.Rotten) {
+ BackupError(
+ hint = backupErrorHint,
+ message = (screen.backupResult.fruit as Fruit.Rotten).exception.message.toString(),
+ consume = screen.consumeBackupError,
+ )
+ }
- if (screen.backupOngoing) {
- BackupOngoingDialog()
- }
+ if (screen.backupOngoing) {
+ BackupOngoingDialog()
}
}
@@ -121,6 +119,7 @@ private fun Header(
Text(
text = text,
modifier = modifier.padding(horizontal = 12.dp, vertical = 12.dp),
+ color = MaterialTheme.colorScheme.onBackground,
style = MaterialTheme.typography.titleSmall,
)
}
diff --git a/app-backup/src/main/java/xyz/aprildown/timer/app/backup/Export2Fragment.kt b/app-backup/src/main/java/xyz/aprildown/timer/app/backup/Export2Fragment.kt
index 5acd6f7c..5d192e9a 100644
--- a/app-backup/src/main/java/xyz/aprildown/timer/app/backup/Export2Fragment.kt
+++ b/app-backup/src/main/java/xyz/aprildown/timer/app/backup/Export2Fragment.kt
@@ -27,6 +27,7 @@ import com.github.deweyreed.tools.helper.requireCallback
import dagger.hilt.android.AndroidEntryPoint
import okio.sink
import xyz.aprildown.timer.app.base.data.PreferenceData.lastBackupUri
+import xyz.aprildown.timer.app.base.ui.AppTheme
import xyz.aprildown.timer.app.base.ui.MainCallback
import xyz.aprildown.timer.domain.usecases.Fruit
import xyz.aprildown.timer.domain.utils.AppTracker
@@ -135,13 +136,15 @@ private fun Export(
onLocationChange: () -> Unit,
modifier: Modifier = Modifier,
) {
- Backup(
- screen = screen,
- contentLocationTitle = stringResource(id = RBase.string.export_path_title),
- contentLocationButtonText = stringResource(id = RBase.string.export_select_location),
- onChangeContentLocation = onLocationChange,
- backupButtonText = stringResource(id = RBase.string.export_action),
- backupErrorHint = stringResource(id = RBase.string.export_error),
- modifier = modifier,
- )
+ AppTheme {
+ Backup(
+ screen = screen,
+ contentLocationTitle = stringResource(id = RBase.string.export_path_title),
+ contentLocationButtonText = stringResource(id = RBase.string.export_select_location),
+ onChangeContentLocation = onLocationChange,
+ backupButtonText = stringResource(id = RBase.string.export_action),
+ backupErrorHint = stringResource(id = RBase.string.export_error),
+ modifier = modifier,
+ )
+ }
}
diff --git a/app-backup/src/main/java/xyz/aprildown/timer/app/backup/Import2Fragment.kt b/app-backup/src/main/java/xyz/aprildown/timer/app/backup/Import2Fragment.kt
index 59e0f6b3..100c272e 100644
--- a/app-backup/src/main/java/xyz/aprildown/timer/app/backup/Import2Fragment.kt
+++ b/app-backup/src/main/java/xyz/aprildown/timer/app/backup/Import2Fragment.kt
@@ -38,6 +38,7 @@ import dagger.hilt.android.AndroidEntryPoint
import okio.source
import xyz.aprildown.timer.app.base.data.PreferenceData.lastBackupUri
import xyz.aprildown.timer.app.base.ui.AppNavigator
+import xyz.aprildown.timer.app.base.ui.AppTheme
import xyz.aprildown.timer.app.base.ui.MainCallback
import xyz.aprildown.timer.domain.usecases.Fruit
import xyz.aprildown.timer.domain.utils.AppTracker
@@ -137,22 +138,24 @@ private fun Import(
onLocationChange: () -> Unit,
modifier: Modifier = Modifier,
) {
- Backup(
- screen = screen,
- contentLocationTitle = stringResource(id = RBase.string.import_path_title),
- contentLocationButtonText = stringResource(id = RBase.string.import_select_location),
- onChangeContentLocation = onLocationChange,
- backupButtonText = stringResource(id = RBase.string.import_action),
- backupErrorHint = stringResource(id = RBase.string.import_error),
- modifier = modifier,
- extraOptions = {
- WipeContent(
- wipe = importScreen.wipe,
- onWipeChanged = importScreen.onWipeChanged,
- modifier = Modifier.fillMaxSize(),
- )
- },
- )
+ AppTheme {
+ Backup(
+ screen = screen,
+ contentLocationTitle = stringResource(id = RBase.string.import_path_title),
+ contentLocationButtonText = stringResource(id = RBase.string.import_select_location),
+ onChangeContentLocation = onLocationChange,
+ backupButtonText = stringResource(id = RBase.string.import_action),
+ backupErrorHint = stringResource(id = RBase.string.import_error),
+ modifier = modifier,
+ extraOptions = {
+ WipeContent(
+ wipe = importScreen.wipe,
+ onWipeChanged = importScreen.onWipeChanged,
+ modifier = Modifier.fillMaxSize(),
+ )
+ },
+ )
+ }
}
@Composable
diff --git a/app-base/build.gradle b/app-base/build.gradle
index b2b1211e..84694eda 100644
--- a/app-base/build.gradle
+++ b/app-base/build.gradle
@@ -7,6 +7,12 @@ plugins {
android {
namespace 'xyz.aprildown.timer.app.base'
+ buildFeatures {
+ compose true
+ }
+ composeOptions {
+ kotlinCompilerExtensionVersion = libs.versions.composeCompiler.get()
+ }
}
dependencies {
@@ -25,6 +31,10 @@ dependencies {
api libs.androidx.navigation.fragment
api libs.androidx.navigation.ui
+ implementation platform(libs.compose.bom)
+ implementation libs.bundles.compose
+ implementation libs.materialColorUtilities
+
implementation libs.hilt.android
kapt libs.hilt.compiler
diff --git a/app-base/src/main/java/xyz/aprildown/timer/app/base/ui/AppTheme.kt b/app-base/src/main/java/xyz/aprildown/timer/app/base/ui/AppTheme.kt
new file mode 100644
index 00000000..c7a8d48b
--- /dev/null
+++ b/app-base/src/main/java/xyz/aprildown/timer/app/base/ui/AppTheme.kt
@@ -0,0 +1,108 @@
+package xyz.aprildown.timer.app.base.ui
+
+import androidx.compose.foundation.isSystemInDarkTheme
+import androidx.compose.material3.ColorScheme
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.compose.ui.graphics.Color
+import com.materialkolor.palettes.CorePalette
+
+@Composable
+fun AppTheme(
+ isDarkTheme: Boolean = isSystemInDarkTheme(),
+ content: @Composable () -> Unit,
+) {
+ val dynamicTheme = remember { newDynamicTheme }
+ MaterialTheme(
+ colorScheme = remember(
+ isDarkTheme,
+ dynamicTheme.colorPrimary,
+ dynamicTheme.colorSecondary,
+ ) {
+ val core = CorePalette.of(dynamicTheme.colorPrimary)
+ val secondary = CorePalette.of(dynamicTheme.colorSecondary)
+ if (isDarkTheme) {
+ darkFromCorePalette(core = core, secondary = secondary)
+ } else {
+ lightFromCorePalette(core = core, secondary = secondary)
+ }
+ },
+ content = content,
+ )
+}
+
+/** [com.materialkolor.scheme.Scheme]
+ * [androidx.compose.material3.lightColorScheme]
+ * */
+private fun lightFromCorePalette(core: CorePalette, secondary: CorePalette = core): ColorScheme {
+ return ColorScheme(
+ primary = Color(core.a1.tone(40)),
+ onPrimary = Color(core.a1.tone(100)),
+ primaryContainer = Color(core.a1.tone(90)),
+ onPrimaryContainer = Color(core.a1.tone(10)),
+ inversePrimary = Color(core.a1.tone(80)),
+
+ secondary = Color(secondary.a1.tone(40)),
+ onSecondary = Color(secondary.a1.tone(100)),
+ secondaryContainer = Color(secondary.a1.tone(90)),
+ onSecondaryContainer = Color(secondary.a1.tone(10)),
+
+ tertiary = Color(core.a3.tone(40)),
+ onTertiary = Color(core.a3.tone(100)),
+ tertiaryContainer = Color(core.a3.tone(90)),
+ onTertiaryContainer = Color(core.a3.tone(10)),
+ background = Color(core.n1.tone(99)),
+ onBackground = Color(core.n1.tone(10)),
+ surface = Color(core.n1.tone(99)),
+ onSurface = Color(core.n1.tone(10)),
+ surfaceVariant = Color(core.n2.tone(90)),
+ onSurfaceVariant = Color(core.n2.tone(30)),
+ surfaceTint = Color(core.a1.tone(40)),
+ inverseSurface = Color(core.n1.tone(20)),
+ inverseOnSurface = Color(core.n1.tone(95)),
+ error = Color(core.error.tone(40)),
+ onError = Color(core.error.tone(100)),
+ errorContainer = Color(core.error.tone(90)),
+ onErrorContainer = Color(core.error.tone(10)),
+ outline = Color(core.n2.tone(50)),
+ outlineVariant = Color(core.n2.tone(80)),
+ scrim = Color(core.n1.tone(0)),
+ )
+}
+
+private fun darkFromCorePalette(core: CorePalette, secondary: CorePalette = core): ColorScheme {
+ return ColorScheme(
+ primary = Color(core.a1.tone(80)),
+ onPrimary = Color(core.a1.tone(20)),
+ primaryContainer = Color(core.a1.tone(30)),
+ onPrimaryContainer = Color(core.a1.tone(90)),
+ inversePrimary = Color(core.a1.tone(40)),
+
+ secondary = Color(secondary.a1.tone(80)),
+ onSecondary = Color(secondary.a1.tone(20)),
+ secondaryContainer = Color(secondary.a1.tone(30)),
+ onSecondaryContainer = Color(secondary.a1.tone(90)),
+
+ tertiary = Color(core.a3.tone(80)),
+ onTertiary = Color(core.a3.tone(20)),
+ tertiaryContainer = Color(core.a3.tone(30)),
+ onTertiaryContainer = Color(core.a3.tone(90)),
+ background = Color(core.n1.tone(10)),
+ onBackground = Color(core.n1.tone(90)),
+ surface = Color(core.n1.tone(10)),
+ onSurface = Color(core.n1.tone(90)),
+ surfaceVariant = Color(core.n2.tone(30)),
+ onSurfaceVariant = Color(core.n2.tone(80)),
+ surfaceTint = Color(core.a1.tone(80)),
+ inverseSurface = Color(core.n1.tone(90)),
+ inverseOnSurface = Color(core.n1.tone(20)),
+ error = Color(core.error.tone(80)),
+ onError = Color(core.error.tone(20)),
+ errorContainer = Color(core.error.tone(30)),
+ onErrorContainer = Color(core.error.tone(80)),
+ outline = Color(core.n2.tone(60)),
+ outlineVariant = Color(core.n2.tone(30)),
+ scrim = Color(core.n1.tone(0)),
+ )
+}
diff --git a/app/dependencies/dogReleaseRuntimeClasspath.txt b/app/dependencies/dogReleaseRuntimeClasspath.txt
index e12a1530..04a35e8f 100644
--- a/app/dependencies/dogReleaseRuntimeClasspath.txt
+++ b/app/dependencies/dogReleaseRuntimeClasspath.txt
@@ -167,6 +167,8 @@ com.google.dagger:hilt-core:2.51.1
com.google.guava:listenablefuture:1.0
com.jakewharton.timber:timber:5.0.1
com.joaomgcd:taskerpluginlibrary:0.4.10
+com.materialkolor:material-color-utilities-android:1.5.1
+com.materialkolor:material-color-utilities:1.5.1
com.mikepenz:fastadapter-extensions-binding:5.7.0
com.mikepenz:fastadapter-extensions-expandable:5.7.0
com.mikepenz:fastadapter:5.7.0
@@ -177,6 +179,8 @@ com.squareup.moshi:moshi:1.15.1
com.squareup.okhttp3:okhttp:4.12.0
com.squareup.okio:okio-jvm:3.9.0
com.squareup.okio:okio:3.9.0
+dev.drewhamilton.poko:poko-annotations-jvm:0.15.2
+dev.drewhamilton.poko:poko-annotations:0.15.2
io.coil-kt:coil-base:2.6.0
io.coil-kt:coil-compose-base:2.6.0
io.coil-kt:coil-compose:2.6.0
diff --git a/app/dependencies/googleReleaseRuntimeClasspath.txt b/app/dependencies/googleReleaseRuntimeClasspath.txt
index 1fc58c2d..05e113d8 100644
--- a/app/dependencies/googleReleaseRuntimeClasspath.txt
+++ b/app/dependencies/googleReleaseRuntimeClasspath.txt
@@ -211,6 +211,8 @@ com.google.firebase:firebase-storage:21.0.0
com.google.guava:listenablefuture:1.0
com.jakewharton.timber:timber:5.0.1
com.joaomgcd:taskerpluginlibrary:0.4.10
+com.materialkolor:material-color-utilities-android:1.5.1
+com.materialkolor:material-color-utilities:1.5.1
com.microsoft.appcenter:appcenter-crashes:5.0.4
com.microsoft.appcenter:appcenter:5.0.4
com.mikepenz:fastadapter-extensions-binding:5.7.0
@@ -223,6 +225,8 @@ com.squareup.moshi:moshi:1.15.1
com.squareup.okhttp3:okhttp:4.12.0
com.squareup.okio:okio-jvm:3.9.0
com.squareup.okio:okio:3.9.0
+dev.drewhamilton.poko:poko-annotations-jvm:0.15.2
+dev.drewhamilton.poko:poko-annotations:0.15.2
io.coil-kt:coil-base:2.6.0
io.coil-kt:coil-compose-base:2.6.0
io.coil-kt:coil-compose:2.6.0
diff --git a/app/dependencies/otherReleaseRuntimeClasspath.txt b/app/dependencies/otherReleaseRuntimeClasspath.txt
index e12a1530..04a35e8f 100644
--- a/app/dependencies/otherReleaseRuntimeClasspath.txt
+++ b/app/dependencies/otherReleaseRuntimeClasspath.txt
@@ -167,6 +167,8 @@ com.google.dagger:hilt-core:2.51.1
com.google.guava:listenablefuture:1.0
com.jakewharton.timber:timber:5.0.1
com.joaomgcd:taskerpluginlibrary:0.4.10
+com.materialkolor:material-color-utilities-android:1.5.1
+com.materialkolor:material-color-utilities:1.5.1
com.mikepenz:fastadapter-extensions-binding:5.7.0
com.mikepenz:fastadapter-extensions-expandable:5.7.0
com.mikepenz:fastadapter:5.7.0
@@ -177,6 +179,8 @@ com.squareup.moshi:moshi:1.15.1
com.squareup.okhttp3:okhttp:4.12.0
com.squareup.okio:okio-jvm:3.9.0
com.squareup.okio:okio:3.9.0
+dev.drewhamilton.poko:poko-annotations-jvm:0.15.2
+dev.drewhamilton.poko:poko-annotations:0.15.2
io.coil-kt:coil-base:2.6.0
io.coil-kt:coil-compose-base:2.6.0
io.coil-kt:coil-compose:2.6.0
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 4be5ebc4..bd7c87d1 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -86,6 +86,7 @@ compose-icons = { module = "androidx.compose.material:material-icons-extended" }
compose-activity = "androidx.activity:activity-compose:1.9.0"
compose-liveData = { module = "androidx.compose.runtime:runtime-livedata" }
compose-constraintLayout = "androidx.constraintlayout:constraintlayout-compose:1.0.1"
+materialColorUtilities = "com.materialkolor:material-color-utilities:1.5.1"
material = "com.google.android.material:material:1.4.0"
From 29049917235cf49966e96e8faa8691259171d547 Mon Sep 17 00:00:00 2001
From: DeweyReed <32358486+DeweyReed@users.noreply.github.com>
Date: Tue, 18 Jun 2024 20:47:15 +0800
Subject: [PATCH 24/31] Retire the old backup fragments
---
app-backup/build.gradle | 4 +-
.../timer/app/backup/AbstractStep.kt | 21 -
.../timer/app/backup/Export2Fragment.kt | 150 -------
.../timer/app/backup/ExportFragment.kt | 263 ++++---------
...Export2ViewModel.kt => ExportViewModel.kt} | 7 +-
.../timer/app/backup/Import2Fragment.kt | 235 -----------
.../timer/app/backup/ImportFragment.kt | 369 ++++++++----------
...Import2ViewModel.kt => ImportViewModel.kt} | 7 +-
.../app/backup/SelectAppContentHelper.kt | 93 -----
.../main/res/layout/layout_vertical_form.xml | 6 -
.../main/res/layout/step_select_content.xml | 55 ---
.../src/main/res/layout/step_text_button.xml | 23 --
app/build.gradle | 1 -
.../dogReleaseRuntimeClasspath.txt | 1 -
.../googleReleaseRuntimeClasspath.txt | 1 -
.../otherReleaseRuntimeClasspath.txt | 1 -
app/src/main/res/navigation/nav_graph.xml | 4 +-
gradle/libs.versions.toml | 1 -
.../presentation/backup/ExportViewModel.kt | 51 ---
.../presentation/backup/ImportViewModel.kt | 43 --
.../backup/ImportViewModelTest.kt | 45 ---
21 files changed, 247 insertions(+), 1134 deletions(-)
delete mode 100644 app-backup/src/main/java/xyz/aprildown/timer/app/backup/AbstractStep.kt
delete mode 100644 app-backup/src/main/java/xyz/aprildown/timer/app/backup/Export2Fragment.kt
rename app-backup/src/main/java/xyz/aprildown/timer/app/backup/{Export2ViewModel.kt => ExportViewModel.kt} (90%)
delete mode 100644 app-backup/src/main/java/xyz/aprildown/timer/app/backup/Import2Fragment.kt
rename app-backup/src/main/java/xyz/aprildown/timer/app/backup/{Import2ViewModel.kt => ImportViewModel.kt} (91%)
delete mode 100644 app-backup/src/main/java/xyz/aprildown/timer/app/backup/SelectAppContentHelper.kt
delete mode 100644 app-backup/src/main/res/layout/layout_vertical_form.xml
delete mode 100644 app-backup/src/main/res/layout/step_select_content.xml
delete mode 100644 app-backup/src/main/res/layout/step_text_button.xml
delete mode 100644 presentation/src/main/java/xyz/aprildown/timer/presentation/backup/ExportViewModel.kt
delete mode 100644 presentation/src/main/java/xyz/aprildown/timer/presentation/backup/ImportViewModel.kt
delete mode 100644 presentation/src/test/java/xyz/aprildown/timer/presentation/backup/ImportViewModelTest.kt
diff --git a/app-backup/build.gradle b/app-backup/build.gradle
index fa860409..052bb9ea 100644
--- a/app-backup/build.gradle
+++ b/app-backup/build.gradle
@@ -19,6 +19,8 @@ dependencies {
implementation project(':app-base')
implementation project(':component-key')
+ testImplementation libs.junit
+
implementation libs.androidx.preference
implementation platform(libs.compose.bom)
@@ -30,6 +32,4 @@ dependencies {
implementation libs.okio
implementation libs.permission
- implementation libs.verticalStepperForm
-
}
diff --git a/app-backup/src/main/java/xyz/aprildown/timer/app/backup/AbstractStep.kt b/app-backup/src/main/java/xyz/aprildown/timer/app/backup/AbstractStep.kt
deleted file mode 100644
index 7b8ab8bd..00000000
--- a/app-backup/src/main/java/xyz/aprildown/timer/app/backup/AbstractStep.kt
+++ /dev/null
@@ -1,21 +0,0 @@
-package xyz.aprildown.timer.app.backup
-
-import ernestoyaquello.com.verticalstepperform.Step
-
-internal abstract class AbstractStep(
- title: String,
- subTitle: String = "",
- nextButtonText: String = ""
-) : Step(title, subTitle, nextButtonText) {
- override fun restoreStepData(data: T) = Unit
-
- override fun onStepMarkedAsCompleted(animated: Boolean) = Unit
-
- override fun onStepOpened(animated: Boolean) = Unit
-
- override fun onStepMarkedAsUncompleted(animated: Boolean) = Unit
-
- override fun onStepClosed(animated: Boolean) = Unit
-
- override fun getStepDataAsHumanReadableString(): String = ""
-}
diff --git a/app-backup/src/main/java/xyz/aprildown/timer/app/backup/Export2Fragment.kt b/app-backup/src/main/java/xyz/aprildown/timer/app/backup/Export2Fragment.kt
deleted file mode 100644
index 5d192e9a..00000000
--- a/app-backup/src/main/java/xyz/aprildown/timer/app/backup/Export2Fragment.kt
+++ /dev/null
@@ -1,150 +0,0 @@
-package xyz.aprildown.timer.app.backup
-
-import android.app.Activity
-import android.content.Context
-import android.content.Intent
-import android.net.Uri
-import android.os.Bundle
-import android.view.LayoutInflater
-import android.view.View
-import android.view.ViewGroup
-import androidx.activity.result.contract.ActivityResultContracts
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.LaunchedEffect
-import androidx.compose.runtime.collectAsState
-import androidx.compose.runtime.getValue
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.platform.ComposeView
-import androidx.compose.ui.platform.ViewCompositionStrategy
-import androidx.compose.ui.res.stringResource
-import androidx.documentfile.provider.DocumentFile
-import androidx.fragment.app.Fragment
-import androidx.fragment.app.viewModels
-import androidx.navigation.fragment.findNavController
-import com.github.deweyreed.tools.anko.longSnackbar
-import com.github.deweyreed.tools.helper.requireCallback
-import dagger.hilt.android.AndroidEntryPoint
-import okio.sink
-import xyz.aprildown.timer.app.base.data.PreferenceData.lastBackupUri
-import xyz.aprildown.timer.app.base.ui.AppTheme
-import xyz.aprildown.timer.app.base.ui.MainCallback
-import xyz.aprildown.timer.domain.usecases.Fruit
-import xyz.aprildown.timer.domain.utils.AppTracker
-import java.text.SimpleDateFormat
-import java.util.Date
-import java.util.Locale
-import javax.inject.Inject
-import xyz.aprildown.timer.app.base.R as RBase
-
-@AndroidEntryPoint
-class Export2Fragment : Fragment() {
-
- private val viewModel: Export2ViewModel by viewModels()
-
- private lateinit var mainCallback: MainCallback.ActivityCallback
-
- @Inject
- lateinit var appTracker: AppTracker
-
- private val launcher =
- registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
- handleActivityResult(resultCode = it.resultCode, data = it.data)
- }
-
- override fun onAttach(context: Context) {
- super.onAttach(context)
- mainCallback = requireCallback()
- }
-
- override fun onCreateView(
- inflater: LayoutInflater,
- container: ViewGroup?,
- savedInstanceState: Bundle?
- ): View {
- return ComposeView(requireContext()).apply {
- setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
- }
- }
-
- override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
- super.onViewCreated(view, savedInstanceState)
- val navController = findNavController()
- (view as ComposeView).setContent {
- val screen by viewModel.screen.collectAsState()
- LaunchedEffect(screen.backupResult) {
- val result = screen.backupResult
- if (result?.fruit is Fruit.Ripe) {
- mainCallback.snackbarView.longSnackbar(RBase.string.export_done)
- navController.popBackStack(
- destinationId = RBase.id.dest_backup_restore,
- inclusive = false,
- )
- }
- }
- Export(
- screen = screen,
- onLocationChange = ::onLocationChange,
- modifier = Modifier.fillMaxSize(),
- )
- }
- }
-
- private fun onLocationChange() {
- val date = Date()
- val timeString = SimpleDateFormat("yyyy-MM-dd-kk-mm", Locale.getDefault()).format(date)
- val initialFilename = "timer-machine-$timeString.json"
- SafIntentSafeBelt(
- context = requireContext(),
- appTracker = appTracker,
- viewForSnackbar = mainCallback.snackbarView
- ).drive(
- launcher = launcher,
- intent = Intent(Intent.ACTION_CREATE_DOCUMENT)
- .addCategory(Intent.CATEGORY_OPENABLE)
- .setType("*/*")
- .putExtra(Intent.EXTRA_TITLE, initialFilename)
- )
- }
-
- private fun handleActivityResult(resultCode: Int, data: Intent?) {
- if (resultCode != Activity.RESULT_OK) return
- val uri = data?.data ?: return
- if (uri.toString().isBlank() || uri == Uri.EMPTY) return
- val context = requireContext()
- context.lastBackupUri = uri
- val contentResolver = context.contentResolver
- val documentFile = DocumentFile.fromSingleUri(context, uri)
- viewModel.changeContent(
- content = Export2ViewModel.WritableContent(
- getSink = {
- checkNotNull(contentResolver.openOutputStream(uri)).sink()
- },
- delete = {
- // ContentResolver.delete doesn't work
- documentFile?.delete()
- },
- ),
- name = documentFile?.name?.takeIf { it.isNotBlank() } ?: uri.toString(),
- )
- }
-}
-
-@Composable
-private fun Export(
- screen: BaseBackupViewModel.Screen<*>,
- onLocationChange: () -> Unit,
- modifier: Modifier = Modifier,
-) {
- AppTheme {
- Backup(
- screen = screen,
- contentLocationTitle = stringResource(id = RBase.string.export_path_title),
- contentLocationButtonText = stringResource(id = RBase.string.export_select_location),
- onChangeContentLocation = onLocationChange,
- backupButtonText = stringResource(id = RBase.string.export_action),
- backupErrorHint = stringResource(id = RBase.string.export_error),
- modifier = modifier,
- )
- }
-}
diff --git a/app-backup/src/main/java/xyz/aprildown/timer/app/backup/ExportFragment.kt b/app-backup/src/main/java/xyz/aprildown/timer/app/backup/ExportFragment.kt
index 08d579b6..a7c1c288 100644
--- a/app-backup/src/main/java/xyz/aprildown/timer/app/backup/ExportFragment.kt
+++ b/app-backup/src/main/java/xyz/aprildown/timer/app/backup/ExportFragment.kt
@@ -3,35 +3,34 @@ package xyz.aprildown.timer.app.backup
import android.app.Activity
import android.content.Context
import android.content.Intent
-import android.graphics.Color
import android.net.Uri
import android.os.Bundle
-import android.provider.OpenableColumns
+import android.view.LayoutInflater
import android.view.View
-import android.widget.Button
-import android.widget.TextView
+import android.view.ViewGroup
import androidx.activity.result.contract.ActivityResultContracts
-import androidx.core.database.getStringOrNull
-import androidx.core.text.buildSpannedString
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.ComposeView
+import androidx.compose.ui.platform.ViewCompositionStrategy
+import androidx.compose.ui.res.stringResource
+import androidx.documentfile.provider.DocumentFile
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
-import androidx.navigation.fragment.NavHostFragment
+import androidx.navigation.fragment.findNavController
import com.github.deweyreed.tools.anko.longSnackbar
-import com.github.deweyreed.tools.arch.observeNonNull
-import com.github.deweyreed.tools.helper.gone
import com.github.deweyreed.tools.helper.requireCallback
-import com.github.deweyreed.tools.helper.show
-import com.google.android.material.dialog.MaterialAlertDialogBuilder
import dagger.hilt.android.AndroidEntryPoint
-import ernestoyaquello.com.verticalstepperform.VerticalStepperFormView
-import ernestoyaquello.com.verticalstepperform.listener.StepperFormListener
+import okio.sink
import xyz.aprildown.timer.app.base.data.PreferenceData.lastBackupUri
+import xyz.aprildown.timer.app.base.ui.AppTheme
import xyz.aprildown.timer.app.base.ui.MainCallback
-import xyz.aprildown.timer.app.base.ui.newDynamicTheme
import xyz.aprildown.timer.domain.usecases.Fruit
-import xyz.aprildown.timer.domain.usecases.data.ExportAppData
import xyz.aprildown.timer.domain.utils.AppTracker
-import xyz.aprildown.timer.presentation.backup.ExportViewModel
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
@@ -39,17 +38,14 @@ import javax.inject.Inject
import xyz.aprildown.timer.app.base.R as RBase
@AndroidEntryPoint
-class ExportFragment : Fragment(R.layout.layout_vertical_form), StepperFormListener {
+class ExportFragment : Fragment() {
private val viewModel: ExportViewModel by viewModels()
- @Inject
- lateinit var appTracker: AppTracker
-
private lateinit var mainCallback: MainCallback.ActivityCallback
- private lateinit var locationStep: ExportLocationStep
- private lateinit var contentStep: ExportContentStep
+ @Inject
+ lateinit var appTracker: AppTracker
private val launcher =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
@@ -61,83 +57,40 @@ class ExportFragment : Fragment(R.layout.layout_vertical_form), StepperFormListe
mainCallback = requireCallback()
}
- override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
- val context = view.context
- val form = view as VerticalStepperFormView
- locationStep = ExportLocationStep(resources.getString(RBase.string.export_path_title), this)
- contentStep = ExportContentStep(resources.getString(RBase.string.export_content_title))
- form.setup(this, locationStep, contentStep)
- .displayBottomNavigation(false)
- .apply {
- newDynamicTheme.run {
- basicColorScheme(colorPrimary, colorPrimaryVariant, colorOnPrimary)
- nextButtonColors(colorSecondary, colorSecondary, Color.WHITE, Color.WHITE)
- }
- }
- .stepNextButtonText(getString(RBase.string.backup_next_step))
- .confirmationStepTitle(resources.getString(RBase.string.export_begin))
- .lastStepNextButtonText(getString(RBase.string.export_action))
- .displayCancelButtonInLastStep(true)
- .lastStepCancelButtonText(getString(android.R.string.cancel))
- .init()
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View {
+ return ComposeView(requireContext()).apply {
+ setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
+ }
+ }
- viewModel.result.observeNonNull(viewLifecycleOwner) { fruit ->
- when (fruit) {
- is Fruit.Ripe -> {
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+ val navController = findNavController()
+ (view as ComposeView).setContent {
+ val screen by viewModel.screen.collectAsState()
+ LaunchedEffect(screen.backupResult) {
+ val result = screen.backupResult
+ if (result?.fruit is Fruit.Ripe) {
mainCallback.snackbarView.longSnackbar(RBase.string.export_done)
- popBackToBackup()
- }
- is Fruit.Rotten -> {
- val exception = fruit.exception
- MaterialAlertDialogBuilder(context)
- .setMessage(
- buildSpannedString {
- append(getText(RBase.string.export_error))
-
- (exception.localizedMessage ?: exception.message)
- ?.takeIf { it.isNotBlank() }
- ?.let { message ->
- append("\n\n")
- append(message)
- }
- }
- )
- .setPositiveButton(android.R.string.ok, null)
- .setOnDismissListener {
- popBackToBackup()
- }
- .show()
+ navController.popBackStack(
+ destinationId = RBase.id.dest_backup_restore,
+ inclusive = false,
+ )
}
}
- viewModel.consumeResult()
- }
- }
-
- override fun onCompletedForm() {
- val context = requireContext()
- val settings = contentStep.stepData
- viewModel.export(
- params = ExportAppData.Params(
- exportTimers = settings.isTimersChecked,
- exportTimerStamps = settings.isTimerStampsChecked,
- exportSchedulers = settings.isSchedulersChecked,
- exportPreferences = settings.isSettingsChecked
- ),
- outputStream = checkNotNull(
- context.contentResolver.openOutputStream(locationStep.stepData, "rwt")
+ Export(
+ screen = screen,
+ onLocationChange = ::onLocationChange,
+ modifier = Modifier.fillMaxSize(),
)
- )
- }
-
- override fun onCancelledForm() {
- popBackToBackup()
- }
-
- private fun popBackToBackup() {
- NavHostFragment.findNavController(this).popBackStack(RBase.id.dest_backup_restore, false)
+ }
}
- fun pickExportPath() {
+ private fun onLocationChange() {
val date = Date()
val timeString = SimpleDateFormat("yyyy-MM-dd-kk-mm", Locale.getDefault()).format(date)
val initialFilename = "timer-machine-$timeString.json"
@@ -155,101 +108,43 @@ class ExportFragment : Fragment(R.layout.layout_vertical_form), StepperFormListe
}
private fun handleActivityResult(resultCode: Int, data: Intent?) {
- if (resultCode == Activity.RESULT_OK) {
- val uri = data?.data ?: return
- val context = requireContext()
- var name: String? = null
-
- try {
- context.contentResolver.query(
- uri,
- arrayOf(OpenableColumns.DISPLAY_NAME),
- null,
- null,
- null,
- null
- )?.use { cursor ->
- if (cursor.moveToFirst()) {
- name =
- cursor.getStringOrNull(cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME))
- }
- }
- } catch (e: Exception) {
- appTracker.trackError(e)
- mainCallback.snackbarView.longSnackbar(e.message.toString())
- return
- }
-
- context.lastBackupUri = uri
- locationStep.locationPicked(uri, name)
- }
- }
-}
-
-private class ExportLocationStep(
- title: String,
- private val parentFragment: ExportFragment
-) : AbstractStep(title) {
-
- private var locationUri: Uri = Uri.EMPTY
- private lateinit var locationTextView: TextView
-
- override fun createStepContentLayout(): View {
- val context = context
- val view = View.inflate(context, R.layout.step_text_button, null)
- locationTextView = view.findViewById(R.id.textStepTextButton).apply {
- gone()
- }
- view.findViewById