diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 19dcc50..7339168 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -13,6 +13,7 @@ androidXTestJunit = "1.1.5" androidXTestEspresso = "3.5.1" skydovesBalloon = "1.6.4" flexbox = "3.0.0" +counterSlider = "0.1.4" composeActivity = "1.9.0" composeBom = "2024.06.00" @@ -29,6 +30,7 @@ fastadapter-extensions-binding = { group = "com.mikepenz", name = "fastadapter-e fastadapter-extensions-diff = { group = "com.mikepenz", name = "fastadapter-extensions-diff", version.ref = "fastadapter" } arklib = { group = "dev.arkbuilders", name = "arklib", version.ref = "arkLib" } orbit-mvi-viewmodel = { group = "org.orbit-mvi", name = "orbit-viewmodel", version.ref = "orbitMvi" } +orbit-mvi-compose = { group = "org.orbit-mvi", name = "orbit-compose", version.ref = "orbitMvi" } viewbinding-property-delegate = { group = "com.github.kirich1409", name = "viewbindingpropertydelegate-noreflection", version.ref = "viewbindingPropertyDelegate" } androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "androidXCore" } androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "androidXAppcompat" } @@ -38,6 +40,7 @@ androidx-test-junit = { group = "androidx.test.ext", name = "junit", version.ref androidx-test-espresso = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "androidXTestEspresso" } skydoves-balloon = { group = "com.github.skydoves", name = "balloon", version.ref = "skydovesBalloon" } flexbox = { group = "com.google.android.flexbox", name = "flexbox", version.ref = "flexbox" } +counterslider = { module = "com.github.mdrlzy:ComposeCounterSlider", version.ref = "counterSlider" } #Compose androidx-compose-activity = { group = "androidx.activity", name = "activity-compose", version.ref = "composeActivity" } @@ -47,3 +50,4 @@ androidx-compose-ui-graphics = { group = "androidx.compose.ui", name = "ui-graph androidx-compose-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" } androidx-compose-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" } androidx-compose-material3 = { group = "androidx.compose.material3", name = "material3" } +androidx-compose-icons-extended = { group = "androidx.compose.material", name = "material-icons-extended" } diff --git a/sample/build.gradle.kts b/sample/build.gradle.kts index 5897047..deb3915 100644 --- a/sample/build.gradle.kts +++ b/sample/build.gradle.kts @@ -46,6 +46,10 @@ android { buildFeatures { buildConfig = true viewBinding = true + compose = true + } + composeOptions { + kotlinCompilerExtensionVersion = libs.versions.composeCompiler.get() } splits { @@ -72,11 +76,16 @@ android { dependencies { implementation(project(":filepicker")) implementation(project(":about")) + implementation(project(":scorewidget")) + implementation(platform(libs.androidx.compose.bom)) + implementation(libs.androidx.compose.ui) + implementation(libs.androidx.compose.material3) implementation(libraries.arklib) implementation("androidx.core:core-ktx:1.12.0") implementation(libraries.androidx.appcompat) implementation(libraries.android.material) + implementation(libraries.orbit.mvi.viewmodel) testImplementation(libraries.junit) androidTestImplementation(libraries.androidx.test.junit) androidTestImplementation(libraries.androidx.test.espresso) diff --git a/sample/src/main/AndroidManifest.xml b/sample/src/main/AndroidManifest.xml index 4680920..34778de 100644 --- a/sample/src/main/AndroidManifest.xml +++ b/sample/src/main/AndroidManifest.xml @@ -34,6 +34,7 @@ + diff --git a/sample/src/main/java/dev/arkbuilders/sample/MainActivity.kt b/sample/src/main/java/dev/arkbuilders/sample/MainActivity.kt index db201bd..7922f87 100644 --- a/sample/src/main/java/dev/arkbuilders/sample/MainActivity.kt +++ b/sample/src/main/java/dev/arkbuilders/sample/MainActivity.kt @@ -59,6 +59,12 @@ class MainActivity : AppCompatActivity() { val intent = Intent(this, AboutActivity::class.java) startActivity(intent) } + + findViewById(R.id.btn_score).setOnClickListener { + resolvePermissions() + val intent = Intent(this, ScoreActivity::class.java) + startActivity(intent) + } } private fun getFilePickerConfig(mode: ArkFilePickerMode? = null) = ArkFilePickerConfig( diff --git a/sample/src/main/java/dev/arkbuilders/sample/ScoreActivity.kt b/sample/src/main/java/dev/arkbuilders/sample/ScoreActivity.kt new file mode 100644 index 0000000..96c088b --- /dev/null +++ b/sample/src/main/java/dev/arkbuilders/sample/ScoreActivity.kt @@ -0,0 +1,138 @@ +package dev.arkbuilders.sample + +import android.os.Bundle +import android.widget.Toast +import androidx.appcompat.app.AppCompatActivity +import androidx.compose.foundation.layout.padding +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.ComposeView +import androidx.compose.ui.unit.DpSize +import androidx.compose.ui.unit.dp +import androidx.lifecycle.lifecycleScope +import com.google.android.material.button.MaterialButton +import dev.arkbuilders.arklib.data.folders.FoldersRepo +import dev.arkbuilders.arklib.data.index.ResourceIndex +import dev.arkbuilders.arklib.data.index.ResourceIndexRepo +import dev.arkbuilders.arklib.user.score.ScoreStorage +import dev.arkbuilders.arklib.user.score.ScoreStorageRepo +import dev.arkbuilders.components.filepicker.ArkFilePickerConfig +import dev.arkbuilders.components.filepicker.ArkFilePickerFragment +import dev.arkbuilders.components.filepicker.ArkFilePickerMode +import dev.arkbuilders.components.filepicker.onArkPathPicked +import dev.arkbuilders.components.scorewidget.HorizontalScoreWidgetComposable +import dev.arkbuilders.components.scorewidget.ScoreWidgetController +import dev.arkbuilders.components.scorewidget.VerticalScoreWidgetComposable +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import java.nio.file.Path +import kotlin.io.path.name + +class ScoreActivity : AppCompatActivity() { + private var rootFolder: Path? = null + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_score) + + val btnPickRoot = findViewById(R.id.btn_pick_root) + val btnPickResource = findViewById(R.id.btn_pick_resource) + + supportFragmentManager.onArkPathPicked( + this, + customRequestKey = PICK_ROOT_KEY + ) { root -> + rootFolder = root + btnPickRoot.text = root.name + } + + supportFragmentManager.onArkPathPicked( + this, + customRequestKey = PICK_RESOURCE_KEY + ) { resourcePath -> + rootFolder?.let { + onResourcePicked(root = it, resourcePath) + } + } + + btnPickRoot.setOnClickListener { + ArkFilePickerFragment + .newInstance(ArkFilePickerConfig(pathPickedRequestKey = PICK_ROOT_KEY)) + .show(supportFragmentManager, null) + } + + btnPickResource.setOnClickListener { + ArkFilePickerFragment + .newInstance( + ArkFilePickerConfig( + pathPickedRequestKey = PICK_RESOURCE_KEY, + mode = ArkFilePickerMode.FILE + ) + ).show(supportFragmentManager, null) + } + } + + private fun onResourcePicked( + root: Path, + resourcePath: Path + ) = lifecycleScope.launch { + val (index, scoreStorage) = setupIndexAndScoreStorage(root) + val id = index.allPaths().toList() + .find { it.second == resourcePath }?.first + + id ?: let { + Toast.makeText( + this@ScoreActivity, + "File does not belong to root", + Toast.LENGTH_SHORT + ).show() + return@launch + } + + findViewById(R.id.btn_pick_resource).text = resourcePath.name + + val scoreWidgetController = ScoreWidgetController( + lifecycleScope, + getCurrentId = { id }, + onScoreChanged = {} + ) + scoreWidgetController.init(scoreStorage) + + val horizontal = findViewById(R.id.score_widget_horizontal) + val vertical = findViewById(R.id.score_widget_vertical) + + horizontal.disposeComposition() + horizontal.setContent { + HorizontalScoreWidgetComposable( + size = DpSize(200.dp, 80.dp), + controller = scoreWidgetController + ) + } + + vertical.disposeComposition() + vertical.setContent { + VerticalScoreWidgetComposable( + modifier = Modifier.padding(40.dp), + size = DpSize(50.dp, 120.dp), + controller = scoreWidgetController + ) + } + + scoreWidgetController.setVisible(true) + scoreWidgetController.displayScore() + } + + private suspend fun setupIndexAndScoreStorage( + root: Path + ): Pair = withContext(Dispatchers.IO) { + val foldersRepo = FoldersRepo(applicationContext) + val index = ResourceIndexRepo(foldersRepo).provide(root) + val scoreStorage = ScoreStorageRepo(lifecycleScope).provide(index) + return@withContext index to scoreStorage + } + + companion object { + private val PICK_ROOT_KEY = "pickRootKey" + private val PICK_RESOURCE_KEY = "pickResourceKey" + } +} \ No newline at end of file diff --git a/sample/src/main/res/layout/activity_main.xml b/sample/src/main/res/layout/activity_main.xml index 09d74b6..f404d8e 100644 --- a/sample/src/main/res/layout/activity_main.xml +++ b/sample/src/main/res/layout/activity_main.xml @@ -1,8 +1,10 @@ - + android:layout_height="match_parent" + android:gravity="center" + android:orientation="vertical"> - \ No newline at end of file + + + \ No newline at end of file diff --git a/sample/src/main/res/layout/activity_score.xml b/sample/src/main/res/layout/activity_score.xml new file mode 100644 index 0000000..c6cd6e0 --- /dev/null +++ b/sample/src/main/res/layout/activity_score.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/scorewidget/build.gradle.kts b/scorewidget/build.gradle.kts index b05f489..06081fb 100644 --- a/scorewidget/build.gradle.kts +++ b/scorewidget/build.gradle.kts @@ -34,19 +34,30 @@ android { kotlinOptions { jvmTarget = "17" } - + buildFeatures { + compose = true + } + composeOptions { + kotlinCompilerExtensionVersion = libs.versions.composeCompiler.get() + } buildFeatures { viewBinding = true } } dependencies { - - implementation(libraries.androidx.core.ktx) - implementation(libraries.androidx.appcompat) - implementation(libraries.android.material) + implementation(libraries.androidx.compose.activity) + implementation(platform(libs.androidx.compose.bom)) + implementation(libs.androidx.compose.ui) + implementation(libs.androidx.compose.ui.graphics) + implementation(libs.androidx.compose.ui.tooling) + implementation(libs.androidx.compose.ui.tooling.preview) + implementation(libs.androidx.compose.material3) + implementation(libs.androidx.compose.icons.extended) implementation(libraries.arklib) implementation(libraries.orbit.mvi.viewmodel) + implementation(libs.orbit.mvi.compose) + implementation(libs.counterslider) testImplementation(libraries.junit) androidTestImplementation(libraries.androidx.test.junit) androidTestImplementation(libraries.androidx.test.espresso) diff --git a/scorewidget/src/main/java/dev/arkbuilders/components/scorewidget/ScoreWidget.kt b/scorewidget/src/main/java/dev/arkbuilders/components/scorewidget/ScoreWidget.kt deleted file mode 100644 index 9f03495..0000000 --- a/scorewidget/src/main/java/dev/arkbuilders/components/scorewidget/ScoreWidget.kt +++ /dev/null @@ -1,34 +0,0 @@ -package dev.arkbuilders.components.scorewidget - -import androidx.core.view.isVisible -import androidx.lifecycle.LifecycleOwner -import dev.arkbuilders.components.databinding.ScoreWidgetBinding -import org.orbitmvi.orbit.viewmodel.observe - -class ScoreWidget( - val controller: ScoreWidgetController, - val lifecycleOwner: LifecycleOwner, -) { - var binding: ScoreWidgetBinding? = null - - fun init(binding: ScoreWidgetBinding) { - this.binding = binding - binding.increaseScore.setOnClickListener { - controller.onIncrease() - } - binding.decreaseScore.setOnClickListener { - controller.onDecrease() - } - controller.observe(lifecycleOwner, state = ::render) - } - - fun onDestroyView() { - binding = null - } - - private fun render(state: ScoreWidgetState) { - val score = state.score - binding!!.root.isVisible = state.visible - binding!!.scoreValue.text = if (score == 0) null else score.toString() - } -} \ No newline at end of file diff --git a/scorewidget/src/main/java/dev/arkbuilders/components/scorewidget/ScoreWidgetComposable.kt b/scorewidget/src/main/java/dev/arkbuilders/components/scorewidget/ScoreWidgetComposable.kt new file mode 100644 index 0000000..0de9964 --- /dev/null +++ b/scorewidget/src/main/java/dev/arkbuilders/components/scorewidget/ScoreWidgetComposable.kt @@ -0,0 +1,57 @@ +package dev.arkbuilders.components.scorewidget + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.DpSize +import androidx.compose.ui.unit.dp +import com.mdrlzy.counterslider.HorizontalCounterSlider +import com.mdrlzy.counterslider.VerticalCounterSlider +import org.orbitmvi.orbit.compose.collectAsState + + +@Composable +fun HorizontalScoreWidgetComposable( + modifier: Modifier = Modifier, + size: DpSize = DpSize(200.dp, 80.dp), + allowTopToReset: Boolean = true, + allowBottomToReset: Boolean = true, + controller: ScoreWidgetController, +) { + val state by controller.collectAsState() + if (state.visible.not()) + return + HorizontalCounterSlider( + modifier = modifier, + size = size, + value = state.score.toString(), + allowTopToReset = allowTopToReset, + allowBottomToReset = allowBottomToReset, + onValueIncreaseClick = { controller.onIncrease() }, + onValueDecreaseClick = { controller.onDecrease() }, + onValueClearClick = { controller.onReset() } + ) +} + +@Composable +fun VerticalScoreWidgetComposable( + modifier: Modifier = Modifier, + size: DpSize = DpSize(80.dp, 200.dp), + allowLeftToReset: Boolean = true, + allowRightToReset: Boolean = true, + controller: ScoreWidgetController, +) { + val state by controller.collectAsState() + if (state.visible.not()) + return + VerticalCounterSlider( + modifier = modifier, + size = size, + value = state.score.toString(), + allowLeftToReset = allowLeftToReset, + allowRightToReset = allowRightToReset, + onValueIncreaseClick = { controller.onIncrease() }, + onValueDecreaseClick = { controller.onDecrease() }, + onValueClearClick = { controller.onReset() } + ) +} diff --git a/scorewidget/src/main/java/dev/arkbuilders/components/scorewidget/ScoreWidgetController.kt b/scorewidget/src/main/java/dev/arkbuilders/components/scorewidget/ScoreWidgetController.kt index 63f905f..df18ddf 100644 --- a/scorewidget/src/main/java/dev/arkbuilders/components/scorewidget/ScoreWidgetController.kt +++ b/scorewidget/src/main/java/dev/arkbuilders/components/scorewidget/ScoreWidgetController.kt @@ -22,7 +22,7 @@ class ScoreWidgetController( val scope: CoroutineScope, val getCurrentId: () -> ResourceId, val onScoreChanged: (ResourceId) -> Unit -): ContainerHost { +) : ContainerHost { private lateinit var scoreStorage: ScoreStorage @@ -33,25 +33,31 @@ class ScoreWidgetController( this.scoreStorage = scoreStorage } - fun setVisible(visible: Boolean) = intent { - reduce { - state.copy(visible = visible) + fun setVisible(visible: Boolean) { + intent { + reduce { + state.copy(visible = visible) + } } } - fun displayScore() = intent { - reduce { - state.copy(score = scoreStorage.getScore(getCurrentId())) + + fun displayScore() { + intent { + reduce { + state.copy(score = scoreStorage.getScore(getCurrentId())) + } } } - fun onIncrease() = changeScore(1) + fun onIncrease() = changeScore(scoreStorage.getScore(getCurrentId()) + 1) + + fun onDecrease() = changeScore(scoreStorage.getScore(getCurrentId()) - 1) - fun onDecrease() = changeScore(-1) + fun onReset() = changeScore(0) - private fun changeScore(inc: Score) = scope.launch { + private fun changeScore(score: Score) = scope.launch { val id = getCurrentId() - val score = scoreStorage.getScore(id) + inc scoreStorage.setScore(id, score) withContext(Dispatchers.IO) { diff --git a/scorewidget/src/main/res/layout/item_tag.xml b/scorewidget/src/main/res/layout/item_tag.xml deleted file mode 100644 index f4d1745..0000000 --- a/scorewidget/src/main/res/layout/item_tag.xml +++ /dev/null @@ -1,7 +0,0 @@ - - diff --git a/scorewidget/src/main/res/layout/score_widget.xml b/scorewidget/src/main/res/layout/score_widget.xml deleted file mode 100644 index 7e582fc..0000000 --- a/scorewidget/src/main/res/layout/score_widget.xml +++ /dev/null @@ -1,42 +0,0 @@ - - - - - - - - - - - - \ No newline at end of file