├── .editorconfig ├── .gitattributes ├── .github └── workflows │ └── build-apk.yml ├── .gitignore ├── LICENSE ├── README.md ├── app ├── .gitignore ├── build.gradle.kts ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── ic_launcher-playstore.png │ ├── java │ └── com │ │ └── uravgcode │ │ └── chooser │ │ ├── MainActivity.kt │ │ ├── chooser │ │ ├── domain │ │ │ └── Mode.kt │ │ └── presentation │ │ │ ├── Chooser.kt │ │ │ ├── ChooserScreen.kt │ │ │ ├── button │ │ │ ├── AnimatedButton.kt │ │ │ └── BaseButton.kt │ │ │ ├── circle │ │ │ ├── Circle.kt │ │ │ ├── GroupCircle.kt │ │ │ └── OrderCircle.kt │ │ │ ├── component │ │ │ └── Number.kt │ │ │ └── manager │ │ │ ├── CircleManager.kt │ │ │ ├── ColorManager.kt │ │ │ └── SoundManager.kt │ │ ├── navigation │ │ ├── domain │ │ │ └── Screen.kt │ │ └── presentation │ │ │ └── Navigation.kt │ │ ├── settings │ │ ├── data │ │ │ ├── SettingsData.kt │ │ │ ├── SettingsDataStore.kt │ │ │ └── SettingsSerializer.kt │ │ └── presentation │ │ │ ├── SettingsScreen.kt │ │ │ ├── button │ │ │ ├── ExportButton.kt │ │ │ ├── ImportButton.kt │ │ │ ├── ResetButton.kt │ │ │ └── SettingsButton.kt │ │ │ ├── component │ │ │ ├── SettingsSeparator.kt │ │ │ ├── SettingsSwitch.kt │ │ │ └── SettingsTopAppBar.kt │ │ │ └── slider │ │ │ ├── PaddingSlider.kt │ │ │ ├── PercentSlider.kt │ │ │ ├── SettingsSlider.kt │ │ │ └── TimeSlider.kt │ │ ├── tutorial │ │ ├── domain │ │ │ ├── PageData.kt │ │ │ └── PagerContent.kt │ │ └── presentation │ │ │ ├── TutorialScreen.kt │ │ │ └── component │ │ │ ├── PageIndicator.kt │ │ │ └── TutorialPage.kt │ │ └── ui │ │ └── theme │ │ ├── Color.kt │ │ ├── Theme.kt │ │ └── Type.kt │ └── res │ ├── animator │ ├── fade_in_full_1.xml │ ├── fade_in_full_2.xml │ ├── fade_in_full_3.xml │ ├── fade_in_full_4.xml │ ├── fade_in_growing_1.xml │ ├── fade_in_growing_2.xml │ ├── fade_in_growing_3.xml │ ├── fade_in_growing_4.xml │ ├── fade_in_partial_1.xml │ ├── fade_in_partial_2.xml │ ├── fade_in_partial_3.xml │ ├── fade_in_partial_4.xml │ ├── fade_in_shrinking.xml │ ├── fade_out_shrinking.xml │ ├── fill_color_white_to_blue.xml │ ├── fill_color_white_to_orange.xml │ ├── infinite_rotation.xml │ ├── stroke_color_black_to_white.xml │ ├── stroke_color_white_to_blue.xml │ └── stroke_color_white_to_orange.xml │ ├── drawable │ ├── button_preview.xml │ ├── button_preview_animated.xml │ ├── chooser_preview.xml │ ├── chooser_preview_animated.xml │ ├── group_icon.xml │ ├── group_preview.xml │ ├── group_preview_animated.xml │ ├── ic_launcher_animated.xml │ ├── ic_launcher_foreground.xml │ ├── ic_launcher_monochrome.xml │ ├── order_icon.xml │ ├── order_preview.xml │ ├── order_preview_animated.xml │ ├── single_icon.xml │ ├── single_preview.xml │ └── single_preview_animated.xml │ ├── mipmap-anydpi │ └── ic_launcher.xml │ ├── raw │ ├── finger_chosen.mp3 │ ├── finger_down.mp3 │ └── finger_up.mp3 │ └── values │ ├── colors.xml │ ├── ic_launcher_background.xml │ └── themes.xml ├── build.gradle.kts ├── fastlane └── metadata │ └── android │ ├── de │ ├── changelogs │ │ ├── 1.txt │ │ ├── 10.txt │ │ ├── 11.txt │ │ ├── 12.txt │ │ ├── 13.txt │ │ ├── 14.txt │ │ ├── 15.txt │ │ ├── 16.txt │ │ ├── 2.txt │ │ ├── 3.txt │ │ ├── 4.txt │ │ ├── 5.txt │ │ ├── 6.txt │ │ ├── 7.txt │ │ ├── 8.txt │ │ └── 9.txt │ ├── full_description.txt │ └── short_description.txt │ └── en-US │ ├── changelogs │ ├── 1.txt │ ├── 10.txt │ ├── 11.txt │ ├── 12.txt │ ├── 13.txt │ ├── 14.txt │ ├── 15.txt │ ├── 16.txt │ ├── 2.txt │ ├── 3.txt │ ├── 4.txt │ ├── 5.txt │ ├── 6.txt │ ├── 7.txt │ ├── 8.txt │ └── 9.txt │ ├── full_description.txt │ ├── images │ ├── featureGraphic.png │ ├── icon.png │ └── phoneScreenshots │ │ ├── 1.png │ │ ├── 2.png │ │ ├── 3.png │ │ ├── 4.png │ │ └── 5.png │ ├── short_description.txt │ └── title.txt ├── gradle.properties ├── gradle ├── libs.versions.toml └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── readme ├── chooser-icon.svg ├── get-it-on-github.png ├── group-mode.gif ├── order-mode.gif └── single-mode.gif └── settings.gradle.kts /.editorconfig: -------------------------------------------------------------------------------- 1 | # https://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | max_line_length = 140 8 | 9 | charset = utf-8 10 | end_of_line = lf 11 | trim_trailing_whitespace = true 12 | insert_final_newline = true 13 | 14 | [*.{java,kt,kts,xml,gradle}] 15 | indent_size = 4 16 | 17 | [*.md] 18 | trim_trailing_whitespace = false 19 | max_line_length = unset 20 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.github/workflows/build-apk.yml: -------------------------------------------------------------------------------- 1 | name: build-apk 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | branches: [ "main" ] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: checkout sources 14 | uses: actions/checkout@v4 15 | 16 | - name: setup java 17 | uses: actions/setup-java@v4 18 | with: 19 | java-version: '21' 20 | distribution: 'temurin' 21 | 22 | - name: setup gradle 23 | uses: gradle/actions/setup-gradle@v4 24 | 25 | - name: build debug apk 26 | run: ./gradlew assembleDebug --stacktrace 27 | 28 | - name: build release apk 29 | run: ./gradlew assembleRelease --stacktrace 30 | 31 | - name: upload debug apk 32 | uses: actions/upload-artifact@v4 33 | with: 34 | name: app-debug 35 | path: app/build/outputs/apk/debug/*.apk 36 | 37 | - name: upload release apk 38 | uses: actions/upload-artifact@v4 39 | with: 40 | name: app-release 41 | path: app/build/outputs/apk/release/*.apk 42 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Gradle files 2 | .gradle/ 3 | build/ 4 | debug/ 5 | release/ 6 | 7 | # Local configuration file (sdk path, etc) 8 | local.properties 9 | 10 | # Log/OS Files 11 | *.log 12 | 13 | # Android Studio generated files and folders 14 | captures/ 15 | .externalNativeBuild/ 16 | .cxx/ 17 | *.apk 18 | output.json 19 | 20 | # IntelliJ 21 | *.iml 22 | .idea/ 23 | 24 | # Keystore files 25 | *.jks 26 | *.keystore 27 | 28 | # Google Services (e.g. APIs or Firebase) 29 | google-services.json 30 | 31 | # Android Profiling 32 | *.hprof 33 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | App icon 2 | 3 | # Chooser 4 | 5 |
6 | 7 | [Github release](https://github.com/UrAvgCode/Chooser/releases) 9 | [IzzyOnDroid release](https://apt.izzysoft.de/fdroid/index/apk/com.uravgcode.chooser) 11 | [F-Droid release](https://f-droid.org/en/packages/com.uravgcode.chooser) 13 | [Downloads](https://github.com/UrAvgCode/Chooser/releases) 15 | [GPLv3 License](https://www.gnu.org/licenses/gpl-3.0) 17 | 18 | Chooser is an Android app designed to help you make random selections among friends or groups. 19 | Whether you're deciding who should pay at the checkout, who should start a game, or which teams to play in, Chooser has you covered. 20 | 21 | With Chooser, everyone simply touches the screen to select random fingers, ensuring fair and unbiased decisions every time. 22 | It adds an exciting twist to decision-making, making it not only fair but also entertaining. 23 | 24 | ## Features 25 | 26 | - Choose a random person from the group 27 | - Select multiple people at once 28 | - Divide people into customizable groups 29 | - Easily count through a group 30 | 31 | ## Download 32 | 33 | [Get it on Github](https://github.com/UrAvgCode/Chooser/releases) 35 | [Get it on IzzyOnDroid](https://apt.izzysoft.de/fdroid/index/apk/com.uravgcode.chooser) 37 | [Get it on F-Droid](https://f-droid.org/en/packages/com.uravgcode.chooser) 39 | 40 | ## Screenshots 41 | 42 | | Finger Selection | Group Mode | Order Mode | 43 | |-----------------------------|----------------------------|----------------------------| 44 | | ![](readme/single-mode.gif) | ![](readme/group-mode.gif) | ![](readme/order-mode.gif) | 45 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /app/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | alias(libs.plugins.android.application) 3 | alias(libs.plugins.kotlin.android) 4 | alias(libs.plugins.kotlin.compose) 5 | alias(libs.plugins.kotlin.serialization) 6 | } 7 | 8 | android { 9 | namespace = "com.uravgcode.chooser" 10 | compileSdk = 36 11 | 12 | defaultConfig { 13 | applicationId = "com.uravgcode.chooser" 14 | minSdk = 26 15 | targetSdk = 36 16 | versionCode = 16 17 | versionName = "1.4.3" 18 | 19 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" 20 | } 21 | 22 | buildTypes { 23 | release { 24 | isShrinkResources = true 25 | isMinifyEnabled = true 26 | proguardFiles( 27 | getDefaultProguardFile("proguard-android-optimize.txt"), 28 | "proguard-rules.pro" 29 | ) 30 | } 31 | } 32 | 33 | compileOptions { 34 | sourceCompatibility = JavaVersion.VERSION_11 35 | targetCompatibility = JavaVersion.VERSION_11 36 | } 37 | 38 | kotlinOptions { 39 | jvmTarget = "11" 40 | } 41 | 42 | buildFeatures { 43 | compose = true 44 | } 45 | 46 | dependenciesInfo { 47 | includeInApk = false 48 | } 49 | } 50 | 51 | dependencies { 52 | implementation(libs.androidx.core.ktx) 53 | implementation(libs.androidx.lifecycle.runtime.ktx) 54 | 55 | implementation(libs.androidx.datastore) 56 | implementation(libs.kotlinx.serialization.json) 57 | 58 | implementation(libs.androidx.activity.compose) 59 | implementation(platform(libs.androidx.compose.bom)) 60 | 61 | implementation(libs.androidx.material3) 62 | implementation(libs.androidx.material.icons.extended) 63 | 64 | implementation(libs.androidx.ui) 65 | implementation(libs.androidx.ui.graphics) 66 | implementation(libs.androidx.ui.tooling.preview) 67 | 68 | implementation(libs.androidx.animation.graphics) 69 | 70 | implementation(libs.androidx.navigation.compose) 71 | 72 | implementation(libs.androidx.core.splashscreen) 73 | 74 | debugImplementation(libs.androidx.ui.tooling) 75 | debugImplementation(libs.androidx.ui.test.manifest) 76 | } 77 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 12 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /app/src/main/ic_launcher-playstore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UrAvgCode/Chooser/0ba2cbac77c152260c3b909eed64c4c94fc73f38/app/src/main/ic_launcher-playstore.png -------------------------------------------------------------------------------- /app/src/main/java/com/uravgcode/chooser/MainActivity.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 UrAvgCode 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU General Public License as published by 6 | * the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * You should have received a copy of the GNU General Public License 10 | * along with this program. If not, see . 11 | * 12 | * @author UrAvgCode 13 | * @description MainActivity is the entry point of the application. 14 | */ 15 | 16 | package com.uravgcode.chooser 17 | 18 | import android.os.Bundle 19 | import androidx.activity.ComponentActivity 20 | import androidx.activity.compose.setContent 21 | import androidx.activity.enableEdgeToEdge 22 | import com.uravgcode.chooser.navigation.presentation.Navigation 23 | import com.uravgcode.chooser.settings.data.settingsDataStore 24 | import com.uravgcode.chooser.ui.theme.ChooserTheme 25 | 26 | class MainActivity : ComponentActivity() { 27 | override fun onCreate(savedInstanceState: Bundle?) { 28 | super.onCreate(savedInstanceState) 29 | 30 | enableEdgeToEdge() 31 | setContent { 32 | ChooserTheme { 33 | Navigation(settingsDataStore) 34 | } 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /app/src/main/java/com/uravgcode/chooser/chooser/domain/Mode.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 UrAvgCode 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU General Public License as published by 6 | * the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * You should have received a copy of the GNU General Public License 10 | * along with this program. If not, see . 11 | * 12 | * @author UrAvgCode 13 | * @description Mode represents the different modes of the application. 14 | */ 15 | 16 | package com.uravgcode.chooser.chooser.domain 17 | 18 | import com.uravgcode.chooser.R 19 | 20 | enum class Mode { 21 | SINGLE, GROUP, ORDER; 22 | 23 | fun next(): Mode = when (this) { 24 | SINGLE -> GROUP 25 | GROUP -> ORDER 26 | ORDER -> SINGLE 27 | } 28 | 29 | fun initialCount(): Int = when (this) { 30 | SINGLE, ORDER -> 1 31 | GROUP -> 2 32 | } 33 | 34 | fun nextCount(count: Int): Int = when (this) { 35 | SINGLE -> count % 5 + 1 36 | GROUP -> (count - 1) % 4 + 2 37 | ORDER -> 1 38 | } 39 | 40 | fun drawable(): Int = when (this) { 41 | SINGLE -> R.drawable.single_icon 42 | GROUP -> R.drawable.group_icon 43 | ORDER -> R.drawable.order_icon 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /app/src/main/java/com/uravgcode/chooser/chooser/presentation/Chooser.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 UrAvgCode 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU General Public License as published by 6 | * the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * You should have received a copy of the GNU General Public License 10 | * along with this program. If not, see . 11 | * 12 | * @author UrAvgCode 13 | * @description Chooser is the main view of the application. 14 | */ 15 | 16 | package com.uravgcode.chooser.chooser.presentation 17 | 18 | import android.annotation.SuppressLint 19 | import android.content.Context 20 | import android.graphics.Canvas 21 | import android.graphics.Color 22 | import android.os.Build 23 | import android.os.CombinedVibration 24 | import android.os.Handler 25 | import android.os.Looper 26 | import android.os.VibrationEffect 27 | import android.os.Vibrator 28 | import android.os.VibratorManager 29 | import android.view.MotionEvent 30 | import android.view.MotionEvent.ACTION_DOWN 31 | import android.view.MotionEvent.ACTION_MOVE 32 | import android.view.MotionEvent.ACTION_POINTER_DOWN 33 | import android.view.MotionEvent.ACTION_POINTER_UP 34 | import android.view.MotionEvent.ACTION_UP 35 | import android.view.View 36 | import com.uravgcode.chooser.chooser.domain.Mode 37 | import com.uravgcode.chooser.chooser.presentation.circle.Circle 38 | import com.uravgcode.chooser.chooser.presentation.circle.GroupCircle 39 | import com.uravgcode.chooser.chooser.presentation.circle.OrderCircle 40 | import com.uravgcode.chooser.chooser.presentation.component.Number 41 | import com.uravgcode.chooser.chooser.presentation.manager.CircleManager 42 | import com.uravgcode.chooser.chooser.presentation.manager.ColorManager 43 | import com.uravgcode.chooser.chooser.presentation.manager.SoundManager 44 | import kotlin.math.max 45 | import kotlin.math.min 46 | import kotlin.math.sign 47 | import kotlin.random.Random 48 | 49 | @SuppressLint("ViewConstructor") 50 | class Chooser( 51 | context: Context, 52 | val setButtonVisibility: (Boolean) -> Unit 53 | ) : View(context) { 54 | 55 | private val screenHeight = resources.displayMetrics.heightPixels 56 | private val scale = resources.displayMetrics.density 57 | private var previousTime = System.currentTimeMillis() 58 | 59 | private val handler = Handler(Looper.getMainLooper()) 60 | 61 | private val colorManager = ColorManager() 62 | private val soundManager = SoundManager(context) 63 | private val circles = CircleManager() 64 | private val numbers = mutableListOf() 65 | 66 | private var winnerChosen = false 67 | 68 | private val circleSize = 50.0f 69 | private val blackRadiusSize = 105.0f 70 | private var blackRadius = 0f 71 | private var blackSpeed = 1f 72 | 73 | var mode = Mode.SINGLE 74 | var count = 1 75 | 76 | init { 77 | setBackgroundColor(Color.BLACK) 78 | } 79 | 80 | override fun onDraw(canvas: Canvas) { 81 | val currentTime = System.currentTimeMillis() 82 | val deltaTime = currentTime - previousTime 83 | previousTime = currentTime 84 | 85 | circles.update(deltaTime) 86 | 87 | if (winnerChosen && mode == Mode.SINGLE) { 88 | blackSpeed += deltaTime * 0.04f * sign(blackSpeed) 89 | blackRadius = max( 90 | blackRadius + blackSpeed * deltaTime, 91 | blackRadiusSize * circleSizeFactor * scale 92 | ) 93 | circles.drawBlackCircles(canvas, blackRadius, scale) 94 | } 95 | 96 | circles.draw(canvas) 97 | 98 | numbers.removeIf { number -> 99 | number.update(deltaTime) 100 | number.draw(canvas) 101 | number.isMarkedForDeletion() 102 | } 103 | 104 | invalidate() 105 | } 106 | 107 | @SuppressLint("ClickableViewAccessibility") 108 | override fun onTouchEvent(event: MotionEvent?): Boolean { 109 | event ?: return false 110 | 111 | val actionIndex = event.actionIndex 112 | val pointerId = event.getPointerId(actionIndex) 113 | 114 | when (event.actionMasked) { 115 | ACTION_DOWN, ACTION_POINTER_DOWN -> handleActionDown(event, actionIndex, pointerId) 116 | ACTION_MOVE -> handleActionMove(event) 117 | ACTION_UP, ACTION_POINTER_UP -> handleActionUp(pointerId) 118 | } 119 | return true 120 | } 121 | 122 | private fun handleActionDown(event: MotionEvent, actionIndex: Int, pointerId: Int) { 123 | if (winnerChosen) return 124 | 125 | setButtonVisibility(false) 126 | 127 | soundManager.playFingerDown() 128 | circles[pointerId] = createCircle(event.getX(actionIndex), event.getY(actionIndex)) 129 | handler.removeCallbacksAndMessages(null) 130 | handler.postDelayed( 131 | { selectWinner() }, 132 | when (mode) { 133 | Mode.SINGLE -> singleDelay 134 | Mode.GROUP -> groupDelay 135 | Mode.ORDER -> orderDelay 136 | } 137 | ) 138 | } 139 | 140 | private fun handleActionMove(event: MotionEvent) { 141 | for (index in 0 until event.pointerCount) { 142 | circles[event.getPointerId(index)]?.let { circle -> 143 | circle.x = event.getX(index) 144 | circle.y = event.getY(index) 145 | } 146 | } 147 | } 148 | 149 | private fun handleActionUp(pointerId: Int) { 150 | circles.remove(pointerId) 151 | if (!winnerChosen) soundManager.playFingerUp() 152 | if (circles.isEmpty()) resetGame() 153 | } 154 | 155 | private fun createCircle(x: Float, y: Float) = when (mode) { 156 | Mode.SINGLE -> Circle(x, y, circleSize * circleSizeFactor * scale, colorManager.nextColor()) 157 | Mode.GROUP -> GroupCircle(x, y, circleSize * circleSizeFactor * scale) 158 | Mode.ORDER -> OrderCircle(x, y, circleSize * circleSizeFactor * scale, colorManager.nextColor()) 159 | } 160 | 161 | private fun resetGame() { 162 | colorManager.generateRandomColorPalette(5) 163 | 164 | if (!winnerChosen) { 165 | setButtonVisibility(true) 166 | return 167 | } 168 | 169 | handler.postDelayed( 170 | { 171 | blackSpeed = 1f 172 | handler.postDelayed({ 173 | setButtonVisibility(true) 174 | winnerChosen = false 175 | setBackgroundColor(Color.BLACK) 176 | }, 150) 177 | }, 178 | when (mode) { 179 | Mode.SINGLE -> Circle.circleLifetime 180 | Mode.GROUP -> GroupCircle.circleLifetime 181 | Mode.ORDER -> OrderCircle.circleLifetime 182 | } 183 | ) 184 | } 185 | 186 | private fun selectWinner() { 187 | if (circles.size <= count) return 188 | 189 | when (mode) { 190 | Mode.SINGLE -> chooseFinger() 191 | Mode.GROUP -> chooseGroup() 192 | Mode.ORDER -> chooseOrder() 193 | } 194 | 195 | winnerChosen = true 196 | } 197 | 198 | private fun chooseFinger() { 199 | val indexList = circles.keys.toMutableList() 200 | indexList.shuffle() 201 | 202 | indexList.takeLast(count).forEach { index -> 203 | circles[index]!!.setWinner() 204 | } 205 | 206 | indexList.dropLast(count).forEach { index -> 207 | circles.remove(index) 208 | } 209 | 210 | val colors = circles.values.map { it.color } 211 | setBackgroundColor(colorManager.averageColor(colors)) 212 | 213 | blackSpeed = -1f 214 | blackRadius = screenHeight.toFloat() 215 | soundManager.playFingerChosen() 216 | vibrate(100) 217 | } 218 | 219 | private fun chooseGroup() { 220 | val indexList = circles.keys.toMutableList() 221 | val teamSize = circles.size / count 222 | var remainder = circles.size % count 223 | 224 | colorManager.generateRandomColorPalette(count) 225 | repeat(count) { 226 | val size = if (remainder-- > 0) teamSize + 1 else teamSize 227 | 228 | val color = colorManager.nextColor() 229 | repeat(size) { 230 | val randomIndex = Random.nextInt(indexList.size) 231 | val circle = circles[indexList[randomIndex]] 232 | circle!!.color = color 233 | circle.setWinner() 234 | indexList.removeAt(randomIndex) 235 | } 236 | } 237 | 238 | vibrate(100) 239 | } 240 | 241 | private fun chooseOrder(number: Int = 1) { 242 | val selectionMap = circles.filterValues { !it.isWinner() } 243 | if (selectionMap.isEmpty()) return 244 | 245 | val randomIndex = selectionMap.keys.random() 246 | val circle = circles[randomIndex]!! 247 | 248 | circle.setWinner() 249 | numbers.add( 250 | Number( 251 | circle.x, 252 | circle.y - circleSize * circleSizeFactor * scale, 253 | circle.color, 254 | number, 255 | circleSize * circleSizeFactor * scale 256 | ) 257 | ) 258 | soundManager.playFingerUp() 259 | vibrate(40) 260 | 261 | handler.postDelayed( 262 | { chooseOrder(number + 1) }, 263 | min(3000L / circles.size, 800L) 264 | ) 265 | } 266 | 267 | private fun vibrate(millis: Long) { 268 | if (!vibrationEnabled) return 269 | val effect = VibrationEffect.createOneShot(millis, VibrationEffect.DEFAULT_AMPLITUDE) 270 | 271 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { 272 | val systemService = context.getSystemService(Context.VIBRATOR_MANAGER_SERVICE) 273 | val vibratorManager = systemService as VibratorManager 274 | vibratorManager.vibrate(CombinedVibration.createParallel(effect)) 275 | } else { 276 | @Suppress("DEPRECATION") 277 | val vibrator = context.getSystemService(Context.VIBRATOR_SERVICE) as Vibrator 278 | vibrator.vibrate(effect) 279 | } 280 | } 281 | 282 | companion object { 283 | var vibrationEnabled = true 284 | var circleSizeFactor = 1.0f 285 | 286 | var singleDelay = 3000L 287 | var groupDelay = 3000L 288 | var orderDelay = 3000L 289 | } 290 | } 291 | -------------------------------------------------------------------------------- /app/src/main/java/com/uravgcode/chooser/chooser/presentation/ChooserScreen.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 UrAvgCode 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU General Public License as published by 6 | * the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * You should have received a copy of the GNU General Public License 10 | * along with this program. If not, see . 11 | * 12 | * @author UrAvgCode 13 | * @description ChooserScreen is the screen that displays the chooser view. 14 | */ 15 | 16 | package com.uravgcode.chooser.chooser.presentation 17 | 18 | import androidx.compose.foundation.layout.WindowInsets 19 | import androidx.compose.foundation.layout.WindowInsetsSides 20 | import androidx.compose.foundation.layout.fillMaxSize 21 | import androidx.compose.foundation.layout.only 22 | import androidx.compose.foundation.layout.safeDrawing 23 | import androidx.compose.foundation.layout.size 24 | import androidx.compose.material3.Icon 25 | import androidx.compose.material3.Scaffold 26 | import androidx.compose.material3.Text 27 | import androidx.compose.runtime.Composable 28 | import androidx.compose.runtime.LaunchedEffect 29 | import androidx.compose.runtime.collectAsState 30 | import androidx.compose.runtime.getValue 31 | import androidx.compose.runtime.mutableStateOf 32 | import androidx.compose.runtime.remember 33 | import androidx.compose.runtime.rememberCoroutineScope 34 | import androidx.compose.runtime.setValue 35 | import androidx.compose.ui.Alignment 36 | import androidx.compose.ui.Modifier 37 | import androidx.compose.ui.res.painterResource 38 | import androidx.compose.ui.text.style.TextAlign 39 | import androidx.compose.ui.unit.dp 40 | import androidx.compose.ui.unit.max 41 | import androidx.compose.ui.unit.sp 42 | import androidx.compose.ui.viewinterop.AndroidView 43 | import androidx.datastore.core.DataStore 44 | import com.uravgcode.chooser.chooser.domain.Mode 45 | import com.uravgcode.chooser.chooser.presentation.button.AnimatedButton 46 | import com.uravgcode.chooser.chooser.presentation.circle.Circle 47 | import com.uravgcode.chooser.chooser.presentation.circle.GroupCircle 48 | import com.uravgcode.chooser.chooser.presentation.circle.OrderCircle 49 | import com.uravgcode.chooser.chooser.presentation.manager.SoundManager 50 | import com.uravgcode.chooser.settings.data.SettingsData 51 | import kotlinx.coroutines.launch 52 | 53 | @Composable 54 | fun ChooserScreen( 55 | onNavigate: () -> Unit, 56 | dataStore: DataStore 57 | ) { 58 | val coroutineScope = rememberCoroutineScope() 59 | val settings by dataStore.data.collectAsState(initial = SettingsData()) 60 | 61 | var isVisible by remember { mutableStateOf(true) } 62 | 63 | LaunchedEffect(settings) { 64 | SoundManager.soundEnabled = settings.soundEnabled 65 | Chooser.vibrationEnabled = settings.vibrationEnabled 66 | Chooser.circleSizeFactor = settings.circleSizeFactor 67 | 68 | Chooser.singleDelay = settings.singleDelay 69 | Chooser.groupDelay = settings.groupDelay 70 | Chooser.orderDelay = settings.orderDelay 71 | 72 | Circle.circleLifetime = settings.circleLifetime 73 | GroupCircle.circleLifetime = settings.groupCircleLifetime 74 | OrderCircle.circleLifetime = settings.orderCircleLifetime 75 | } 76 | 77 | Scaffold( 78 | contentWindowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top), 79 | ) { padding -> 80 | val buttonTopPadding = remember(settings.fullScreen, settings.additionalButtonPadding) { 81 | val minTopPadding = 24.dp 82 | if (settings.fullScreen) { 83 | minTopPadding 84 | } else { 85 | max(padding.calculateTopPadding(), minTopPadding) 86 | } + settings.additionalButtonPadding.dp 87 | } 88 | 89 | AndroidView( 90 | factory = { context -> 91 | Chooser( 92 | context = context, 93 | setButtonVisibility = { isVisible = it } 94 | ) 95 | }, 96 | modifier = Modifier.fillMaxSize(), 97 | update = { view -> 98 | view.mode = settings.mode 99 | view.count = settings.count 100 | }, 101 | ) 102 | 103 | AnimatedButton( 104 | alignment = Alignment.TopStart, 105 | topPadding = buttonTopPadding, 106 | onClick = { 107 | if (isVisible) { 108 | coroutineScope.launch { 109 | dataStore.updateData { 110 | it.copy( 111 | mode = settings.mode.next(), 112 | count = settings.mode.next().initialCount() 113 | ) 114 | } 115 | } 116 | } 117 | }, 118 | onLongClick = onNavigate, 119 | visible = isVisible, 120 | content = { 121 | Icon( 122 | painter = painterResource(id = settings.mode.drawable()), 123 | contentDescription = "Mode", 124 | modifier = Modifier.size(38.dp) 125 | ) 126 | }, 127 | ) 128 | 129 | AnimatedButton( 130 | alignment = Alignment.TopEnd, 131 | topPadding = buttonTopPadding, 132 | onClick = { 133 | if (isVisible) { 134 | coroutineScope.launch { 135 | dataStore.updateData { 136 | it.copy( 137 | count = settings.mode.nextCount(settings.count) 138 | ) 139 | } 140 | } 141 | } 142 | }, 143 | visible = settings.mode != Mode.ORDER && isVisible, 144 | content = { 145 | Text( 146 | text = settings.count.toString(), 147 | fontSize = 38.sp, 148 | textAlign = TextAlign.Center, 149 | ) 150 | }, 151 | ) 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /app/src/main/java/com/uravgcode/chooser/chooser/presentation/button/AnimatedButton.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 UrAvgCode 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU General Public License as published by 6 | * the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * You should have received a copy of the GNU General Public License 10 | * along with this program. If not, see . 11 | * 12 | * @author UrAvgCode 13 | * @description AnimatedButton is a button that animates in and out. 14 | */ 15 | 16 | package com.uravgcode.chooser.chooser.presentation.button 17 | 18 | import androidx.compose.animation.AnimatedVisibility 19 | import androidx.compose.animation.core.tween 20 | import androidx.compose.animation.slideInVertically 21 | import androidx.compose.animation.slideOutVertically 22 | import androidx.compose.foundation.layout.Box 23 | import androidx.compose.foundation.layout.fillMaxSize 24 | import androidx.compose.foundation.layout.padding 25 | import androidx.compose.runtime.Composable 26 | import androidx.compose.ui.Alignment 27 | import androidx.compose.ui.Modifier 28 | import androidx.compose.ui.unit.Dp 29 | import androidx.compose.ui.unit.dp 30 | 31 | @Composable 32 | fun AnimatedButton( 33 | alignment: Alignment, 34 | topPadding: Dp, 35 | onClick: () -> Unit, 36 | onLongClick: (() -> Unit)? = null, 37 | visible: Boolean = true, 38 | content: @Composable () -> Unit, 39 | ) { 40 | Box(modifier = Modifier.fillMaxSize()) { 41 | AnimatedVisibility( 42 | visible = visible, 43 | enter = slideInVertically( 44 | initialOffsetY = { fullHeight -> -2 * fullHeight - topPadding.value.toInt() }, 45 | animationSpec = tween(durationMillis = 400) 46 | ), 47 | exit = slideOutVertically( 48 | targetOffsetY = { fullHeight -> -2 * fullHeight - topPadding.value.toInt() }, 49 | animationSpec = tween(durationMillis = 400) 50 | ), 51 | modifier = Modifier 52 | .align(alignment) 53 | .padding(horizontal = 24.dp) 54 | .padding(top = topPadding) 55 | ) { 56 | BaseButton( 57 | onClick = onClick, 58 | onLongClick = onLongClick, 59 | content = content, 60 | radius = 56.dp 61 | ) 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /app/src/main/java/com/uravgcode/chooser/chooser/presentation/button/BaseButton.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 UrAvgCode 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU General Public License as published by 6 | * the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * You should have received a copy of the GNU General Public License 10 | * along with this program. If not, see . 11 | * 12 | * @author UrAvgCode 13 | * @description BaseButton is the base component for buttons in the application. 14 | */ 15 | 16 | package com.uravgcode.chooser.chooser.presentation.button 17 | 18 | import androidx.compose.foundation.ExperimentalFoundationApi 19 | import androidx.compose.foundation.combinedClickable 20 | import androidx.compose.foundation.interaction.MutableInteractionSource 21 | import androidx.compose.foundation.layout.Box 22 | import androidx.compose.foundation.layout.size 23 | import androidx.compose.foundation.shape.CircleShape 24 | import androidx.compose.material3.MaterialTheme 25 | import androidx.compose.material3.Surface 26 | import androidx.compose.material3.ripple 27 | import androidx.compose.runtime.Composable 28 | import androidx.compose.runtime.remember 29 | import androidx.compose.ui.Alignment 30 | import androidx.compose.ui.Modifier 31 | import androidx.compose.ui.graphics.Color 32 | import androidx.compose.ui.hapticfeedback.HapticFeedbackType 33 | import androidx.compose.ui.platform.LocalHapticFeedback 34 | import androidx.compose.ui.semantics.Role 35 | import androidx.compose.ui.semantics.role 36 | import androidx.compose.ui.semantics.semantics 37 | import androidx.compose.ui.unit.Dp 38 | 39 | 40 | @Composable 41 | @OptIn(ExperimentalFoundationApi::class) 42 | fun BaseButton( 43 | onClick: () -> Unit, 44 | onLongClick: (() -> Unit)? = null, 45 | content: @Composable () -> Unit, 46 | radius: Dp 47 | ) { 48 | val haptic = LocalHapticFeedback.current 49 | Surface( 50 | shape = CircleShape, 51 | color = MaterialTheme.colorScheme.secondaryContainer, 52 | contentColor = Color.White, 53 | modifier = Modifier 54 | .semantics { 55 | role = Role.Button 56 | } 57 | .combinedClickable( 58 | onClick = onClick, 59 | onLongClick = { 60 | onLongClick?.let { 61 | haptic.performHapticFeedback(HapticFeedbackType.LongPress) 62 | it() 63 | } 64 | }, 65 | interactionSource = remember { MutableInteractionSource() }, 66 | indication = ripple(radius = radius / 2) 67 | ) 68 | .size(radius) 69 | ) { 70 | Box( 71 | contentAlignment = Alignment.Center, 72 | ) { 73 | content() 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /app/src/main/java/com/uravgcode/chooser/chooser/presentation/circle/Circle.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 UrAvgCode 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU General Public License as published by 6 | * the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * You should have received a copy of the GNU General Public License 10 | * along with this program. If not, see . 11 | * 12 | * @author UrAvgCode 13 | * @description Circle is the circle that is drawn in the chooser mode. 14 | * It is the base class for the other circle types. 15 | */ 16 | 17 | package com.uravgcode.chooser.chooser.presentation.circle 18 | 19 | import android.graphics.Canvas 20 | import android.graphics.Color 21 | import android.graphics.Paint 22 | import android.graphics.RectF 23 | import android.os.Handler 24 | import android.os.Looper 25 | import kotlin.math.max 26 | import kotlin.math.min 27 | import kotlin.math.sin 28 | import kotlin.random.Random 29 | 30 | open class Circle(var x: Float, var y: Float, radius: Float, var color: Int) { 31 | 32 | protected val corePaint = Paint().apply { 33 | color = this@Circle.color 34 | style = Paint.Style.FILL_AND_STROKE 35 | } 36 | 37 | protected val ringPaint = Paint().apply { 38 | color = this@Circle.color 39 | style = Paint.Style.STROKE 40 | strokeCap = Paint.Cap.ROUND 41 | } 42 | 43 | protected val ringPaintLight = Paint().apply { 44 | color = Color.argb(65, 255, 255, 255) 45 | style = Paint.Style.STROKE 46 | strokeCap = Paint.Cap.ROUND 47 | } 48 | 49 | private val center = RectF() 50 | private val ring = RectF() 51 | 52 | private var startAngle = Random.nextFloat() * 360f 53 | private var sweepAngle = Random.nextFloat() * -360f 54 | 55 | protected var coreRadius = 0f 56 | protected val defaultRadius = radius 57 | protected val radiusVariance = radius * 0.08f 58 | 59 | protected var winnerCircle = false 60 | protected var hasFinger = true 61 | 62 | protected var timeMillis = 0L 63 | 64 | open fun update(deltaTime: Long) { 65 | val radius = coreRadius + radiusVariance * sin(timeMillis * 0.006f) 66 | val innerRadius = radius * 0.6f 67 | val strokeWidth = radius * 0.19f 68 | 69 | center.set(x - innerRadius, y - innerRadius, x + innerRadius, y + innerRadius) 70 | ring.set(x - radius, y - radius, x + radius, y + radius) 71 | 72 | corePaint.strokeWidth = strokeWidth 73 | ringPaint.strokeWidth = strokeWidth 74 | ringPaintLight.strokeWidth = strokeWidth 75 | 76 | startAngle = (startAngle + deltaTime * 0.3f) % 360 77 | if (sweepAngle <= 360) sweepAngle += deltaTime * 0.45f 78 | 79 | val target = if (hasFinger) defaultRadius else 0f 80 | coreRadius = if (coreRadius < target) { 81 | min(coreRadius + deltaTime * 0.6f, target) 82 | } else { 83 | max(coreRadius - deltaTime * 0.6f, target) 84 | } 85 | 86 | timeMillis += deltaTime 87 | } 88 | 89 | open fun draw(canvas: Canvas) { 90 | canvas.drawOval(center, corePaint) 91 | canvas.drawArc(ring, startAngle, sweepAngle, false, ringPaint) 92 | canvas.drawArc(center, startAngle + 180f, sweepAngle / 2f, false, ringPaintLight) 93 | canvas.drawArc(ring, startAngle, sweepAngle / 2f, false, ringPaintLight) 94 | } 95 | 96 | open fun removeFinger() { 97 | if (winnerCircle) { 98 | val handler = Handler(Looper.getMainLooper()) 99 | handler.postDelayed({ hasFinger = false }, circleLifetime) 100 | } else { 101 | hasFinger = false 102 | } 103 | } 104 | 105 | open fun setWinner() { 106 | winnerCircle = true 107 | } 108 | 109 | fun isWinner(): Boolean = winnerCircle 110 | 111 | fun getRadius(): Float = coreRadius 112 | 113 | fun isMarkedForDeletion(): Boolean = coreRadius <= 0 114 | 115 | companion object { 116 | var circleLifetime = 1000L 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /app/src/main/java/com/uravgcode/chooser/chooser/presentation/circle/GroupCircle.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 UrAvgCode 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU General Public License as published by 6 | * the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * You should have received a copy of the GNU General Public License 10 | * along with this program. If not, see . 11 | * 12 | * @author UrAvgCode 13 | * @description GroupCircle is the circle that is drawn in the group mode. 14 | */ 15 | 16 | package com.uravgcode.chooser.chooser.presentation.circle 17 | 18 | import android.graphics.Color 19 | import android.os.Handler 20 | import android.os.Looper 21 | import androidx.core.graphics.ColorUtils 22 | import kotlin.math.min 23 | 24 | class GroupCircle(x: Float, y: Float, radius: Float) : Circle(x, y, radius, Color.WHITE) { 25 | 26 | private var blend = 0f 27 | private val grayStroke = Color.rgb(180, 180, 180) 28 | private val whiteStroke = Color.argb(65, 255, 255, 255) 29 | 30 | init { 31 | ringPaintLight.color = grayStroke 32 | } 33 | 34 | override fun update(deltaTime: Long) { 35 | super.update(deltaTime) 36 | if (color == corePaint.color || blend > 1f) return 37 | 38 | blend = min(blend + deltaTime * 0.01f, 1f) 39 | val newColor = ColorUtils.blendARGB(Color.WHITE, color, blend) 40 | corePaint.color = newColor 41 | ringPaint.color = newColor 42 | ringPaintLight.color = ColorUtils.blendARGB(grayStroke, whiteStroke, blend) 43 | } 44 | 45 | override fun removeFinger() { 46 | if (winnerCircle) { 47 | val handler = Handler(Looper.getMainLooper()) 48 | handler.postDelayed({ hasFinger = false }, circleLifetime) 49 | } else { 50 | hasFinger = false 51 | } 52 | } 53 | 54 | companion object { 55 | var circleLifetime = 1000L 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /app/src/main/java/com/uravgcode/chooser/chooser/presentation/circle/OrderCircle.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 UrAvgCode 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU General Public License as published by 6 | * the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * You should have received a copy of the GNU General Public License 10 | * along with this program. If not, see . 11 | * 12 | * @author UrAvgCode 13 | * @description OrderCircle is the circle that is drawn in the order mode. 14 | */ 15 | 16 | package com.uravgcode.chooser.chooser.presentation.circle 17 | 18 | import android.graphics.Canvas 19 | import android.graphics.Color 20 | import android.graphics.Paint 21 | import android.os.Handler 22 | import android.os.Looper 23 | import androidx.core.graphics.ColorUtils 24 | import kotlin.math.sin 25 | 26 | class OrderCircle(x: Float, y: Float, radius: Float, color: Int) : Circle(x, y, radius, color) { 27 | 28 | private val textPaint = Paint().apply { 29 | val hsvColor = FloatArray(3) 30 | Color.colorToHSV(this@OrderCircle.color, hsvColor) 31 | hsvColor[1] = 0.2f 32 | hsvColor[2] = 1f 33 | 34 | this.color = Color.HSVToColor(hsvColor) 35 | textAlign = Paint.Align.CENTER 36 | } 37 | 38 | private val textShadowPaint = Paint().apply { 39 | textAlign = Paint.Align.CENTER 40 | this.color = Color.argb(80, 0, 0, 0) 41 | } 42 | 43 | private var number: Int? = null 44 | 45 | override fun update(deltaTime: Long) { 46 | super.update(deltaTime) 47 | corePaint.color = if (coreRadius <= defaultRadius) { 48 | color 49 | } else { 50 | ColorUtils.blendARGB(color, Color.WHITE, 0.5f) 51 | } 52 | 53 | val textSize = coreRadius + radiusVariance * sin(timeMillis * 0.006).toFloat() 54 | textPaint.textSize = textSize 55 | textShadowPaint.textSize = textSize 56 | } 57 | 58 | override fun draw(canvas: Canvas) { 59 | super.draw(canvas) 60 | number?.let { 61 | val y = y - (textPaint.descent() + textPaint.ascent()) / 2f 62 | val shadowOffset = textPaint.textSize * 0.04f 63 | canvas.drawText(it.toString(), x + shadowOffset, y + shadowOffset, textShadowPaint) 64 | canvas.drawText(it.toString(), x, y, textPaint) 65 | } 66 | } 67 | 68 | override fun setWinner() { 69 | winnerCircle = true 70 | coreRadius *= 1.2f 71 | number = ++counter 72 | } 73 | 74 | override fun removeFinger() { 75 | if (winnerCircle) { 76 | val handler = Handler(Looper.getMainLooper()) 77 | handler.postDelayed({ 78 | hasFinger = false 79 | counter-- 80 | }, circleLifetime) 81 | } else { 82 | hasFinger = false 83 | } 84 | } 85 | 86 | companion object { 87 | private var counter = 0 88 | var circleLifetime = 1500L 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /app/src/main/java/com/uravgcode/chooser/chooser/presentation/component/Number.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 UrAvgCode 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU General Public License as published by 6 | * the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * You should have received a copy of the GNU General Public License 10 | * along with this program. If not, see . 11 | * 12 | * @author UrAvgCode 13 | * @description Number represents the floating numbers displayed in the order mode. 14 | */ 15 | 16 | package com.uravgcode.chooser.chooser.presentation.component 17 | 18 | import android.graphics.Canvas 19 | import android.graphics.Paint 20 | import kotlin.math.sin 21 | 22 | class Number(private var x: Float, private var y: Float, color: Int, private val number: Int, private var size: Float) { 23 | 24 | private val textPaint = Paint().apply { 25 | this.color = color 26 | textAlign = Paint.Align.CENTER 27 | textSize = size 28 | } 29 | 30 | private val xOrigin = x 31 | private var alpha = 255f 32 | private var alphaSpeed = 0f 33 | private var time = 0L 34 | 35 | fun update(deltaTime: Long) { 36 | alphaSpeed += deltaTime * 0.00004f 37 | alpha -= alphaSpeed * deltaTime 38 | alphaSpeed += deltaTime * 0.00004f 39 | 40 | textPaint.alpha = alpha.coerceIn(0f, 255f).toInt() 41 | 42 | x = xOrigin + sin(time * 0.003).toFloat() * size * 0.25f 43 | y -= size * deltaTime * 0.0005f 44 | 45 | time += deltaTime 46 | } 47 | 48 | fun draw(canvas: Canvas) { 49 | canvas.drawText(number.toString(), x, y, textPaint) 50 | } 51 | 52 | fun isMarkedForDeletion(): Boolean = alpha <= 0 53 | } 54 | -------------------------------------------------------------------------------- /app/src/main/java/com/uravgcode/chooser/chooser/presentation/manager/CircleManager.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 UrAvgCode 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU General Public License as published by 6 | * the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * You should have received a copy of the GNU General Public License 10 | * along with this program. If not, see . 11 | * 12 | * @author UrAvgCode 13 | * @description CircleManager manages a map of all the circles. 14 | */ 15 | 16 | package com.uravgcode.chooser.chooser.presentation.manager 17 | 18 | import android.graphics.Canvas 19 | import android.graphics.Color 20 | import android.graphics.Paint 21 | import com.uravgcode.chooser.chooser.presentation.circle.Circle 22 | 23 | class CircleManager : MutableMap { 24 | private val blackPaint = Paint().apply { color = Color.BLACK } 25 | 26 | private val activeCircles = mutableMapOf() 27 | private val deadCircles = mutableListOf() 28 | 29 | override val size: Int get() = activeCircles.size 30 | override val entries: MutableSet> get() = activeCircles.entries 31 | override val keys: MutableSet get() = activeCircles.keys 32 | override val values: MutableCollection get() = activeCircles.values 33 | override fun get(key: Int): Circle? = activeCircles[key] 34 | override fun isEmpty(): Boolean = activeCircles.isEmpty() 35 | override fun containsKey(key: Int): Boolean = activeCircles.containsKey(key) 36 | override fun containsValue(value: Circle): Boolean = activeCircles.containsValue(value) 37 | override fun put(key: Int, value: Circle): Circle? = activeCircles.put(key, value) 38 | override fun putAll(from: Map) = activeCircles.putAll(from) 39 | override fun clear() = activeCircles.clear() 40 | 41 | override fun remove(key: Int): Circle? { 42 | return activeCircles.remove(key)?.also { 43 | it.removeFinger() 44 | deadCircles += it 45 | } 46 | } 47 | 48 | fun update(deltaTime: Long) { 49 | activeCircles.forEach { (_, circle) -> circle.update(deltaTime) } 50 | deadCircles.forEach { it.update(deltaTime) } 51 | deadCircles.removeAll { it.isMarkedForDeletion() } 52 | } 53 | 54 | fun draw(canvas: Canvas) { 55 | activeCircles.forEach { (_, circle) -> circle.draw(canvas) } 56 | deadCircles.forEach { it.draw(canvas) } 57 | } 58 | 59 | fun drawBlackCircles(canvas: Canvas, blackRadius: Float, scale: Float) { 60 | activeCircles.values.forEach { circle -> 61 | canvas.drawCircle(circle.x, circle.y, blackRadius, blackPaint) 62 | } 63 | 64 | deadCircles.filter { it.isWinner() }.forEach { circle -> 65 | var radius = blackRadius 66 | if (activeCircles.isNotEmpty()) radius *= circle.getRadius() / (50f * scale) 67 | canvas.drawCircle(circle.x, circle.y, radius, blackPaint) 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /app/src/main/java/com/uravgcode/chooser/chooser/presentation/manager/ColorManager.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 UrAvgCode 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU General Public License as published by 6 | * the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * You should have received a copy of the GNU General Public License 10 | * along with this program. If not, see . 11 | * 12 | * @author UrAvgCode 13 | * @description ColorManager manages the color palette of the circles. 14 | */ 15 | 16 | package com.uravgcode.chooser.chooser.presentation.manager 17 | 18 | import android.graphics.Color 19 | import kotlin.random.Random 20 | 21 | class ColorManager { 22 | private val colorPalette = mutableListOf() 23 | 24 | fun generateRandomColorPalette(amount: Int) { 25 | colorPalette.clear() 26 | val hueStep = 360f / amount 27 | val initialHue = Random.nextFloat() * hueStep 28 | 29 | for (i in 0 until amount) { 30 | val hue = initialHue + hueStep * i 31 | val saturation = 0.5f + Random.nextFloat() / 2f 32 | val value = 0.5f + Random.nextFloat() / 2f 33 | val color = Color.HSVToColor(floatArrayOf(hue, saturation, value)) 34 | colorPalette.add(color) 35 | } 36 | 37 | colorPalette.shuffle() 38 | } 39 | 40 | fun nextColor(): Int { 41 | if (colorPalette.isEmpty()) generateRandomColorPalette(5) 42 | return colorPalette.removeAt(0) 43 | } 44 | 45 | fun averageColor(colors: List): Int { 46 | if (colors.isEmpty()) return Color.BLACK 47 | 48 | val size = colors.size 49 | val r = colors.sumOf { Color.red(it) } / size 50 | val g = colors.sumOf { Color.green(it) } / size 51 | val b = colors.sumOf { Color.blue(it) } / size 52 | 53 | return Color.rgb(r, g, b) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /app/src/main/java/com/uravgcode/chooser/chooser/presentation/manager/SoundManager.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 UrAvgCode 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU General Public License as published by 6 | * the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * You should have received a copy of the GNU General Public License 10 | * along with this program. If not, see . 11 | * 12 | * @author UrAvgCode 13 | * @description SoundManager manages the application sounds. 14 | */ 15 | 16 | package com.uravgcode.chooser.chooser.presentation.manager 17 | 18 | import android.content.Context 19 | import android.media.AudioAttributes 20 | import android.media.SoundPool 21 | import com.uravgcode.chooser.R 22 | 23 | class SoundManager(context: Context) { 24 | private val soundPool: SoundPool 25 | private val fingerUpSound: Int 26 | private val fingerDownSound: Int 27 | private val fingerChosenSound: Int 28 | 29 | init { 30 | val audioAttributes = AudioAttributes.Builder() 31 | .setUsage(AudioAttributes.USAGE_GAME) 32 | .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION) 33 | .build() 34 | 35 | soundPool = SoundPool.Builder() 36 | .setAudioAttributes(audioAttributes) 37 | .setMaxStreams(1) 38 | .build() 39 | 40 | fingerUpSound = soundPool.load(context, R.raw.finger_up, 1) 41 | fingerDownSound = soundPool.load(context, R.raw.finger_down, 1) 42 | fingerChosenSound = soundPool.load(context, R.raw.finger_chosen, 1) 43 | } 44 | 45 | private fun playSound(soundId: Int) { 46 | if (soundEnabled) soundPool.play(soundId, 1f, 1f, 0, 0, 1f) 47 | } 48 | 49 | fun playFingerUp() { 50 | playSound(fingerUpSound) 51 | } 52 | 53 | fun playFingerDown() { 54 | playSound(fingerDownSound) 55 | } 56 | 57 | fun playFingerChosen() { 58 | playSound(fingerChosenSound) 59 | } 60 | 61 | companion object { 62 | var soundEnabled = true 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /app/src/main/java/com/uravgcode/chooser/navigation/domain/Screen.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 UrAvgCode 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU General Public License as published by 6 | * the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * You should have received a copy of the GNU General Public License 10 | * along with this program. If not, see . 11 | * 12 | * @author UrAvgCode 13 | * @description Sealed class representing the different screens available in the app. 14 | */ 15 | 16 | package com.uravgcode.chooser.navigation.domain 17 | 18 | import kotlinx.serialization.Serializable 19 | 20 | sealed class Screen { 21 | @Serializable 22 | data object Tutorial : Screen() 23 | 24 | @Serializable 25 | data object Chooser : Screen() 26 | 27 | @Serializable 28 | data object Settings : Screen() 29 | } 30 | -------------------------------------------------------------------------------- /app/src/main/java/com/uravgcode/chooser/navigation/presentation/Navigation.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 UrAvgCode 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU General Public License as published by 6 | * the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * You should have received a copy of the GNU General Public License 10 | * along with this program. If not, see . 11 | * 12 | * @author UrAvgCode 13 | * @description Navigation composable that sets up the navigation between different screens. 14 | */ 15 | 16 | package com.uravgcode.chooser.navigation.presentation 17 | 18 | import androidx.compose.animation.core.spring 19 | import androidx.compose.animation.fadeIn 20 | import androidx.compose.animation.fadeOut 21 | import androidx.compose.animation.scaleIn 22 | import androidx.compose.animation.scaleOut 23 | import androidx.compose.foundation.background 24 | import androidx.compose.runtime.Composable 25 | import androidx.compose.runtime.collectAsState 26 | import androidx.compose.runtime.getValue 27 | import androidx.compose.runtime.rememberCoroutineScope 28 | import androidx.compose.ui.Modifier 29 | import androidx.compose.ui.graphics.Color 30 | import androidx.datastore.core.DataStore 31 | import androidx.navigation.compose.NavHost 32 | import androidx.navigation.compose.composable 33 | import androidx.navigation.compose.rememberNavController 34 | import com.uravgcode.chooser.chooser.presentation.ChooserScreen 35 | import com.uravgcode.chooser.navigation.domain.Screen 36 | import com.uravgcode.chooser.settings.data.SettingsData 37 | import com.uravgcode.chooser.settings.presentation.SettingsScreen 38 | import com.uravgcode.chooser.tutorial.presentation.TutorialScreen 39 | import kotlinx.coroutines.flow.map 40 | import kotlinx.coroutines.launch 41 | 42 | @Composable 43 | fun Navigation(dataStore: DataStore) { 44 | val navController = rememberNavController() 45 | val coroutineScope = rememberCoroutineScope() 46 | 47 | val hasSeenTutorial by dataStore.data.map { it.hasSeenTutorial }.collectAsState(initial = true) 48 | val startDestination = if (hasSeenTutorial) Screen.Chooser else Screen.Tutorial 49 | 50 | NavHost( 51 | navController = navController, 52 | startDestination = startDestination, 53 | modifier = Modifier.background(Color.Black), 54 | enterTransition = { fadeIn(spring()) + scaleIn(initialScale = 1.1f) }, 55 | exitTransition = { fadeOut(spring()) + scaleOut(targetScale = 1.1f) } 56 | ) { 57 | composable { 58 | TutorialScreen( 59 | onComplete = { 60 | coroutineScope.launch { 61 | dataStore.updateData { 62 | it.copy(hasSeenTutorial = true) 63 | } 64 | navController.navigate(Screen.Chooser) { 65 | popUpTo(Screen.Tutorial) { inclusive = true } 66 | } 67 | } 68 | } 69 | ) 70 | } 71 | composable { 72 | ChooserScreen( 73 | onNavigate = { navController.navigate(Screen.Settings) }, 74 | dataStore = dataStore 75 | ) 76 | } 77 | composable { 78 | SettingsScreen( 79 | onNavigateBack = { navController.popBackStack() }, 80 | dataStore = dataStore 81 | ) 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /app/src/main/java/com/uravgcode/chooser/settings/data/SettingsData.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 UrAvgCode 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU General Public License as published by 6 | * the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * You should have received a copy of the GNU General Public License 10 | * along with this program. If not, see . 11 | * 12 | * @author UrAvgCode 13 | * @description Data model for application settings 14 | */ 15 | 16 | package com.uravgcode.chooser.settings.data 17 | 18 | import com.uravgcode.chooser.chooser.domain.Mode 19 | import kotlinx.serialization.Serializable 20 | 21 | @Serializable 22 | data class SettingsData( 23 | val hasSeenTutorial: Boolean = false, 24 | 25 | val mode: Mode = Mode.SINGLE, 26 | val count: Int = 1, 27 | 28 | val soundEnabled: Boolean = true, 29 | val vibrationEnabled: Boolean = true, 30 | 31 | val fullScreen: Boolean = true, 32 | val additionalButtonPadding: Float = 0.0f, 33 | val circleSizeFactor: Float = 1.0f, 34 | 35 | val singleDelay: Long = 3000L, 36 | val groupDelay: Long = 3000L, 37 | val orderDelay: Long = 3000L, 38 | 39 | val circleLifetime: Long = 1000L, 40 | val groupCircleLifetime: Long = 1000L, 41 | val orderCircleLifetime: Long = 1500L, 42 | ) 43 | -------------------------------------------------------------------------------- /app/src/main/java/com/uravgcode/chooser/settings/data/SettingsDataStore.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 UrAvgCode 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU General Public License as published by 6 | * the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * You should have received a copy of the GNU General Public License 10 | * along with this program. If not, see . 11 | * 12 | * @author UrAvgCode 13 | * @description DataStore extension for persisting application settings 14 | */ 15 | 16 | package com.uravgcode.chooser.settings.data 17 | 18 | import android.content.Context 19 | import androidx.datastore.core.DataStore 20 | import androidx.datastore.dataStore 21 | 22 | val Context.settingsDataStore: DataStore by dataStore( 23 | fileName = "settings.json", 24 | serializer = SettingsSerializer 25 | ) 26 | -------------------------------------------------------------------------------- /app/src/main/java/com/uravgcode/chooser/settings/data/SettingsSerializer.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 UrAvgCode 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU General Public License as published by 6 | * the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * You should have received a copy of the GNU General Public License 10 | * along with this program. If not, see . 11 | * 12 | * @author UrAvgCode 13 | * @description JSON serializer for settings data 14 | */ 15 | 16 | package com.uravgcode.chooser.settings.data 17 | 18 | import androidx.datastore.core.Serializer 19 | import kotlinx.serialization.json.Json 20 | import java.io.InputStream 21 | import java.io.OutputStream 22 | 23 | object SettingsSerializer : Serializer { 24 | override val defaultValue: SettingsData 25 | get() = SettingsData() 26 | 27 | override suspend fun readFrom(input: InputStream): SettingsData { 28 | return try { 29 | Json.decodeFromString( 30 | deserializer = SettingsData.serializer(), 31 | string = input.readBytes().decodeToString() 32 | ) 33 | } catch (e: Exception) { 34 | e.printStackTrace() 35 | defaultValue 36 | } 37 | } 38 | 39 | override suspend fun writeTo( 40 | settingsData: SettingsData, 41 | output: OutputStream 42 | ) { 43 | output.write( 44 | Json.encodeToString( 45 | serializer = SettingsData.serializer(), 46 | value = settingsData 47 | ).encodeToByteArray() 48 | ) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /app/src/main/java/com/uravgcode/chooser/settings/presentation/SettingsScreen.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 UrAvgCode 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU General Public License as published by 6 | * the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * You should have received a copy of the GNU General Public License 10 | * along with this program. If not, see . 11 | * 12 | * @author UrAvgCode, Patch4Code 13 | * @description SettingsScreen is the settings screen of the application. 14 | */ 15 | 16 | package com.uravgcode.chooser.settings.presentation 17 | 18 | import androidx.compose.foundation.layout.WindowInsets 19 | import androidx.compose.foundation.layout.fillMaxSize 20 | import androidx.compose.foundation.layout.padding 21 | import androidx.compose.foundation.layout.safeDrawing 22 | import androidx.compose.foundation.lazy.LazyColumn 23 | import androidx.compose.material3.ExperimentalMaterial3Api 24 | import androidx.compose.material3.Scaffold 25 | import androidx.compose.material3.TopAppBarDefaults 26 | import androidx.compose.runtime.Composable 27 | import androidx.compose.runtime.collectAsState 28 | import androidx.compose.runtime.getValue 29 | import androidx.compose.runtime.rememberCoroutineScope 30 | import androidx.compose.ui.Modifier 31 | import androidx.compose.ui.input.nestedscroll.nestedScroll 32 | import androidx.compose.ui.unit.dp 33 | import androidx.datastore.core.DataStore 34 | import com.uravgcode.chooser.settings.data.SettingsData 35 | import com.uravgcode.chooser.settings.presentation.button.ExportButton 36 | import com.uravgcode.chooser.settings.presentation.button.ImportButton 37 | import com.uravgcode.chooser.settings.presentation.button.ResetButton 38 | import com.uravgcode.chooser.settings.presentation.component.SettingsSeparator 39 | import com.uravgcode.chooser.settings.presentation.component.SettingsSwitch 40 | import com.uravgcode.chooser.settings.presentation.component.SettingsTopAppBar 41 | import com.uravgcode.chooser.settings.presentation.slider.PaddingSlider 42 | import com.uravgcode.chooser.settings.presentation.slider.PercentSlider 43 | import com.uravgcode.chooser.settings.presentation.slider.TimeSlider 44 | import kotlinx.coroutines.launch 45 | 46 | @Composable 47 | @OptIn(ExperimentalMaterial3Api::class) 48 | fun SettingsScreen( 49 | onNavigateBack: () -> Unit, 50 | dataStore: DataStore 51 | ) { 52 | val coroutineScope = rememberCoroutineScope() 53 | val settings by dataStore.data.collectAsState(initial = SettingsData()) 54 | 55 | val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior() 56 | 57 | Scaffold( 58 | modifier = Modifier 59 | .fillMaxSize() 60 | .nestedScroll(scrollBehavior.nestedScrollConnection), 61 | topBar = { 62 | SettingsTopAppBar( 63 | onNavigateBack, 64 | scrollBehavior 65 | ) 66 | }, 67 | contentWindowInsets = WindowInsets.safeDrawing 68 | ) { padding -> 69 | LazyColumn( 70 | modifier = Modifier 71 | .padding(padding) 72 | .padding(horizontal = 16.dp) 73 | ) { 74 | item { 75 | SettingsSeparator( 76 | heading = "General Settings", 77 | showDivider = false, 78 | ) 79 | SettingsSwitch( 80 | title = "Enable Sound", 81 | isChecked = settings.soundEnabled, 82 | onCheckedChange = { isChecked -> 83 | coroutineScope.launch { 84 | dataStore.updateData { it.copy(soundEnabled = isChecked) } 85 | } 86 | } 87 | ) 88 | SettingsSwitch( 89 | title = "Enable Vibration", 90 | isChecked = settings.vibrationEnabled, 91 | onCheckedChange = { isChecked -> 92 | coroutineScope.launch { 93 | dataStore.updateData { it.copy(vibrationEnabled = isChecked) } 94 | } 95 | } 96 | ) 97 | } 98 | 99 | item { 100 | SettingsSeparator("Display Settings") 101 | SettingsSwitch( 102 | title = "Full Screen Mode", 103 | isChecked = settings.fullScreen, 104 | onCheckedChange = { isChecked -> 105 | coroutineScope.launch { 106 | dataStore.updateData { it.copy(fullScreen = isChecked) } 107 | } 108 | } 109 | ) 110 | PaddingSlider( 111 | title = "Additional Button Padding", 112 | value = settings.additionalButtonPadding, 113 | onValueChange = { sliderValue -> 114 | coroutineScope.launch { 115 | dataStore.updateData { it.copy(additionalButtonPadding = sliderValue) } 116 | } 117 | }, 118 | valueRange = 0f..50f, 119 | steps = 9 120 | ) 121 | PercentSlider( 122 | title = "Circle Size", 123 | value = settings.circleSizeFactor, 124 | onValueChange = { sliderValue -> 125 | coroutineScope.launch { 126 | dataStore.updateData { it.copy(circleSizeFactor = sliderValue) } 127 | } 128 | }, 129 | valueRange = 0.5f..1.5f, 130 | steps = 9 131 | ) 132 | } 133 | 134 | item { 135 | SettingsSeparator("Selection Delays") 136 | TimeSlider( 137 | title = "Single Mode Delay", 138 | value = settings.singleDelay, 139 | onValueChange = { sliderValue -> 140 | coroutineScope.launch { 141 | dataStore.updateData { it.copy(singleDelay = sliderValue) } 142 | } 143 | }, 144 | valueRange = 0L..5000L, 145 | steps = 9, 146 | ) 147 | TimeSlider( 148 | title = "Group Mode Delay", 149 | value = settings.groupDelay, 150 | onValueChange = { sliderValue -> 151 | coroutineScope.launch { 152 | dataStore.updateData { it.copy(groupDelay = sliderValue) } 153 | } 154 | }, 155 | valueRange = 0L..5000L, 156 | steps = 9, 157 | ) 158 | TimeSlider( 159 | title = "Order Mode Delay", 160 | value = settings.orderDelay, 161 | onValueChange = { sliderValue -> 162 | coroutineScope.launch { 163 | dataStore.updateData { it.copy(orderDelay = sliderValue) } 164 | } 165 | }, 166 | valueRange = 0L..5000L, 167 | steps = 9, 168 | ) 169 | } 170 | 171 | item { 172 | SettingsSeparator("Circle Lifetimes") 173 | TimeSlider( 174 | title = "Circle Lifetime", 175 | value = settings.circleLifetime, 176 | onValueChange = { sliderValue -> 177 | coroutineScope.launch { 178 | dataStore.updateData { it.copy(circleLifetime = sliderValue) } 179 | } 180 | }, 181 | valueRange = 0L..3000L, 182 | steps = 5, 183 | ) 184 | TimeSlider( 185 | title = "Group Circle Lifetime", 186 | value = settings.groupCircleLifetime, 187 | onValueChange = { sliderValue -> 188 | coroutineScope.launch { 189 | dataStore.updateData { it.copy(groupCircleLifetime = sliderValue) } 190 | } 191 | }, 192 | valueRange = 0L..3000L, 193 | steps = 5, 194 | ) 195 | TimeSlider( 196 | title = "Order Circle Lifetime", 197 | value = settings.orderCircleLifetime, 198 | onValueChange = { sliderValue -> 199 | coroutineScope.launch { 200 | dataStore.updateData { it.copy(orderCircleLifetime = sliderValue) } 201 | } 202 | }, 203 | valueRange = 0L..3000L, 204 | steps = 5, 205 | ) 206 | } 207 | 208 | item { 209 | SettingsSeparator() 210 | ImportButton(dataStore) 211 | ExportButton(dataStore) 212 | ResetButton(dataStore) 213 | } 214 | } 215 | } 216 | } 217 | -------------------------------------------------------------------------------- /app/src/main/java/com/uravgcode/chooser/settings/presentation/button/ExportButton.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 UrAvgCode 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU General Public License as published by 6 | * the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * You should have received a copy of the GNU General Public License 10 | * along with this program. If not, see . 11 | * 12 | * @author UrAvgCode 13 | * @description ExportButton provides functionality to export settings to a file. 14 | */ 15 | 16 | package com.uravgcode.chooser.settings.presentation.button 17 | 18 | import android.widget.Toast 19 | import androidx.activity.compose.rememberLauncherForActivityResult 20 | import androidx.activity.result.contract.ActivityResultContracts 21 | import androidx.compose.runtime.Composable 22 | import androidx.compose.runtime.rememberCoroutineScope 23 | import androidx.compose.ui.platform.LocalContext 24 | import androidx.datastore.core.DataStore 25 | import com.uravgcode.chooser.settings.data.SettingsData 26 | import com.uravgcode.chooser.settings.data.SettingsSerializer 27 | import kotlinx.coroutines.flow.first 28 | import kotlinx.coroutines.launch 29 | 30 | @Composable 31 | fun ExportButton(dataStore: DataStore) { 32 | val scope = rememberCoroutineScope() 33 | val context = LocalContext.current 34 | 35 | val launcher = rememberLauncherForActivityResult( 36 | contract = ActivityResultContracts.CreateDocument("application/json") 37 | ) { uri -> 38 | uri?.let { 39 | scope.launch { 40 | try { 41 | context.contentResolver.openOutputStream(uri)?.use { outputStream -> 42 | SettingsSerializer.writeTo(dataStore.data.first(), outputStream) 43 | } 44 | Toast.makeText(context, "Settings exported successfully", Toast.LENGTH_SHORT).show() 45 | } catch (e: Exception) { 46 | Toast.makeText(context, "Failed to export settings: ${e.message}", Toast.LENGTH_SHORT).show() 47 | } 48 | } 49 | } 50 | } 51 | 52 | SettingsButton( 53 | text = "Export Settings", 54 | onClick = { launcher.launch("settings.json") } 55 | ) 56 | } 57 | -------------------------------------------------------------------------------- /app/src/main/java/com/uravgcode/chooser/settings/presentation/button/ImportButton.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 UrAvgCode 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU General Public License as published by 6 | * the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * You should have received a copy of the GNU General Public License 10 | * along with this program. If not, see . 11 | * 12 | * @author UrAvgCode 13 | * @description ImportButton provides functionality to import settings from a previously exported file. 14 | */ 15 | 16 | package com.uravgcode.chooser.settings.presentation.button 17 | 18 | import android.widget.Toast 19 | import androidx.activity.compose.rememberLauncherForActivityResult 20 | import androidx.activity.result.contract.ActivityResultContracts 21 | import androidx.compose.runtime.Composable 22 | import androidx.compose.runtime.rememberCoroutineScope 23 | import androidx.compose.ui.platform.LocalContext 24 | import androidx.datastore.core.DataStore 25 | import com.uravgcode.chooser.settings.data.SettingsData 26 | import com.uravgcode.chooser.settings.data.SettingsSerializer 27 | import kotlinx.coroutines.launch 28 | 29 | @Composable 30 | fun ImportButton(dataStore: DataStore) { 31 | val scope = rememberCoroutineScope() 32 | val context = LocalContext.current 33 | 34 | val launcher = rememberLauncherForActivityResult( 35 | contract = ActivityResultContracts.OpenDocument() 36 | ) { uri -> 37 | uri?.let { 38 | scope.launch { 39 | try { 40 | context.contentResolver.openInputStream(uri)?.use { inputStream -> 41 | val imported = SettingsSerializer.readFrom(inputStream) 42 | dataStore.updateData { imported } 43 | } 44 | Toast.makeText(context, "Settings imported successfully", Toast.LENGTH_SHORT).show() 45 | } catch (e: Exception) { 46 | Toast.makeText(context, "Failed to import settings: ${e.message}", Toast.LENGTH_SHORT).show() 47 | } 48 | } 49 | } 50 | } 51 | 52 | SettingsButton( 53 | text = "Import Settings", 54 | onClick = { launcher.launch(arrayOf("application/json")) } 55 | ) 56 | } 57 | -------------------------------------------------------------------------------- /app/src/main/java/com/uravgcode/chooser/settings/presentation/button/ResetButton.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 UrAvgCode 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU General Public License as published by 6 | * the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * You should have received a copy of the GNU General Public License 10 | * along with this program. If not, see . 11 | * 12 | * @author UrAvgCode 13 | * @description ResetButton allows users to restore all settings to their default values. 14 | */ 15 | 16 | package com.uravgcode.chooser.settings.presentation.button 17 | 18 | import androidx.compose.material3.AlertDialog 19 | import androidx.compose.material3.Button 20 | import androidx.compose.material3.Text 21 | import androidx.compose.runtime.Composable 22 | import androidx.compose.runtime.getValue 23 | import androidx.compose.runtime.mutableStateOf 24 | import androidx.compose.runtime.remember 25 | import androidx.compose.runtime.rememberCoroutineScope 26 | import androidx.compose.runtime.setValue 27 | import androidx.datastore.core.DataStore 28 | import com.uravgcode.chooser.settings.data.SettingsData 29 | import kotlinx.coroutines.launch 30 | 31 | @Composable 32 | fun ResetButton(dataStore: DataStore) { 33 | val coroutineScope = rememberCoroutineScope() 34 | var showResetDialog by remember { mutableStateOf(false) } 35 | 36 | if (showResetDialog) { 37 | AlertDialog( 38 | onDismissRequest = { showResetDialog = false }, 39 | confirmButton = { 40 | Button( 41 | onClick = { 42 | coroutineScope.launch { 43 | dataStore.updateData { SettingsData(hasSeenTutorial = true) } 44 | showResetDialog = false 45 | } 46 | } 47 | ) { 48 | Text("Reset") 49 | } 50 | }, 51 | dismissButton = { 52 | Button( 53 | onClick = { showResetDialog = false } 54 | ) { 55 | Text("Cancel") 56 | } 57 | }, 58 | title = { Text(text = "Reset Settings") }, 59 | text = { Text(text = "Are you sure you want to reset all settings to their default values?") }, 60 | ) 61 | } 62 | 63 | SettingsButton( 64 | text = "Reset Settings", 65 | onClick = { showResetDialog = true } 66 | ) 67 | } 68 | -------------------------------------------------------------------------------- /app/src/main/java/com/uravgcode/chooser/settings/presentation/button/SettingsButton.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 UrAvgCode 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU General Public License as published by 6 | * the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * You should have received a copy of the GNU General Public License 10 | * along with this program. If not, see . 11 | * 12 | * @author UrAvgCode 13 | * @description SettingsButton is a reusable button component for the settings screen. 14 | */ 15 | 16 | package com.uravgcode.chooser.settings.presentation.button 17 | 18 | import androidx.compose.foundation.layout.fillMaxWidth 19 | import androidx.compose.foundation.layout.padding 20 | import androidx.compose.material3.ElevatedButton 21 | import androidx.compose.material3.Text 22 | import androidx.compose.runtime.Composable 23 | import androidx.compose.ui.Modifier 24 | import androidx.compose.ui.unit.dp 25 | 26 | @Composable 27 | fun SettingsButton( 28 | text: String, 29 | onClick: () -> Unit, 30 | ) { 31 | ElevatedButton( 32 | content = { Text(text) }, 33 | onClick = onClick, 34 | modifier = Modifier 35 | .fillMaxWidth() 36 | .padding(vertical = 4.dp) 37 | ) 38 | } 39 | -------------------------------------------------------------------------------- /app/src/main/java/com/uravgcode/chooser/settings/presentation/component/SettingsSeparator.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 UrAvgCode 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU General Public License as published by 6 | * the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * You should have received a copy of the GNU General Public License 10 | * along with this program. If not, see . 11 | * 12 | * @author UrAvgCode 13 | * @description SettingsSeparator separates settings groups with a horizontal divider and a heading. 14 | */ 15 | 16 | package com.uravgcode.chooser.settings.presentation.component 17 | 18 | import androidx.compose.foundation.layout.padding 19 | import androidx.compose.material3.HorizontalDivider 20 | import androidx.compose.material3.MaterialTheme 21 | import androidx.compose.material3.Text 22 | import androidx.compose.runtime.Composable 23 | import androidx.compose.ui.Modifier 24 | import androidx.compose.ui.unit.dp 25 | 26 | @Composable 27 | fun SettingsSeparator( 28 | heading: String? = null, 29 | showDivider: Boolean = true 30 | ) { 31 | if (showDivider) { 32 | HorizontalDivider( 33 | modifier = Modifier.padding(vertical = 8.dp) 34 | ) 35 | } 36 | heading?.let { 37 | Text( 38 | text = it.uppercase(), 39 | style = MaterialTheme.typography.titleMedium, 40 | modifier = Modifier.padding(vertical = 8.dp) 41 | ) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /app/src/main/java/com/uravgcode/chooser/settings/presentation/component/SettingsSwitch.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 UrAvgCode 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU General Public License as published by 6 | * the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * You should have received a copy of the GNU General Public License 10 | * along with this program. If not, see . 11 | * 12 | * @author UrAvgCode 13 | * @description SettingsSwitch is a component that provides a toggle switch for settings. 14 | */ 15 | 16 | package com.uravgcode.chooser.settings.presentation.component 17 | 18 | import androidx.compose.foundation.layout.Row 19 | import androidx.compose.foundation.layout.fillMaxWidth 20 | import androidx.compose.foundation.layout.padding 21 | import androidx.compose.material3.MaterialTheme 22 | import androidx.compose.material3.Switch 23 | import androidx.compose.material3.SwitchDefaults 24 | import androidx.compose.material3.Text 25 | import androidx.compose.runtime.Composable 26 | import androidx.compose.ui.Alignment 27 | import androidx.compose.ui.Modifier 28 | import androidx.compose.ui.unit.dp 29 | 30 | @Composable 31 | fun SettingsSwitch( 32 | title: String, 33 | isChecked: Boolean, 34 | onCheckedChange: (Boolean) -> Unit 35 | ) { 36 | Row( 37 | modifier = Modifier 38 | .fillMaxWidth() 39 | .padding(vertical = 8.dp), 40 | verticalAlignment = Alignment.CenterVertically 41 | ) { 42 | Text( 43 | text = title, 44 | modifier = Modifier.weight(1f) 45 | ) 46 | Switch( 47 | checked = isChecked, 48 | onCheckedChange = onCheckedChange, 49 | colors = SwitchDefaults.colors( 50 | checkedThumbColor = MaterialTheme.colorScheme.primary, 51 | uncheckedThumbColor = MaterialTheme.colorScheme.onSurface, 52 | checkedTrackColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.54f), 53 | uncheckedTrackColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f) 54 | ) 55 | ) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /app/src/main/java/com/uravgcode/chooser/settings/presentation/component/SettingsTopAppBar.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 UrAvgCode 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU General Public License as published by 6 | * the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * You should have received a copy of the GNU General Public License 10 | * along with this program. If not, see . 11 | * 12 | * @author UrAvgCode 13 | * @description SettingsTopAppBar is a top bar with a back button for the settings screen . 14 | */ 15 | 16 | package com.uravgcode.chooser.settings.presentation.component 17 | 18 | import androidx.compose.foundation.layout.WindowInsets 19 | import androidx.compose.foundation.layout.WindowInsetsSides 20 | import androidx.compose.foundation.layout.only 21 | import androidx.compose.foundation.layout.safeDrawing 22 | import androidx.compose.material.icons.Icons 23 | import androidx.compose.material.icons.automirrored.filled.ArrowBack 24 | import androidx.compose.material3.ExperimentalMaterial3Api 25 | import androidx.compose.material3.Icon 26 | import androidx.compose.material3.IconButton 27 | import androidx.compose.material3.MaterialTheme 28 | import androidx.compose.material3.Text 29 | import androidx.compose.material3.TopAppBar 30 | import androidx.compose.material3.TopAppBarScrollBehavior 31 | import androidx.compose.runtime.Composable 32 | 33 | @Composable 34 | @OptIn(ExperimentalMaterial3Api::class) 35 | fun SettingsTopAppBar( 36 | onNavigateBack: () -> Unit, 37 | scrollBehavior: TopAppBarScrollBehavior?, 38 | ) { 39 | TopAppBar( 40 | title = { 41 | Text( 42 | text = "Settings", 43 | style = MaterialTheme.typography.titleLarge 44 | ) 45 | }, 46 | navigationIcon = { 47 | IconButton(onClick = onNavigateBack) { 48 | Icon( 49 | imageVector = Icons.AutoMirrored.Filled.ArrowBack, 50 | contentDescription = "Back" 51 | ) 52 | } 53 | }, 54 | windowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top), 55 | scrollBehavior = scrollBehavior 56 | ) 57 | } 58 | -------------------------------------------------------------------------------- /app/src/main/java/com/uravgcode/chooser/settings/presentation/slider/PaddingSlider.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 UrAvgCode 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU General Public License as published by 6 | * the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * You should have received a copy of the GNU General Public License 10 | * along with this program. If not, see . 11 | * 12 | * @author UrAvgCode 13 | * @description PaddingSlider is a component that provides a settings slider for padding values. 14 | */ 15 | 16 | package com.uravgcode.chooser.settings.presentation.slider 17 | 18 | import androidx.compose.runtime.Composable 19 | import kotlin.math.roundToInt 20 | 21 | @Composable 22 | fun PaddingSlider( 23 | title: String, 24 | value: Float, 25 | onValueChange: (Float) -> Unit, 26 | valueRange: ClosedFloatingPointRange = 0f..50f, 27 | steps: Int = 0 28 | ) { 29 | SettingsSlider( 30 | title = title, 31 | value = value, 32 | onValueChange = onValueChange, 33 | valueRange = valueRange, 34 | steps = steps, 35 | valueFormatter = { "${it.roundToInt()} dp" } 36 | ) 37 | } 38 | -------------------------------------------------------------------------------- /app/src/main/java/com/uravgcode/chooser/settings/presentation/slider/PercentSlider.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 UrAvgCode 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU General Public License as published by 6 | * the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * You should have received a copy of the GNU General Public License 10 | * along with this program. If not, see . 11 | * 12 | * @author UrAvgCode 13 | * @description PercentSlider is a component that provides a settings slider for percent values. 14 | */ 15 | 16 | package com.uravgcode.chooser.settings.presentation.slider 17 | 18 | import androidx.compose.runtime.Composable 19 | import kotlin.math.roundToInt 20 | 21 | @Composable 22 | fun PercentSlider( 23 | title: String, 24 | value: Float, 25 | onValueChange: (Float) -> Unit, 26 | valueRange: ClosedFloatingPointRange = 0f..1f, 27 | steps: Int = 0 28 | ) { 29 | SettingsSlider( 30 | title = title, 31 | value = value, 32 | onValueChange = onValueChange, 33 | valueRange = valueRange, 34 | steps = steps, 35 | valueFormatter = { "${(it * 100).roundToInt()}%" } 36 | ) 37 | } 38 | -------------------------------------------------------------------------------- /app/src/main/java/com/uravgcode/chooser/settings/presentation/slider/SettingsSlider.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 UrAvgCode 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU General Public License as published by 6 | * the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * You should have received a copy of the GNU General Public License 10 | * along with this program. If not, see . 11 | * 12 | * @author UrAvgCode 13 | * @description SettingsSlider is a reusable slider component for the settings screen. 14 | */ 15 | 16 | package com.uravgcode.chooser.settings.presentation.slider 17 | 18 | import androidx.compose.foundation.layout.Column 19 | import androidx.compose.foundation.layout.fillMaxWidth 20 | import androidx.compose.foundation.layout.padding 21 | import androidx.compose.material3.Slider 22 | import androidx.compose.material3.Text 23 | import androidx.compose.runtime.Composable 24 | import androidx.compose.ui.Modifier 25 | import androidx.compose.ui.unit.dp 26 | 27 | @Composable 28 | fun SettingsSlider( 29 | title: String, 30 | value: Float, 31 | onValueChange: (Float) -> Unit, 32 | valueRange: ClosedFloatingPointRange, 33 | steps: Int, 34 | valueFormatter: (Float) -> String, 35 | ) { 36 | Column( 37 | modifier = Modifier 38 | .fillMaxWidth() 39 | .padding(vertical = 8.dp) 40 | ) { 41 | Text( 42 | text = "$title: ${valueFormatter(value)}", 43 | modifier = Modifier.padding(vertical = 8.dp) 44 | ) 45 | Slider( 46 | value = value, 47 | onValueChange = onValueChange, 48 | valueRange = valueRange, 49 | steps = steps, 50 | modifier = Modifier.padding(horizontal = 16.dp), 51 | ) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /app/src/main/java/com/uravgcode/chooser/settings/presentation/slider/TimeSlider.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 UrAvgCode 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU General Public License as published by 6 | * the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * You should have received a copy of the GNU General Public License 10 | * along with this program. If not, see . 11 | * 12 | * @author UrAvgCode 13 | * @description TimeSlider is a component that provides a settings slider for time values. 14 | */ 15 | 16 | package com.uravgcode.chooser.settings.presentation.slider 17 | 18 | import androidx.compose.runtime.Composable 19 | import kotlin.math.roundToLong 20 | 21 | @Composable 22 | fun TimeSlider( 23 | title: String, 24 | value: Long, 25 | onValueChange: (Long) -> Unit, 26 | valueRange: ClosedRange = 0L..3000L, 27 | steps: Int = 5 28 | ) { 29 | SettingsSlider( 30 | title = title, 31 | value = value.toFloat(), 32 | onValueChange = { onValueChange(it.roundToLong()) }, 33 | valueRange = valueRange.start.toFloat()..valueRange.endInclusive.toFloat(), 34 | steps = steps, 35 | valueFormatter = { "${it / 1000}s" } 36 | ) 37 | } 38 | -------------------------------------------------------------------------------- /app/src/main/java/com/uravgcode/chooser/tutorial/domain/PageData.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 UrAvgCode 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU General Public License as published by 6 | * the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * You should have received a copy of the GNU General Public License 10 | * along with this program. If not, see . 11 | * 12 | * @author UrAvgCode 13 | * @description Data class that defines the content structure for a single tutorial page. 14 | */ 15 | 16 | package com.uravgcode.chooser.tutorial.domain 17 | 18 | import androidx.annotation.DrawableRes 19 | 20 | data class PageData( 21 | @DrawableRes val iconId: Int? = null, 22 | @DrawableRes val previewId: Int, 23 | val title: String, 24 | val description: String, 25 | ) 26 | -------------------------------------------------------------------------------- /app/src/main/java/com/uravgcode/chooser/tutorial/domain/PagerContent.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 UrAvgCode 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU General Public License as published by 6 | * the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * You should have received a copy of the GNU General Public License 10 | * along with this program. If not, see . 11 | * 12 | * @author UrAvgCode 13 | * @description A list of page data for the tutorial pager. 14 | */ 15 | 16 | package com.uravgcode.chooser.tutorial.domain 17 | 18 | import com.uravgcode.chooser.R 19 | 20 | val pagerContent = listOf( 21 | PageData( 22 | previewId = R.drawable.chooser_preview_animated, 23 | title = "Welcome to Chooser", 24 | description = "Make quick, unbiased decisions with a touch. " + 25 | "Place multiple fingers on screen, wait a brief moment, and Chooser will do the rest.", 26 | ), 27 | PageData( 28 | previewId = R.drawable.button_preview_animated, 29 | title = "How to Use", 30 | description = """ 31 |

32 | Mode Button
33 | Switch between Single, Group, and Order modes. 34 | Long press to access additional settings. 35 |

36 |
37 |

38 | Number Button
39 | Adjust how many fingers to select or groups to create. 40 |

41 | """, 42 | ), 43 | PageData( 44 | iconId = R.drawable.single_icon, 45 | previewId = R.drawable.single_preview_animated, 46 | title = "Single Mode", 47 | description = "Selects a random finger from all touching the screen.", 48 | ), 49 | PageData( 50 | iconId = R.drawable.group_icon, 51 | previewId = R.drawable.group_preview_animated, 52 | title = "Group Mode", 53 | description = "Divides all fingers into balanced teams or groups.", 54 | ), 55 | PageData( 56 | iconId = R.drawable.order_icon, 57 | previewId = R.drawable.order_preview_animated, 58 | title = "Order Mode", 59 | description = "Creates a random sequence of all fingers on screen.", 60 | ), 61 | ) 62 | -------------------------------------------------------------------------------- /app/src/main/java/com/uravgcode/chooser/tutorial/presentation/TutorialScreen.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 UrAvgCode 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU General Public License as published by 6 | * the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * You should have received a copy of the GNU General Public License 10 | * along with this program. If not, see . 11 | * 12 | * @author UrAvgCode 13 | * @description TutorialScreen displays tutorial content on first app start. 14 | */ 15 | 16 | package com.uravgcode.chooser.tutorial.presentation 17 | 18 | import androidx.compose.foundation.layout.Arrangement 19 | import androidx.compose.foundation.layout.Box 20 | import androidx.compose.foundation.layout.Column 21 | import androidx.compose.foundation.layout.WindowInsets 22 | import androidx.compose.foundation.layout.fillMaxWidth 23 | import androidx.compose.foundation.layout.padding 24 | import androidx.compose.foundation.layout.safeDrawing 25 | import androidx.compose.foundation.pager.HorizontalPager 26 | import androidx.compose.foundation.pager.rememberPagerState 27 | import androidx.compose.material.icons.Icons 28 | import androidx.compose.material.icons.automirrored.filled.ArrowBack 29 | import androidx.compose.material.icons.automirrored.filled.ArrowForward 30 | import androidx.compose.material.icons.filled.Check 31 | import androidx.compose.material3.Icon 32 | import androidx.compose.material3.IconButton 33 | import androidx.compose.material3.MaterialTheme 34 | import androidx.compose.material3.Scaffold 35 | import androidx.compose.material3.Text 36 | import androidx.compose.material3.TextButton 37 | import androidx.compose.runtime.Composable 38 | import androidx.compose.runtime.derivedStateOf 39 | import androidx.compose.runtime.getValue 40 | import androidx.compose.runtime.remember 41 | import androidx.compose.runtime.rememberCoroutineScope 42 | import androidx.compose.ui.Alignment 43 | import androidx.compose.ui.Modifier 44 | import androidx.compose.ui.unit.dp 45 | import com.uravgcode.chooser.tutorial.domain.pagerContent 46 | import com.uravgcode.chooser.tutorial.presentation.component.PageIndicator 47 | import com.uravgcode.chooser.tutorial.presentation.component.TutorialPage 48 | import kotlinx.coroutines.launch 49 | import kotlin.math.absoluteValue 50 | 51 | @Composable 52 | fun TutorialScreen(onComplete: () -> Unit) { 53 | val coroutineScope = rememberCoroutineScope() 54 | val pagerState = rememberPagerState(pageCount = { pagerContent.size }) 55 | 56 | Scaffold( 57 | contentWindowInsets = WindowInsets.safeDrawing, 58 | ) { padding -> 59 | Column( 60 | modifier = Modifier.padding(padding), 61 | verticalArrangement = Arrangement.Center 62 | ) { 63 | Box( 64 | modifier = Modifier 65 | .fillMaxWidth() 66 | .padding(horizontal = 16.dp), 67 | ) { 68 | TextButton( 69 | onClick = { onComplete() }, 70 | modifier = Modifier.align(Alignment.TopEnd) 71 | ) { 72 | Text( 73 | text = "Skip", 74 | style = MaterialTheme.typography.bodyLarge, 75 | ) 76 | } 77 | } 78 | 79 | HorizontalPager( 80 | state = pagerState, 81 | modifier = Modifier.weight(1f), 82 | ) { page -> 83 | val isPageFullyVisible by remember { 84 | derivedStateOf { 85 | pagerState.currentPage == page && 86 | pagerState.currentPageOffsetFraction.absoluteValue < 0.1f 87 | } 88 | } 89 | 90 | TutorialPage( 91 | iconId = pagerContent[page].iconId, 92 | previewId = pagerContent[page].previewId, 93 | title = pagerContent[page].title, 94 | description = pagerContent[page].description, 95 | isVisible = isPageFullyVisible, 96 | ) 97 | } 98 | 99 | Box( 100 | modifier = Modifier 101 | .fillMaxWidth() 102 | .padding(horizontal = 16.dp), 103 | ) { 104 | IconButton( 105 | onClick = { 106 | coroutineScope.launch { 107 | pagerState.animateScrollToPage(pagerState.currentPage - 1) 108 | } 109 | }, 110 | modifier = Modifier.align(Alignment.CenterStart) 111 | ) { 112 | Icon( 113 | imageVector = Icons.AutoMirrored.Filled.ArrowBack, 114 | contentDescription = "Back" 115 | ) 116 | } 117 | 118 | PageIndicator( 119 | modifier = Modifier.align(Alignment.Center), 120 | pagerState = pagerState 121 | ) 122 | 123 | IconButton( 124 | onClick = { 125 | if (pagerState.currentPage < pagerState.pageCount - 1) { 126 | coroutineScope.launch { 127 | pagerState.animateScrollToPage(pagerState.currentPage + 1) 128 | } 129 | } else { 130 | onComplete() 131 | } 132 | }, 133 | modifier = Modifier.align(Alignment.CenterEnd) 134 | ) { 135 | Icon( 136 | imageVector = if (pagerState.currentPage < pagerState.pageCount - 1) 137 | Icons.AutoMirrored.Filled.ArrowForward 138 | else 139 | Icons.Default.Check, 140 | contentDescription = "Next" 141 | ) 142 | } 143 | } 144 | } 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /app/src/main/java/com/uravgcode/chooser/tutorial/presentation/component/PageIndicator.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 UrAvgCode 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU General Public License as published by 6 | * the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * You should have received a copy of the GNU General Public License 10 | * along with this program. If not, see . 11 | * 12 | * @author UrAvgCode 13 | * @description PageIndicator shows the current page position with animated indicators. 14 | */ 15 | 16 | package com.uravgcode.chooser.tutorial.presentation.component 17 | 18 | import androidx.compose.foundation.background 19 | import androidx.compose.foundation.layout.Arrangement 20 | import androidx.compose.foundation.layout.Box 21 | import androidx.compose.foundation.layout.Row 22 | import androidx.compose.foundation.layout.height 23 | import androidx.compose.foundation.layout.width 24 | import androidx.compose.foundation.pager.PagerState 25 | import androidx.compose.foundation.shape.CircleShape 26 | import androidx.compose.material3.MaterialTheme 27 | import androidx.compose.runtime.Composable 28 | import androidx.compose.ui.Modifier 29 | import androidx.compose.ui.unit.dp 30 | import kotlin.math.absoluteValue 31 | 32 | @Composable 33 | fun PageIndicator( 34 | modifier: Modifier = Modifier, 35 | pagerState: PagerState 36 | ) { 37 | Row( 38 | modifier = modifier, 39 | horizontalArrangement = Arrangement.spacedBy(8.dp) 40 | ) { 41 | val page = pagerState.currentPage 42 | val offset = pagerState.currentPageOffsetFraction 43 | 44 | repeat(pagerState.pageCount) { index -> 45 | val distance = (index - (page + offset)).absoluteValue 46 | val percentage = (1f - distance).coerceAtLeast(0f) 47 | 48 | val width = 8.dp + (10.dp * percentage) 49 | val alpha = 0.5f + (0.5f * percentage) 50 | 51 | Box( 52 | modifier = Modifier 53 | .width(width) 54 | .height(8.dp) 55 | .background( 56 | color = MaterialTheme.colorScheme.primary.copy(alpha = alpha), 57 | shape = CircleShape 58 | ) 59 | ) 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /app/src/main/java/com/uravgcode/chooser/tutorial/presentation/component/TutorialPage.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 UrAvgCode 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU General Public License as published by 6 | * the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * You should have received a copy of the GNU General Public License 10 | * along with this program. If not, see . 11 | * 12 | * @author UrAvgCode 13 | * @description TutorialPage displays a tutorial page with an animated preview. 14 | */ 15 | 16 | package com.uravgcode.chooser.tutorial.presentation.component 17 | 18 | import androidx.annotation.DrawableRes 19 | import androidx.compose.animation.graphics.ExperimentalAnimationGraphicsApi 20 | import androidx.compose.animation.graphics.res.animatedVectorResource 21 | import androidx.compose.animation.graphics.res.rememberAnimatedVectorPainter 22 | import androidx.compose.animation.graphics.vector.AnimatedImageVector 23 | import androidx.compose.foundation.Image 24 | import androidx.compose.foundation.layout.Arrangement 25 | import androidx.compose.foundation.layout.Column 26 | import androidx.compose.foundation.layout.Row 27 | import androidx.compose.foundation.layout.aspectRatio 28 | import androidx.compose.foundation.layout.fillMaxSize 29 | import androidx.compose.foundation.layout.padding 30 | import androidx.compose.foundation.layout.size 31 | import androidx.compose.foundation.shape.CircleShape 32 | import androidx.compose.material3.Icon 33 | import androidx.compose.material3.MaterialTheme 34 | import androidx.compose.material3.Surface 35 | import androidx.compose.material3.Text 36 | import androidx.compose.runtime.Composable 37 | import androidx.compose.runtime.LaunchedEffect 38 | import androidx.compose.runtime.getValue 39 | import androidx.compose.runtime.mutableStateOf 40 | import androidx.compose.runtime.remember 41 | import androidx.compose.runtime.setValue 42 | import androidx.compose.ui.Alignment 43 | import androidx.compose.ui.Modifier 44 | import androidx.compose.ui.layout.ContentScale 45 | import androidx.compose.ui.res.painterResource 46 | import androidx.compose.ui.text.AnnotatedString 47 | import androidx.compose.ui.text.font.FontWeight 48 | import androidx.compose.ui.text.fromHtml 49 | import androidx.compose.ui.text.style.TextAlign 50 | import androidx.compose.ui.unit.dp 51 | import kotlinx.coroutines.delay 52 | 53 | @Composable 54 | @OptIn(ExperimentalAnimationGraphicsApi::class) 55 | fun TutorialPage( 56 | @DrawableRes iconId: Int? = null, 57 | @DrawableRes previewId: Int, 58 | title: String, 59 | description: String, 60 | isVisible: Boolean = true, 61 | ) { 62 | val image = AnimatedImageVector.animatedVectorResource(previewId) 63 | var atEnd by remember { mutableStateOf(false) } 64 | 65 | LaunchedEffect(isVisible) { 66 | if (isVisible) { 67 | delay(600) 68 | atEnd = true 69 | } 70 | } 71 | 72 | Column( 73 | modifier = Modifier 74 | .fillMaxSize() 75 | .padding(horizontal = 16.dp), 76 | horizontalAlignment = Alignment.CenterHorizontally, 77 | verticalArrangement = Arrangement.Center 78 | ) { 79 | Surface( 80 | shape = CircleShape, 81 | color = MaterialTheme.colorScheme.surfaceContainerLowest, 82 | modifier = Modifier 83 | .aspectRatio(1f) 84 | .weight(weight = 1f, fill = false), 85 | ) { 86 | Image( 87 | painter = rememberAnimatedVectorPainter(image, atEnd), 88 | contentDescription = null, 89 | modifier = Modifier.fillMaxSize(), 90 | contentScale = ContentScale.Fit, 91 | ) 92 | } 93 | 94 | Row( 95 | modifier = Modifier.padding(top = 16.dp), 96 | verticalAlignment = Alignment.CenterVertically, 97 | ) { 98 | iconId?.let { 99 | Icon( 100 | painter = painterResource(it), 101 | contentDescription = null, 102 | modifier = Modifier 103 | .size(42.dp) 104 | .padding(end = 8.dp), 105 | ) 106 | } 107 | Text( 108 | text = title, 109 | fontWeight = FontWeight.Bold, 110 | style = MaterialTheme.typography.headlineLarge, 111 | ) 112 | } 113 | Text( 114 | text = AnnotatedString.fromHtml(description), 115 | textAlign = TextAlign.Center, 116 | modifier = Modifier.padding(top = 8.dp) 117 | ) 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /app/src/main/java/com/uravgcode/chooser/ui/theme/Color.kt: -------------------------------------------------------------------------------- 1 | package com.uravgcode.chooser.ui.theme 2 | 3 | import androidx.compose.ui.graphics.Color 4 | 5 | val primaryDark = Color(0xFFC7C6C7) 6 | val onPrimaryDark = Color(0xFF303031) 7 | val primaryContainerDark = Color(0xFF252627) 8 | val onPrimaryContainerDark = Color(0xFF8D8D8E) 9 | 10 | val secondaryDark = Color(0xFFC8C6C6) 11 | val onSecondaryDark = Color(0xFF303031) 12 | val secondaryContainerDark = Color(0xFF474747) 13 | val onSecondaryContainerDark = Color(0xFFB7B5B5) 14 | 15 | val tertiaryDark = Color(0xFFCBC5C7) 16 | val onTertiaryDark = Color(0xFF332F31) 17 | val tertiaryContainerDark = Color(0xFF282527) 18 | val onTertiaryContainerDark = Color(0xFF918C8E) 19 | 20 | val errorDark = Color(0xFFFFB4AB) 21 | val onErrorDark = Color(0xFF690005) 22 | val errorContainerDark = Color(0xFF93000A) 23 | val onErrorContainerDark = Color(0xFFFFDAD6) 24 | 25 | val backgroundDark = Color(0xFF141313) 26 | val onBackgroundDark = Color(0xFFE5E2E1) 27 | 28 | val surfaceDark = Color(0xFF141313) 29 | val onSurfaceDark = Color(0xFFE5E2E1) 30 | val surfaceVariantDark = Color(0xFF444749) 31 | val onSurfaceVariantDark = Color(0xFFC5C7C9) 32 | 33 | val outlineDark = Color(0xFF8F9193) 34 | val outlineVariantDark = Color(0xFF444749) 35 | val scrimDark = Color(0xFF000000) 36 | 37 | val inverseSurfaceDark = Color(0xFFE5E2E1) 38 | val inverseOnSurfaceDark = Color(0xFF313030) 39 | val inversePrimaryDark = Color(0xFF5E5E5F) 40 | 41 | val surfaceDimDark = Color(0xFF141313) 42 | val surfaceBrightDark = Color(0xFF3A3939) 43 | val surfaceContainerLowestDark = Color(0xFF0E0E0E) 44 | val surfaceContainerLowDark = Color(0xFF1C1B1B) 45 | val surfaceContainerDark = Color(0xFF201F1F) 46 | val surfaceContainerHighDark = Color(0xFF2A2A2A) 47 | val surfaceContainerHighestDark = Color(0xFF353434) 48 | -------------------------------------------------------------------------------- /app/src/main/java/com/uravgcode/chooser/ui/theme/Theme.kt: -------------------------------------------------------------------------------- 1 | package com.uravgcode.chooser.ui.theme 2 | 3 | import androidx.compose.material3.MaterialTheme 4 | import androidx.compose.material3.darkColorScheme 5 | import androidx.compose.runtime.Composable 6 | 7 | private val darkScheme = darkColorScheme( 8 | primary = primaryDark, 9 | onPrimary = onPrimaryDark, 10 | primaryContainer = primaryContainerDark, 11 | onPrimaryContainer = onPrimaryContainerDark, 12 | 13 | secondary = secondaryDark, 14 | onSecondary = onSecondaryDark, 15 | secondaryContainer = secondaryContainerDark, 16 | onSecondaryContainer = onSecondaryContainerDark, 17 | 18 | tertiary = tertiaryDark, 19 | onTertiary = onTertiaryDark, 20 | tertiaryContainer = tertiaryContainerDark, 21 | onTertiaryContainer = onTertiaryContainerDark, 22 | 23 | error = errorDark, 24 | onError = onErrorDark, 25 | errorContainer = errorContainerDark, 26 | onErrorContainer = onErrorContainerDark, 27 | 28 | background = backgroundDark, 29 | onBackground = onBackgroundDark, 30 | 31 | surface = surfaceDark, 32 | onSurface = onSurfaceDark, 33 | surfaceVariant = surfaceVariantDark, 34 | onSurfaceVariant = onSurfaceVariantDark, 35 | 36 | outline = outlineDark, 37 | outlineVariant = outlineVariantDark, 38 | scrim = scrimDark, 39 | 40 | inverseSurface = inverseSurfaceDark, 41 | inverseOnSurface = inverseOnSurfaceDark, 42 | inversePrimary = inversePrimaryDark, 43 | 44 | surfaceDim = surfaceDimDark, 45 | surfaceBright = surfaceBrightDark, 46 | surfaceContainerLowest = surfaceContainerLowestDark, 47 | surfaceContainerLow = surfaceContainerLowDark, 48 | surfaceContainer = surfaceContainerDark, 49 | surfaceContainerHigh = surfaceContainerHighDark, 50 | surfaceContainerHighest = surfaceContainerHighestDark, 51 | ) 52 | 53 | @Composable 54 | fun ChooserTheme( 55 | content: @Composable () -> Unit 56 | ) { 57 | MaterialTheme( 58 | colorScheme = darkScheme, 59 | typography = Typography, 60 | content = content 61 | ) 62 | } 63 | -------------------------------------------------------------------------------- /app/src/main/java/com/uravgcode/chooser/ui/theme/Type.kt: -------------------------------------------------------------------------------- 1 | package com.uravgcode.chooser.ui.theme 2 | 3 | import androidx.compose.material3.Typography 4 | 5 | val Typography = Typography() 6 | -------------------------------------------------------------------------------- /app/src/main/res/animator/fade_in_full_1.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/animator/fade_in_full_2.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 11 | 12 | -------------------------------------------------------------------------------- /app/src/main/res/animator/fade_in_full_3.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 11 | 12 | -------------------------------------------------------------------------------- /app/src/main/res/animator/fade_in_full_4.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 11 | 12 | -------------------------------------------------------------------------------- /app/src/main/res/animator/fade_in_growing_1.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 10 | 17 | 18 | -------------------------------------------------------------------------------- /app/src/main/res/animator/fade_in_growing_2.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 11 | 19 | 20 | -------------------------------------------------------------------------------- /app/src/main/res/animator/fade_in_growing_3.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 11 | 19 | 20 | -------------------------------------------------------------------------------- /app/src/main/res/animator/fade_in_growing_4.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 11 | 19 | 20 | -------------------------------------------------------------------------------- /app/src/main/res/animator/fade_in_partial_1.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/animator/fade_in_partial_2.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 11 | 12 | -------------------------------------------------------------------------------- /app/src/main/res/animator/fade_in_partial_3.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 11 | 12 | -------------------------------------------------------------------------------- /app/src/main/res/animator/fade_in_partial_4.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 11 | 12 | -------------------------------------------------------------------------------- /app/src/main/res/animator/fade_in_shrinking.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 10 | 17 | 18 | -------------------------------------------------------------------------------- /app/src/main/res/animator/fade_out_shrinking.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 10 | 17 | 18 | -------------------------------------------------------------------------------- /app/src/main/res/animator/fill_color_white_to_blue.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/animator/fill_color_white_to_orange.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/animator/infinite_rotation.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 11 | 12 | -------------------------------------------------------------------------------- /app/src/main/res/animator/stroke_color_black_to_white.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/animator/stroke_color_white_to_blue.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/animator/stroke_color_white_to_orange.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/button_preview.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 12 | 15 | 18 | 19 | 22 | 25 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/button_preview_animated.xml: -------------------------------------------------------------------------------- 1 | 3 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/chooser_preview.xml: -------------------------------------------------------------------------------- 1 | 6 | 13 | 16 | 20 | 27 | 34 | 35 | 41 | 44 | 48 | 55 | 62 | 63 | 70 | 73 | 77 | 84 | 91 | 92 | 99 | 102 | 106 | 113 | 120 | 121 | 122 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/chooser_preview_animated.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 9 | 12 | 15 | 16 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/group_icon.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 12 | 15 | 18 | 21 | 24 | 25 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/group_preview.xml: -------------------------------------------------------------------------------- 1 | 6 | 12 | 16 | 21 | 29 | 37 | 38 | 43 | 47 | 52 | 60 | 68 | 69 | 75 | 79 | 84 | 92 | 100 | 101 | 107 | 111 | 116 | 124 | 132 | 133 | 134 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/group_preview_animated.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 9 | 12 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_animated.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 6 | 10 | 13 | 19 | 26 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_monochrome.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 12 | 18 | 24 | 25 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/order_icon.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 12 | 15 | 16 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/order_preview.xml: -------------------------------------------------------------------------------- 1 | 6 | 12 | 15 | 19 | 26 | 33 | 34 | 37 | 42 | 46 | 47 | 52 | 55 | 59 | 66 | 73 | 74 | 77 | 82 | 86 | 87 | 93 | 96 | 100 | 107 | 114 | 115 | 118 | 123 | 127 | 128 | 134 | 137 | 141 | 148 | 155 | 156 | 159 | 164 | 168 | 169 | 170 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/order_preview_animated.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 9 | 12 | 15 | 18 | 21 | 24 | 27 | 28 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/single_icon.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 12 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/single_preview.xml: -------------------------------------------------------------------------------- 1 | 6 | 12 | 15 | 16 | 23 | 26 | 30 | 37 | 44 | 45 | 51 | 54 | 58 | 65 | 72 | 73 | 80 | 83 | 87 | 94 | 101 | 102 | 109 | 112 | 116 | 123 | 130 | 131 | 132 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/single_preview_animated.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 9 | 12 | 15 | 16 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/raw/finger_chosen.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UrAvgCode/Chooser/0ba2cbac77c152260c3b909eed64c4c94fc73f38/app/src/main/res/raw/finger_chosen.mp3 -------------------------------------------------------------------------------- /app/src/main/res/raw/finger_down.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UrAvgCode/Chooser/0ba2cbac77c152260c3b909eed64c4c94fc73f38/app/src/main/res/raw/finger_down.mp3 -------------------------------------------------------------------------------- /app/src/main/res/raw/finger_up.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UrAvgCode/Chooser/0ba2cbac77c152260c3b909eed64c4c94fc73f38/app/src/main/res/raw/finger_up.mp3 -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #000 4 | #474747 5 | #fff 6 | #2196f3 7 | #4caf50 8 | #ffeb3b 9 | #ce6d29 10 | #ce2949 11 | 12 | -------------------------------------------------------------------------------- /app/src/main/res/values/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #141414 4 | -------------------------------------------------------------------------------- /app/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 13 | 14 | -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | plugins { 3 | alias(libs.plugins.android.application) apply false 4 | alias(libs.plugins.kotlin.android) apply false 5 | alias(libs.plugins.kotlin.compose) apply false 6 | } 7 | -------------------------------------------------------------------------------- /fastlane/metadata/android/de/changelogs/1.txt: -------------------------------------------------------------------------------- 1 | Erste Veröffentlichung 2 | -------------------------------------------------------------------------------- /fastlane/metadata/android/de/changelogs/10.txt: -------------------------------------------------------------------------------- 1 | Verbesserungen 2 | - Unterstützung für Edge-to-Edge Display 3 | 4 | Entwicklung 5 | - SDK 35 6 | - Code Verbesserungen 7 | - Abhängigkeiten aktualisiert 8 | -------------------------------------------------------------------------------- /fastlane/metadata/android/de/changelogs/11.txt: -------------------------------------------------------------------------------- 1 | Funktionen 2 | - Einstellungsmenü hinzugefügt, zugänglich durch langes Drücken der Modus-Taste: 3 | - Ton und Vibration ein-/ausschalten 4 | - Edge-to-Edge Display ein-/ausschalten 5 | - Kreisgröße und Lebensdauer ändern 6 | 7 | Verbesserungen 8 | - Monochromes Icon hinzugefügt 9 | 10 | Entwicklung 11 | - Umstellung auf Jetpack Compose 12 | - Abhängigkeiten aktualisiert 13 | -------------------------------------------------------------------------------- /fastlane/metadata/android/de/changelogs/12.txt: -------------------------------------------------------------------------------- 1 | Funktionen 2 | - Neue Einstellungen: 3 | - Kreisgröße im Gruppen-Modus unabhängig änderbar 4 | - Einstellungsabschnitte mit Titeln hinzugefügt 5 | 6 | Verbesserungen 7 | - Schwarzer Radius skaliert jetzt mit der Kreisgröße 8 | - Schwebende Zahlen skalieren mit der Kreisgröße 9 | 10 | Entwicklung 11 | - Kotlin-Version aktualisiert 12 | - Abhängigkeiten aktualisiert 13 | -------------------------------------------------------------------------------- /fastlane/metadata/android/de/changelogs/13.txt: -------------------------------------------------------------------------------- 1 | Funktionen 2 | - Einstellungsmenü hinzugefügt, zugänglich durch langes Drücken der Modus-Taste: 3 | - Ton und Vibration ein-/ausschalten 4 | - Edge-to-Edge Display ein-/ausschalten 5 | - Position der Buttons mit Abstand von oben anpassen 6 | - Kreisgröße und Lebensdauer ändern 7 | - Setze alle Einstellungen zurück 8 | 9 | Verbesserungen 10 | - Monochromes Icon hinzugefügt 11 | 12 | Entwicklung 13 | - Umstellung auf Jetpack Compose 14 | - Abhängigkeiten aktualisiert 15 | -------------------------------------------------------------------------------- /fastlane/metadata/android/de/changelogs/14.txt: -------------------------------------------------------------------------------- 1 | Verbesserungen 2 | - verbesserte Einstellungsnamen für Button-Padding 3 | - Übergangsanimationen zwischen Bildschirmen hinzugefügt 4 | - Unterstützung für vorausschauende Zurück-Touch-Geste aktiviert 5 | 6 | Behobene Fehler 7 | - unbeabsichtigte Navigation bei Orientierungsänderung behoben 8 | 9 | Entwicklung 10 | - Umstellung auf Compose Navigation 11 | - Abhängigkeiten aktualisiert 12 | -------------------------------------------------------------------------------- /fastlane/metadata/android/de/changelogs/15.txt: -------------------------------------------------------------------------------- 1 | Funktionen 2 | - Beim ersten Start der App öffnet sich ein Tutorial, das die wichtigsten Funktionen vorstellt 3 | 4 | Entwicklung 5 | - Einstellungen auf DataStore umgestellt 6 | - Abhängigkeiten aktualisiert 7 | -------------------------------------------------------------------------------- /fastlane/metadata/android/de/changelogs/16.txt: -------------------------------------------------------------------------------- 1 | Funktionen 2 | - Auswahlzeit Einstellungen für verschiedene Modi hinzugefügt 3 | - Import/Export von Einstellungen 4 | 5 | Verbesserungen 6 | - Animierter Splash Screen hinzugefügt 7 | 8 | Entwicklung 9 | - Einstellungen-Migration entfernt 10 | - Abhängigkeiten aktualisiert 11 | -------------------------------------------------------------------------------- /fastlane/metadata/android/de/changelogs/2.txt: -------------------------------------------------------------------------------- 1 | Behobene Fehler 2 | - Probleme mit der Bildschirmaktualisierungsrate 3 | - Textausrichtung der Tasten 4 | - Geschwindigkeit der Tastenanimation 5 | - Durchschnittlicher Farbwert von mehr als zwei Farben 6 | 7 | Entwicklung 8 | - sdk 34 9 | - material 1.10 10 | - application version 8.1.2 11 | -------------------------------------------------------------------------------- /fastlane/metadata/android/de/changelogs/3.txt: -------------------------------------------------------------------------------- 1 | Entwicklung 2 | - Code Vereinfachungen 3 | - material 1.11 4 | - com.android.application 8.2.1 5 | -------------------------------------------------------------------------------- /fastlane/metadata/android/de/changelogs/4.txt: -------------------------------------------------------------------------------- 1 | Verbesserungen 2 | - Neues Symbol für den Durchzählmodus 3 | - Geringerer App Speicherplatzverbrauch 4 | 5 | Entwicklung 6 | - Ressourcen verkleinern 7 | - material dependency entfernt 8 | - appcompat dependency entfernt 9 | - dependencies info entfernt 10 | -------------------------------------------------------------------------------- /fastlane/metadata/android/de/changelogs/5.txt: -------------------------------------------------------------------------------- 1 | Funktionen 2 | - Einige Testsounds hinzugefügt 3 | - Die Sounds lassen sich durch langes Drücken der Modustaste ein und ausschalten 4 | 5 | (Die Sounds werden sich höchstwahrscheinlich bis zur fertigen nächsten Version noch ändern) 6 | -------------------------------------------------------------------------------- /fastlane/metadata/android/de/changelogs/6.txt: -------------------------------------------------------------------------------- 1 | Verbesserungen 2 | - Kreise im Durchzählmodus bleiben auf dem Bildschirm 3 | - Kreise im Durchzählmodus zeigen ihre jeweilige Nummer 4 | -------------------------------------------------------------------------------- /fastlane/metadata/android/de/changelogs/7.txt: -------------------------------------------------------------------------------- 1 | Verbesserungen 2 | - Die App startet im Modus, in dem man sie verlassen hat 3 | - Kreise im Durchzählmodus bleiben länger auf dem Bildschirm 4 | - Nummern sind leichter zu lesen 5 | 6 | Behobene Fehler 7 | - Parallele Soundwiedergabe 8 | - Den Bildschirm zu drehen, setzt die App nicht mehr zurück 9 | -------------------------------------------------------------------------------- /fastlane/metadata/android/de/changelogs/8.txt: -------------------------------------------------------------------------------- 1 | Funktionen 2 | - Sounds wurden hinzugefügt 3 | - Die Sounds lassen sich durch langes Drücken der Modustaste ein und ausschalten 4 | - Überarbeitung des Durchzählmodus 5 | - Die App startet im Modus, in dem man sie verlassen hat 6 | 7 | Behobene Fehler 8 | - Den Bildschirm zu drehen, setzt die App nicht mehr zurück 9 | -------------------------------------------------------------------------------- /fastlane/metadata/android/de/changelogs/9.txt: -------------------------------------------------------------------------------- 1 | Entwicklung 2 | - kleinere Code-Verbesserungen 3 | - aktualisierte Abhängigkeiten 4 | -------------------------------------------------------------------------------- /fastlane/metadata/android/de/full_description.txt: -------------------------------------------------------------------------------- 1 | Chooser ist eine Android-App, die dabei hilft, eine zufällige Auswahl unter Freunden oder anderen Gruppen zu treffen. 2 | Egal ob zu entscheiden, wer an der Kasse zahlen soll, wer ein Spiel beginnen soll oder in welchen Teams man spielt. 3 | 4 | Jeder berührt einfach den Bildschirm, um die zufällig ausgewählten Personen zu bestimmen. 5 | Das macht die Entscheidungsfindung nicht nur fair, sondern auch unterhaltsam. 6 | 7 | Funktionen 8 | 9 | - Wähle eine zufällige Person aus 10 | - Wähle mehrere Personen aus 11 | - Teile Personen in Gruppen ein 12 | - Zähle durch eine Gruppe durch 13 | -------------------------------------------------------------------------------- /fastlane/metadata/android/de/short_description.txt: -------------------------------------------------------------------------------- 1 | Wähle zufällige Finger auf dem Bildschirm aus 2 | -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/changelogs/1.txt: -------------------------------------------------------------------------------- 1 | initial release 2 | -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/changelogs/10.txt: -------------------------------------------------------------------------------- 1 | Improvements 2 | - enhanced display cutout handling 3 | - added support for edge-to-edge display without black bars 4 | 5 | Development 6 | - target SDK 35 7 | - minor code improvements 8 | - update dependencies 9 | -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/changelogs/11.txt: -------------------------------------------------------------------------------- 1 | Features 2 | - added settings menu accessible by long pressing the mode button: 3 | - enable/disable sound and vibrations 4 | - enable/disable edge-to-edge display 5 | - change circle size and lifetime 6 | 7 | Improvements 8 | - added a monochrome icon 9 | 10 | Development 11 | - switched to Jetpack Compose 12 | - updated dependencies 13 | -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/changelogs/12.txt: -------------------------------------------------------------------------------- 1 | Features 2 | - new settings: 3 | - change group circle lifetime independently 4 | - added section separators with titles 5 | 6 | Improvements 7 | - black radius now scales with circle size 8 | - floating numbers scale with circle size 9 | 10 | Development 11 | - updated kotlin version 12 | - updated dependencies 13 | -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/changelogs/13.txt: -------------------------------------------------------------------------------- 1 | Features 2 | - added settings menu accessible by long pressing the mode button: 3 | - enable/disable sound and vibrations 4 | - enable/disable edge-to-edge display 5 | - change button position with top padding 6 | - change circle size and lifetime 7 | - reset all settings 8 | 9 | Improvements 10 | - added a monochrome icon 11 | 12 | Development 13 | - switched to Jetpack Compose 14 | - updated dependencies 15 | -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/changelogs/14.txt: -------------------------------------------------------------------------------- 1 | Improvements 2 | - improved settings names for button padding 3 | - added transition animations between screens 4 | - enabled predictive back gesture support 5 | 6 | Fixed 7 | - fixed unintended navigation on orientation change 8 | 9 | Development 10 | - migrated to compose navigation 11 | - updated dependencies 12 | -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/changelogs/15.txt: -------------------------------------------------------------------------------- 1 | Features 2 | - added a tutorial that automatically launches on first app start to introduce core features 3 | 4 | Development 5 | - migrated settings to datastore 6 | - updated dependencies 7 | -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/changelogs/16.txt: -------------------------------------------------------------------------------- 1 | Features 2 | - added selection delay settings for different modes 3 | - import/export settings 4 | 5 | Improvements 6 | - added animated splash screen 7 | 8 | Development 9 | - removed settings migration 10 | - updated dependencies 11 | -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/changelogs/2.txt: -------------------------------------------------------------------------------- 1 | Fixed 2 | - screen refresh rate problems 3 | - button text alignment 4 | - button animation speed 5 | - average color of more than two colors 6 | 7 | Development 8 | - sdk 34 9 | - material 1.10 10 | - application version 8.1.2 11 | -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/changelogs/3.txt: -------------------------------------------------------------------------------- 1 | Development 2 | - code cleanup 3 | - material 1.11 4 | - com.android.application 8.2.1 5 | -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/changelogs/4.txt: -------------------------------------------------------------------------------- 1 | Improved 2 | - new order icon 3 | - smaller app size 4 | 5 | Development 6 | - shrink resources 7 | - removed material dependency 8 | - removed appcompat dependency 9 | - removed dependencies info 10 | -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/changelogs/5.txt: -------------------------------------------------------------------------------- 1 | Features 2 | - added some test sounds for proof of concept 3 | - turn sounds on and off by long pressing the mode button 4 | 5 | (the sounds will most likely change until full release) 6 | -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/changelogs/6.txt: -------------------------------------------------------------------------------- 1 | Improved 2 | - circles in order mode no longer disappear 3 | - circles in order mode display their respective number 4 | -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/changelogs/7.txt: -------------------------------------------------------------------------------- 1 | Improved 2 | - return to the app as you left it 3 | - circles in order mode stay on screen for longer 4 | - numbers are easier to read 5 | 6 | Fixed 7 | - parallel sound playback 8 | - turning the screen doesn't reset the app 9 | -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/changelogs/8.txt: -------------------------------------------------------------------------------- 1 | Features 2 | - added sounds 3 | - turn sounds on and off by long pressing the mode button 4 | - overhauled order mode 5 | - return to the app as you left it 6 | 7 | Fixed 8 | - turning the screen doesn't reset the app 9 | -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/changelogs/9.txt: -------------------------------------------------------------------------------- 1 | Development 2 | - minor code improvements 3 | - update dependencies 4 | -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/full_description.txt: -------------------------------------------------------------------------------- 1 | Chooser is an Android app designed to help you make random selections among friends or groups. 2 | Whether you're deciding who should pay at the checkout, who should start a game, or which teams to play in, Chooser has you covered. 3 | 4 | With Chooser, everyone simply touches the screen to select random fingers, ensuring fair and unbiased decisions every time. 5 | It adds an exciting twist to decision-making, making it not only fair but also entertaining. 6 | 7 | Features 8 | 9 | - Choose a random person from the group 10 | - Select multiple people at once 11 | - Divide people into customizable groups 12 | - Easily count through a group 13 | -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/featureGraphic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UrAvgCode/Chooser/0ba2cbac77c152260c3b909eed64c4c94fc73f38/fastlane/metadata/android/en-US/images/featureGraphic.png -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UrAvgCode/Chooser/0ba2cbac77c152260c3b909eed64c4c94fc73f38/fastlane/metadata/android/en-US/images/icon.png -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/phoneScreenshots/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UrAvgCode/Chooser/0ba2cbac77c152260c3b909eed64c4c94fc73f38/fastlane/metadata/android/en-US/images/phoneScreenshots/1.png -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/phoneScreenshots/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UrAvgCode/Chooser/0ba2cbac77c152260c3b909eed64c4c94fc73f38/fastlane/metadata/android/en-US/images/phoneScreenshots/2.png -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/phoneScreenshots/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UrAvgCode/Chooser/0ba2cbac77c152260c3b909eed64c4c94fc73f38/fastlane/metadata/android/en-US/images/phoneScreenshots/3.png -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/phoneScreenshots/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UrAvgCode/Chooser/0ba2cbac77c152260c3b909eed64c4c94fc73f38/fastlane/metadata/android/en-US/images/phoneScreenshots/4.png -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/phoneScreenshots/5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UrAvgCode/Chooser/0ba2cbac77c152260c3b909eed64c4c94fc73f38/fastlane/metadata/android/en-US/images/phoneScreenshots/5.png -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/short_description.txt: -------------------------------------------------------------------------------- 1 | Select random fingers on the screen 2 | -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/title.txt: -------------------------------------------------------------------------------- 1 | Chooser 2 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | # IDE (e.g. Android Studio) users: 3 | # Gradle settings configured through the IDE *will override* 4 | # any settings specified in this file. 5 | # For more details on how to configure your build environment visit 6 | # http://www.gradle.org/docs/current/userguide/build_environment.html 7 | # Specifies the JVM arguments used for the daemon process. 8 | # The setting is particularly useful for tweaking memory settings. 9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 10 | # When configured, Gradle will run in incubating parallel mode. 11 | # This option should only be used with decoupled projects. For more details, visit 12 | # https://developer.android.com/r/tools/gradle-multi-project-decoupled-projects 13 | # org.gradle.parallel=true 14 | # AndroidX package structure to make it clearer which packages are bundled with the 15 | # Android operating system, and which are packaged with your app's APK 16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 17 | android.useAndroidX=true 18 | # Kotlin code style for this project: "official" or "obsolete": 19 | kotlin.code.style=official 20 | # Enables namespacing of each library's R class so that its R class includes only the 21 | # resources declared in the library itself and none from the library's dependencies, 22 | # thereby reducing the size of the R class for that library 23 | android.nonTransitiveRClass=true -------------------------------------------------------------------------------- /gradle/libs.versions.toml: -------------------------------------------------------------------------------- 1 | [versions] 2 | agp = "8.10.0" 3 | kotlin = "2.1.10" 4 | coreKtx = "1.16.0" 5 | lifecycleRuntimeKtx = "2.9.0" 6 | 7 | datastore = "1.1.6" 8 | kotlinSerialization = "1.7.3" 9 | 10 | activityCompose = "1.10.1" 11 | composeBom = "2025.05.00" 12 | navigationCompose = "2.9.0" 13 | 14 | splashscreen = "1.0.1" 15 | 16 | 17 | [libraries] 18 | androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } 19 | androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" } 20 | 21 | androidx-datastore = { module = "androidx.datastore:datastore", version.ref = "datastore" } 22 | kotlinx-serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "kotlinSerialization" } 23 | 24 | androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" } 25 | androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" } 26 | 27 | androidx-material3 = { group = "androidx.compose.material3", name = "material3" } 28 | androidx-material-icons-extended = { module = "androidx.compose.material:material-icons-extended" } 29 | 30 | androidx-ui = { group = "androidx.compose.ui", name = "ui" } 31 | androidx-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" } 32 | androidx-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" } 33 | androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" } 34 | androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" } 35 | 36 | androidx-animation-graphics = { module = "androidx.compose.animation:animation-graphics" } 37 | 38 | androidx-navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version.ref = "navigationCompose" } 39 | 40 | androidx-core-splashscreen = { module = "androidx.core:core-splashscreen", version.ref = "splashscreen" } 41 | 42 | 43 | [plugins] 44 | android-application = { id = "com.android.application", version.ref = "agp" } 45 | kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } 46 | kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } 47 | kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } 48 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UrAvgCode/Chooser/0ba2cbac77c152260c3b909eed64c4c94fc73f38/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionSha256Sum=61ad310d3c7d3e5da131b76bbf22b5a4c0786e9d892dae8c1658d4b484de3caa 4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-bin.zip 5 | networkTimeout=10000 6 | validateDistributionUrl=true 7 | zipStoreBase=GRADLE_USER_HOME 8 | zipStorePath=wrapper/dists 9 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | # SPDX-License-Identifier: Apache-2.0 19 | # 20 | 21 | ############################################################################## 22 | # 23 | # Gradle start up script for POSIX generated by Gradle. 24 | # 25 | # Important for running: 26 | # 27 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 28 | # noncompliant, but you have some other compliant shell such as ksh or 29 | # bash, then to run this script, type that shell name before the whole 30 | # command line, like: 31 | # 32 | # ksh Gradle 33 | # 34 | # Busybox and similar reduced shells will NOT work, because this script 35 | # requires all of these POSIX shell features: 36 | # * functions; 37 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 38 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 39 | # * compound commands having a testable exit status, especially «case»; 40 | # * various built-in commands including «command», «set», and «ulimit». 41 | # 42 | # Important for patching: 43 | # 44 | # (2) This script targets any POSIX shell, so it avoids extensions provided 45 | # by Bash, Ksh, etc; in particular arrays are avoided. 46 | # 47 | # The "traditional" practice of packing multiple parameters into a 48 | # space-separated string is a well documented source of bugs and security 49 | # problems, so this is (mostly) avoided, by progressively accumulating 50 | # options in "$@", and eventually passing that to Java. 51 | # 52 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 53 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 54 | # see the in-line comments for details. 55 | # 56 | # There are tweaks for specific operating systems such as AIX, CygWin, 57 | # Darwin, MinGW, and NonStop. 58 | # 59 | # (3) This script is generated from the Groovy template 60 | # https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 61 | # within the Gradle project. 62 | # 63 | # You can find Gradle at https://github.com/gradle/gradle/. 64 | # 65 | ############################################################################## 66 | 67 | # Attempt to set APP_HOME 68 | 69 | # Resolve links: $0 may be a link 70 | app_path=$0 71 | 72 | # Need this for daisy-chained symlinks. 73 | while 74 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 75 | [ -h "$app_path" ] 76 | do 77 | ls=$( ls -ld "$app_path" ) 78 | link=${ls#*' -> '} 79 | case $link in #( 80 | /*) app_path=$link ;; #( 81 | *) app_path=$APP_HOME$link ;; 82 | esac 83 | done 84 | 85 | # This is normally unused 86 | # shellcheck disable=SC2034 87 | APP_BASE_NAME=${0##*/} 88 | # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) 89 | APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit 90 | 91 | # Use the maximum available, or set MAX_FD != -1 to use that value. 92 | MAX_FD=maximum 93 | 94 | warn () { 95 | echo "$*" 96 | } >&2 97 | 98 | die () { 99 | echo 100 | echo "$*" 101 | echo 102 | exit 1 103 | } >&2 104 | 105 | # OS specific support (must be 'true' or 'false'). 106 | cygwin=false 107 | msys=false 108 | darwin=false 109 | nonstop=false 110 | case "$( uname )" in #( 111 | CYGWIN* ) cygwin=true ;; #( 112 | Darwin* ) darwin=true ;; #( 113 | MSYS* | MINGW* ) msys=true ;; #( 114 | NONSTOP* ) nonstop=true ;; 115 | esac 116 | 117 | CLASSPATH="\\\"\\\"" 118 | 119 | 120 | # Determine the Java command to use to start the JVM. 121 | if [ -n "$JAVA_HOME" ] ; then 122 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 123 | # IBM's JDK on AIX uses strange locations for the executables 124 | JAVACMD=$JAVA_HOME/jre/sh/java 125 | else 126 | JAVACMD=$JAVA_HOME/bin/java 127 | fi 128 | if [ ! -x "$JAVACMD" ] ; then 129 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 130 | 131 | Please set the JAVA_HOME variable in your environment to match the 132 | location of your Java installation." 133 | fi 134 | else 135 | JAVACMD=java 136 | if ! command -v java >/dev/null 2>&1 137 | then 138 | die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 139 | 140 | Please set the JAVA_HOME variable in your environment to match the 141 | location of your Java installation." 142 | fi 143 | fi 144 | 145 | # Increase the maximum file descriptors if we can. 146 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 147 | case $MAX_FD in #( 148 | max*) 149 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. 150 | # shellcheck disable=SC2039,SC3045 151 | MAX_FD=$( ulimit -H -n ) || 152 | warn "Could not query maximum file descriptor limit" 153 | esac 154 | case $MAX_FD in #( 155 | '' | soft) :;; #( 156 | *) 157 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. 158 | # shellcheck disable=SC2039,SC3045 159 | ulimit -n "$MAX_FD" || 160 | warn "Could not set maximum file descriptor limit to $MAX_FD" 161 | esac 162 | fi 163 | 164 | # Collect all arguments for the java command, stacking in reverse order: 165 | # * args from the command line 166 | # * the main class name 167 | # * -classpath 168 | # * -D...appname settings 169 | # * --module-path (only if needed) 170 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 171 | 172 | # For Cygwin or MSYS, switch paths to Windows format before running java 173 | if "$cygwin" || "$msys" ; then 174 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 175 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 176 | 177 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 178 | 179 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 180 | for arg do 181 | if 182 | case $arg in #( 183 | -*) false ;; # don't mess with options #( 184 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 185 | [ -e "$t" ] ;; #( 186 | *) false ;; 187 | esac 188 | then 189 | arg=$( cygpath --path --ignore --mixed "$arg" ) 190 | fi 191 | # Roll the args list around exactly as many times as the number of 192 | # args, so each arg winds up back in the position where it started, but 193 | # possibly modified. 194 | # 195 | # NB: a `for` loop captures its iteration list before it begins, so 196 | # changing the positional parameters here affects neither the number of 197 | # iterations, nor the values presented in `arg`. 198 | shift # remove old arg 199 | set -- "$@" "$arg" # push replacement arg 200 | done 201 | fi 202 | 203 | 204 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 205 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 206 | 207 | # Collect all arguments for the java command: 208 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, 209 | # and any embedded shellness will be escaped. 210 | # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be 211 | # treated as '${Hostname}' itself on the command line. 212 | 213 | set -- \ 214 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 215 | -classpath "$CLASSPATH" \ 216 | -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ 217 | "$@" 218 | 219 | # Stop when "xargs" is not available. 220 | if ! command -v xargs >/dev/null 2>&1 221 | then 222 | die "xargs is not available" 223 | fi 224 | 225 | # Use "xargs" to parse quoted args. 226 | # 227 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 228 | # 229 | # In Bash we could simply go: 230 | # 231 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 232 | # set -- "${ARGS[@]}" "$@" 233 | # 234 | # but POSIX shell has neither arrays nor command substitution, so instead we 235 | # post-process each arg (as a line of input to sed) to backslash-escape any 236 | # character that might be a shell metacharacter, then use eval to reverse 237 | # that process (while maintaining the separation between arguments), and wrap 238 | # the whole thing up as a single "set" statement. 239 | # 240 | # This will of course break if any of these variables contains a newline or 241 | # an unmatched quote. 242 | # 243 | 244 | eval "set -- $( 245 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 246 | xargs -n1 | 247 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 248 | tr '\n' ' ' 249 | )" '"$@"' 250 | 251 | exec "$JAVACMD" "$@" 252 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | @rem SPDX-License-Identifier: Apache-2.0 17 | @rem 18 | 19 | @if "%DEBUG%"=="" @echo off 20 | @rem ########################################################################## 21 | @rem 22 | @rem Gradle startup script for Windows 23 | @rem 24 | @rem ########################################################################## 25 | 26 | @rem Set local scope for the variables with windows NT shell 27 | if "%OS%"=="Windows_NT" setlocal 28 | 29 | set DIRNAME=%~dp0 30 | if "%DIRNAME%"=="" set DIRNAME=. 31 | @rem This is normally unused 32 | set APP_BASE_NAME=%~n0 33 | set APP_HOME=%DIRNAME% 34 | 35 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 36 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 37 | 38 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 39 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 40 | 41 | @rem Find java.exe 42 | if defined JAVA_HOME goto findJavaFromJavaHome 43 | 44 | set JAVA_EXE=java.exe 45 | %JAVA_EXE% -version >NUL 2>&1 46 | if %ERRORLEVEL% equ 0 goto execute 47 | 48 | echo. 1>&2 49 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 50 | echo. 1>&2 51 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 52 | echo location of your Java installation. 1>&2 53 | 54 | goto fail 55 | 56 | :findJavaFromJavaHome 57 | set JAVA_HOME=%JAVA_HOME:"=% 58 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 59 | 60 | if exist "%JAVA_EXE%" goto execute 61 | 62 | echo. 1>&2 63 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 64 | echo. 1>&2 65 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 66 | echo location of your Java installation. 1>&2 67 | 68 | goto fail 69 | 70 | :execute 71 | @rem Setup the command line 72 | 73 | set CLASSPATH= 74 | 75 | 76 | @rem Execute Gradle 77 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* 78 | 79 | :end 80 | @rem End local scope for the variables with windows NT shell 81 | if %ERRORLEVEL% equ 0 goto mainEnd 82 | 83 | :fail 84 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 85 | rem the _cmd.exe /c_ return code! 86 | set EXIT_CODE=%ERRORLEVEL% 87 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 88 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 89 | exit /b %EXIT_CODE% 90 | 91 | :mainEnd 92 | if "%OS%"=="Windows_NT" endlocal 93 | 94 | :omega 95 | -------------------------------------------------------------------------------- /readme/chooser-icon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /readme/get-it-on-github.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UrAvgCode/Chooser/0ba2cbac77c152260c3b909eed64c4c94fc73f38/readme/get-it-on-github.png -------------------------------------------------------------------------------- /readme/group-mode.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UrAvgCode/Chooser/0ba2cbac77c152260c3b909eed64c4c94fc73f38/readme/group-mode.gif -------------------------------------------------------------------------------- /readme/order-mode.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UrAvgCode/Chooser/0ba2cbac77c152260c3b909eed64c4c94fc73f38/readme/order-mode.gif -------------------------------------------------------------------------------- /readme/single-mode.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UrAvgCode/Chooser/0ba2cbac77c152260c3b909eed64c4c94fc73f38/readme/single-mode.gif -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | google { 4 | content { 5 | includeGroupByRegex("com\\.android.*") 6 | includeGroupByRegex("com\\.google.*") 7 | includeGroupByRegex("androidx.*") 8 | } 9 | } 10 | mavenCentral() 11 | gradlePluginPortal() 12 | } 13 | } 14 | 15 | @Suppress("UnstableApiUsage") 16 | dependencyResolutionManagement { 17 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) 18 | repositories { 19 | google() 20 | mavenCentral() 21 | } 22 | } 23 | 24 | rootProject.name = "Chooser" 25 | include(":app") 26 | --------------------------------------------------------------------------------