mirror of
https://github.com/iSoron/uhabits.git
synced 2025-12-06 09:08:52 -06:00
Compare commits
23 Commits
770d1293dc
...
hiqua-patc
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e65c7de086 | ||
|
06090e238a
|
|||
|
e48452f724
|
|||
|
|
936986e110 | ||
|
|
4b3910aea8 | ||
|
|
1280e798d2
|
||
|
|
b09306e793
|
||
|
|
e30636a447
|
||
|
|
ad8738180c
|
||
|
|
08410c59d0
|
||
|
|
ab86cee70b | ||
|
|
3a0603605b
|
||
|
|
6a78b4d853 | ||
|
|
fe43b1435d | ||
|
|
12503b8a6d | ||
|
|
ef7f78bff0 | ||
|
|
53c208ded5 | ||
|
|
1bdc83e92f | ||
|
|
680c1cdc76 | ||
|
|
80916bac50 | ||
|
|
a5e3e9b3cf | ||
|
248ba50a8e
|
|||
|
|
45a82b3c2d
|
2
.github/workflows/main.yml
vendored
2
.github/workflows/main.yml
vendored
@@ -22,7 +22,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Upload artifacts
|
- name: Upload artifacts
|
||||||
if: always()
|
if: always()
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: build
|
name: build
|
||||||
path: |
|
path: |
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
plugins {
|
plugins {
|
||||||
val kotlinVersion = "1.9.21"
|
val kotlinVersion = "1.9.22"
|
||||||
id("com.android.application") version "8.1.4" apply (false)
|
id("com.android.application") version "8.1.4" apply (false)
|
||||||
id("org.jetbrains.kotlin.android") version kotlinVersion apply (false)
|
id("org.jetbrains.kotlin.android") version kotlinVersion apply (false)
|
||||||
id("org.jetbrains.kotlin.kapt") version kotlinVersion apply (false)
|
id("org.jetbrains.kotlin.kapt") version kotlinVersion apply (false)
|
||||||
|
|||||||
@@ -94,8 +94,8 @@ android {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
val daggerVersion = "2.48.1"
|
val daggerVersion = "2.51.1"
|
||||||
val kotlinVersion = "1.9.21"
|
val kotlinVersion = "1.9.22"
|
||||||
val kxCoroutinesVersion = "1.7.3"
|
val kxCoroutinesVersion = "1.7.3"
|
||||||
val ktorVersion = "1.6.8"
|
val ktorVersion = "1.6.8"
|
||||||
val espressoVersion = "3.5.1"
|
val espressoVersion = "3.5.1"
|
||||||
@@ -106,17 +106,17 @@ dependencies {
|
|||||||
androidTestImplementation("com.linkedin.dexmaker:dexmaker-mockito:2.28.3")
|
androidTestImplementation("com.linkedin.dexmaker:dexmaker-mockito:2.28.3")
|
||||||
androidTestImplementation("io.ktor:ktor-client-mock:$ktorVersion")
|
androidTestImplementation("io.ktor:ktor-client-mock:$ktorVersion")
|
||||||
androidTestImplementation("io.ktor:ktor-jackson:$ktorVersion")
|
androidTestImplementation("io.ktor:ktor-jackson:$ktorVersion")
|
||||||
androidTestImplementation("androidx.annotation:annotation:1.7.0")
|
androidTestImplementation("androidx.annotation:annotation:1.7.1")
|
||||||
androidTestImplementation("androidx.test.ext:junit:1.1.5")
|
androidTestImplementation("androidx.test.ext:junit:1.1.5")
|
||||||
androidTestImplementation("androidx.test.uiautomator:uiautomator:2.2.0")
|
androidTestImplementation("androidx.test.uiautomator:uiautomator:2.2.0")
|
||||||
androidTestImplementation("androidx.test:rules:1.5.0")
|
androidTestImplementation("androidx.test:rules:1.5.0")
|
||||||
androidTestImplementation("org.mockito.kotlin:mockito-kotlin:5.1.0")
|
androidTestImplementation("org.mockito.kotlin:mockito-kotlin:5.2.1")
|
||||||
compileOnly("javax.annotation:jsr250-api:1.0")
|
compileOnly("javax.annotation:jsr250-api:1.0")
|
||||||
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.0.4")
|
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.0.4")
|
||||||
implementation("com.github.AppIntro:AppIntro:6.3.1")
|
implementation("com.github.AppIntro:AppIntro:6.3.1")
|
||||||
implementation("com.google.code.findbugs:jsr305:3.0.2")
|
implementation("com.google.code.findbugs:jsr305:3.0.2")
|
||||||
implementation("com.google.dagger:dagger:$daggerVersion")
|
implementation("com.google.dagger:dagger:$daggerVersion")
|
||||||
implementation("com.google.guava:guava:32.1.3-android")
|
implementation("com.google.guava:guava:33.1.0-android")
|
||||||
implementation("io.ktor:ktor-client-android:$ktorVersion")
|
implementation("io.ktor:ktor-client-android:$ktorVersion")
|
||||||
implementation("io.ktor:ktor-client-core:$ktorVersion")
|
implementation("io.ktor:ktor-client-core:$ktorVersion")
|
||||||
implementation("io.ktor:ktor-client-jackson:$ktorVersion")
|
implementation("io.ktor:ktor-client-jackson:$ktorVersion")
|
||||||
@@ -127,14 +127,15 @@ dependencies {
|
|||||||
implementation("androidx.appcompat:appcompat:1.6.1")
|
implementation("androidx.appcompat:appcompat:1.6.1")
|
||||||
implementation("androidx.legacy:legacy-preference-v14:1.0.0")
|
implementation("androidx.legacy:legacy-preference-v14:1.0.0")
|
||||||
implementation("androidx.legacy:legacy-support-v4:1.0.0")
|
implementation("androidx.legacy:legacy-support-v4:1.0.0")
|
||||||
implementation("com.google.android.material:material:1.10.0")
|
implementation("com.google.android.material:material:1.11.0")
|
||||||
implementation("com.opencsv:opencsv:5.9")
|
implementation("com.opencsv:opencsv:5.9")
|
||||||
|
implementation("nl.dionsegijn:konfetti-xml:2.0.2")
|
||||||
implementation(project(":uhabits-core"))
|
implementation(project(":uhabits-core"))
|
||||||
kapt("com.google.dagger:dagger-compiler:$daggerVersion")
|
kapt("com.google.dagger:dagger-compiler:$daggerVersion")
|
||||||
kaptAndroidTest("com.google.dagger:dagger-compiler:$daggerVersion")
|
kaptAndroidTest("com.google.dagger:dagger-compiler:$daggerVersion")
|
||||||
testImplementation("com.google.dagger:dagger:$daggerVersion")
|
testImplementation("com.google.dagger:dagger:$daggerVersion")
|
||||||
testImplementation("junit:junit:4.13.2")
|
testImplementation("junit:junit:4.13.2")
|
||||||
testImplementation("org.mockito.kotlin:mockito-kotlin:5.1.0")
|
testImplementation("org.mockito.kotlin:mockito-kotlin:5.2.1")
|
||||||
}
|
}
|
||||||
|
|
||||||
kapt {
|
kapt {
|
||||||
|
|||||||
@@ -132,7 +132,7 @@ object CommonSteps : BaseUserInterfaceTest() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Throws(Exception::class)
|
@Throws(Exception::class)
|
||||||
fun verifyOpensWebsite(url: String?) {
|
fun verifyOpensWebsite(url: String) {
|
||||||
var browserPkg = "org.chromium.webview_shell"
|
var browserPkg = "org.chromium.webview_shell"
|
||||||
if (SDK_INT <= Build.VERSION_CODES.M) {
|
if (SDK_INT <= Build.VERSION_CODES.M) {
|
||||||
browserPkg = "com.android.browser"
|
browserPkg = "com.android.browser"
|
||||||
|
|||||||
@@ -33,17 +33,19 @@ import org.isoron.uhabits.core.models.Entry.Companion.UNKNOWN
|
|||||||
import org.isoron.uhabits.core.models.Entry.Companion.YES_MANUAL
|
import org.isoron.uhabits.core.models.Entry.Companion.YES_MANUAL
|
||||||
import org.isoron.uhabits.databinding.CheckmarkPopupBinding
|
import org.isoron.uhabits.databinding.CheckmarkPopupBinding
|
||||||
import org.isoron.uhabits.utils.InterfaceUtils.getFontAwesome
|
import org.isoron.uhabits.utils.InterfaceUtils.getFontAwesome
|
||||||
|
import org.isoron.uhabits.utils.getCenter
|
||||||
import org.isoron.uhabits.utils.sres
|
import org.isoron.uhabits.utils.sres
|
||||||
|
|
||||||
class CheckmarkDialog : AppCompatDialogFragment() {
|
class CheckmarkDialog : AppCompatDialogFragment() {
|
||||||
var onToggle: (Int, String) -> Unit = { _, _ -> }
|
var onToggle: (Int, String, Float, Float) -> Unit = { _, _, _, _ -> }
|
||||||
|
|
||||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||||
val appComponent = (requireActivity().application as HabitsApplication).component
|
val appComponent = (requireActivity().application as HabitsApplication).component
|
||||||
val prefs = appComponent.preferences
|
val prefs = appComponent.preferences
|
||||||
val view = CheckmarkPopupBinding.inflate(LayoutInflater.from(context))
|
val view = CheckmarkPopupBinding.inflate(LayoutInflater.from(context))
|
||||||
|
val color = requireArguments().getInt("color")
|
||||||
arrayOf(view.yesBtn, view.skipBtn).forEach {
|
arrayOf(view.yesBtn, view.skipBtn).forEach {
|
||||||
it.setTextColor(requireArguments().getInt("color"))
|
it.setTextColor(color)
|
||||||
}
|
}
|
||||||
arrayOf(view.noBtn, view.unknownBtn).forEach {
|
arrayOf(view.noBtn, view.unknownBtn).forEach {
|
||||||
it.setTextColor(view.root.sres.getColor(R.attr.contrast60))
|
it.setTextColor(view.root.sres.getColor(R.attr.contrast60))
|
||||||
@@ -62,7 +64,8 @@ class CheckmarkDialog : AppCompatDialogFragment() {
|
|||||||
}
|
}
|
||||||
fun onClick(v: Int) {
|
fun onClick(v: Int) {
|
||||||
val notes = view.notes.text.toString().trim()
|
val notes = view.notes.text.toString().trim()
|
||||||
onToggle(v, notes)
|
val location = view.yesBtn.getCenter()
|
||||||
|
onToggle(v, notes, location.x, location.y)
|
||||||
requireDialog().dismiss()
|
requireDialog().dismiss()
|
||||||
}
|
}
|
||||||
view.yesBtn.setOnClickListener { onClick(YES_MANUAL) }
|
view.yesBtn.setOnClickListener { onClick(YES_MANUAL) }
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import org.isoron.uhabits.R
|
|||||||
import org.isoron.uhabits.core.models.Entry
|
import org.isoron.uhabits.core.models.Entry
|
||||||
import org.isoron.uhabits.databinding.CheckmarkPopupBinding
|
import org.isoron.uhabits.databinding.CheckmarkPopupBinding
|
||||||
import org.isoron.uhabits.utils.InterfaceUtils
|
import org.isoron.uhabits.utils.InterfaceUtils
|
||||||
|
import org.isoron.uhabits.utils.getCenter
|
||||||
import org.isoron.uhabits.utils.requestFocusWithKeyboard
|
import org.isoron.uhabits.utils.requestFocusWithKeyboard
|
||||||
import org.isoron.uhabits.utils.sres
|
import org.isoron.uhabits.utils.sres
|
||||||
import java.text.DecimalFormat
|
import java.text.DecimalFormat
|
||||||
@@ -24,7 +25,7 @@ import java.text.ParseException
|
|||||||
|
|
||||||
class NumberDialog : AppCompatDialogFragment() {
|
class NumberDialog : AppCompatDialogFragment() {
|
||||||
|
|
||||||
var onToggle: (Double, String) -> Unit = { _, _ -> }
|
var onToggle: (Double, String, Float, Float) -> Unit = { _, _, _, _ -> }
|
||||||
var onDismiss: () -> Unit = {}
|
var onDismiss: () -> Unit = {}
|
||||||
|
|
||||||
private var originalNotes: String = ""
|
private var originalNotes: String = ""
|
||||||
@@ -104,12 +105,17 @@ class NumberDialog : AppCompatDialogFragment() {
|
|||||||
try {
|
try {
|
||||||
val numberFormat = NumberFormat.getInstance()
|
val numberFormat = NumberFormat.getInstance()
|
||||||
val valueStr = view.value.text.toString()
|
val valueStr = view.value.text.toString()
|
||||||
value = numberFormat.parse(valueStr)!!.toDouble()
|
value = if (valueStr.isNotEmpty()) {
|
||||||
|
numberFormat.parse(valueStr)!!.toDouble()
|
||||||
|
} else {
|
||||||
|
Entry.UNKNOWN.toDouble() / 1000
|
||||||
|
}
|
||||||
} catch (e: ParseException) {
|
} catch (e: ParseException) {
|
||||||
// NOP
|
// NOP
|
||||||
}
|
}
|
||||||
val notes = view.notes.text.toString()
|
val notes = view.notes.text.toString()
|
||||||
onToggle(value, notes)
|
val location = view.saveBtn.getCenter()
|
||||||
|
onToggle(value, notes, location.x, location.y)
|
||||||
requireDialog().dismiss()
|
requireDialog().dismiss()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -58,6 +58,7 @@ class RingView : View {
|
|||||||
private var em = 0f
|
private var em = 0f
|
||||||
private var text: String?
|
private var text: String?
|
||||||
private var textSize: Float
|
private var textSize: Float
|
||||||
|
private var isStrokedTextEnabled: Boolean = false
|
||||||
private var enableFontAwesome = false
|
private var enableFontAwesome = false
|
||||||
private var internalDrawingCache: Bitmap? = null
|
private var internalDrawingCache: Bitmap? = null
|
||||||
private var cacheCanvas: Canvas? = null
|
private var cacheCanvas: Canvas? = null
|
||||||
@@ -131,6 +132,10 @@ class RingView : View {
|
|||||||
invalidate()
|
invalidate()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun setIsStrokedTextEnabled(isStroked: Boolean) {
|
||||||
|
this.isStrokedTextEnabled = isStroked
|
||||||
|
}
|
||||||
|
|
||||||
override fun onDraw(canvas: Canvas) {
|
override fun onDraw(canvas: Canvas) {
|
||||||
super.onDraw(canvas)
|
super.onDraw(canvas)
|
||||||
val activeCanvas: Canvas?
|
val activeCanvas: Canvas?
|
||||||
@@ -159,6 +164,12 @@ class RingView : View {
|
|||||||
pRing!!.xfermode = null
|
pRing!!.xfermode = null
|
||||||
pRing!!.color = color
|
pRing!!.color = color
|
||||||
pRing!!.textSize = textSize
|
pRing!!.textSize = textSize
|
||||||
|
|
||||||
|
if (isStrokedTextEnabled) {
|
||||||
|
pRing!!.style = Paint.Style.STROKE
|
||||||
|
pRing!!.strokeWidth = textSize / 15f
|
||||||
|
}
|
||||||
|
|
||||||
if (enableFontAwesome) pRing!!.typeface = getFontAwesome(context)
|
if (enableFontAwesome) pRing!!.typeface = getFontAwesome(context)
|
||||||
activeCanvas.drawText(
|
activeCanvas.drawText(
|
||||||
text!!,
|
text!!,
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import android.content.Context
|
|||||||
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
|
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
|
||||||
import android.widget.FrameLayout
|
import android.widget.FrameLayout
|
||||||
import android.widget.RelativeLayout
|
import android.widget.RelativeLayout
|
||||||
|
import nl.dionsegijn.konfetti.xml.KonfettiView
|
||||||
import org.isoron.uhabits.R
|
import org.isoron.uhabits.R
|
||||||
import org.isoron.uhabits.activities.common.views.ScrollableChart
|
import org.isoron.uhabits.activities.common.views.ScrollableChart
|
||||||
import org.isoron.uhabits.activities.common.views.TaskProgressBar
|
import org.isoron.uhabits.activities.common.views.TaskProgressBar
|
||||||
@@ -69,6 +70,9 @@ class ListHabitsRootView @Inject constructor(
|
|||||||
val listView: HabitCardListView = habitCardListViewFactory.create()
|
val listView: HabitCardListView = habitCardListViewFactory.create()
|
||||||
val llEmpty = EmptyListView(context)
|
val llEmpty = EmptyListView(context)
|
||||||
val tbar = buildToolbar()
|
val tbar = buildToolbar()
|
||||||
|
val konfettiView = KonfettiView(context).apply {
|
||||||
|
translationZ = 10f
|
||||||
|
}
|
||||||
val progressBar = TaskProgressBar(context, runner)
|
val progressBar = TaskProgressBar(context, runner)
|
||||||
val hintView: HintView
|
val hintView: HintView
|
||||||
val header = HeaderView(context, preferences, midnightTimer)
|
val header = HeaderView(context, preferences, midnightTimer)
|
||||||
@@ -80,6 +84,7 @@ class ListHabitsRootView @Inject constructor(
|
|||||||
|
|
||||||
val rootView = RelativeLayout(context).apply {
|
val rootView = RelativeLayout(context).apply {
|
||||||
background = sres.getDrawable(R.attr.windowBackgroundColor)
|
background = sres.getDrawable(R.attr.windowBackgroundColor)
|
||||||
|
addAtTop(konfettiView)
|
||||||
addAtTop(tbar)
|
addAtTop(tbar)
|
||||||
addBelow(header, tbar)
|
addBelow(header, tbar)
|
||||||
addBelow(listView, header, height = MATCH_PARENT)
|
addBelow(listView, header, height = MATCH_PARENT)
|
||||||
|
|||||||
@@ -25,6 +25,9 @@ import android.content.Intent
|
|||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import dagger.Lazy
|
import dagger.Lazy
|
||||||
|
import nl.dionsegijn.konfetti.core.Party
|
||||||
|
import nl.dionsegijn.konfetti.core.Position
|
||||||
|
import nl.dionsegijn.konfetti.core.emitter.Emitter
|
||||||
import org.isoron.platform.gui.toInt
|
import org.isoron.platform.gui.toInt
|
||||||
import org.isoron.uhabits.R
|
import org.isoron.uhabits.R
|
||||||
import org.isoron.uhabits.activities.common.dialogs.CheckmarkDialog
|
import org.isoron.uhabits.activities.common.dialogs.CheckmarkDialog
|
||||||
@@ -63,6 +66,7 @@ import org.isoron.uhabits.intents.IntentFactory
|
|||||||
import org.isoron.uhabits.tasks.ExportDBTaskFactory
|
import org.isoron.uhabits.tasks.ExportDBTaskFactory
|
||||||
import org.isoron.uhabits.tasks.ImportDataTask
|
import org.isoron.uhabits.tasks.ImportDataTask
|
||||||
import org.isoron.uhabits.tasks.ImportDataTaskFactory
|
import org.isoron.uhabits.tasks.ImportDataTaskFactory
|
||||||
|
import org.isoron.uhabits.utils.ColorUtils
|
||||||
import org.isoron.uhabits.utils.copyTo
|
import org.isoron.uhabits.utils.copyTo
|
||||||
import org.isoron.uhabits.utils.currentTheme
|
import org.isoron.uhabits.utils.currentTheme
|
||||||
import org.isoron.uhabits.utils.dismissCurrentAndShow
|
import org.isoron.uhabits.utils.dismissCurrentAndShow
|
||||||
@@ -72,6 +76,7 @@ import org.isoron.uhabits.utils.showSendEmailScreen
|
|||||||
import org.isoron.uhabits.utils.showSendFileScreen
|
import org.isoron.uhabits.utils.showSendFileScreen
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
const val RESULT_IMPORT_DATA = 101
|
const val RESULT_IMPORT_DATA = 101
|
||||||
@@ -218,6 +223,28 @@ class ListHabitsScreen
|
|||||||
activity.showSendFileScreen(filename)
|
activity.showSendFileScreen(filename)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun showConfetti(color: PaletteColor, x: Float, y: Float) {
|
||||||
|
val baseColor = themeSwitcher.currentTheme!!.color(color).toInt()
|
||||||
|
rootView.get().konfettiView.start(
|
||||||
|
Party(
|
||||||
|
speed = 0f,
|
||||||
|
maxSpeed = 16f,
|
||||||
|
damping = 0.9f,
|
||||||
|
spread = 360,
|
||||||
|
angle = 0,
|
||||||
|
colors = listOf(
|
||||||
|
ColorUtils.changeHue(baseColor, 180f),
|
||||||
|
ColorUtils.changeHue(baseColor, 20f),
|
||||||
|
ColorUtils.changeHue(baseColor, -20f),
|
||||||
|
baseColor
|
||||||
|
),
|
||||||
|
position = Position.Absolute(x, y),
|
||||||
|
emitter = Emitter(duration = 25, TimeUnit.MILLISECONDS).max(25),
|
||||||
|
timeToLive = 0
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
override fun showSettingsScreen() {
|
override fun showSettingsScreen() {
|
||||||
val intent = intentFactory.startSettingsActivity(activity)
|
val intent = intentFactory.startSettingsActivity(activity)
|
||||||
activity.startActivityForResult(intent, REQUEST_SETTINGS)
|
activity.startActivityForResult(intent, REQUEST_SETTINGS)
|
||||||
@@ -240,7 +267,7 @@ class ListHabitsScreen
|
|||||||
putDouble("value", value)
|
putDouble("value", value)
|
||||||
putString("notes", notes)
|
putString("notes", notes)
|
||||||
}
|
}
|
||||||
dialog.onToggle = { v, n -> callback.onNumberPicked(v, n) }
|
dialog.onToggle = { v, n, x, y -> callback.onNumberPicked(v, n, x, y) }
|
||||||
dialog.dismissCurrentAndShow(fm, "numberDialog")
|
dialog.dismissCurrentAndShow(fm, "numberDialog")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -258,7 +285,7 @@ class ListHabitsScreen
|
|||||||
putInt("value", selectedValue)
|
putInt("value", selectedValue)
|
||||||
putString("notes", notes)
|
putString("notes", notes)
|
||||||
}
|
}
|
||||||
dialog.onToggle = { v, n -> callback.onNotesSaved(v, n) }
|
dialog.onToggle = { v, n, x, y -> callback.onNotesSaved(v, n, x, y) }
|
||||||
dialog.dismissCurrentAndShow(fm, "checkmarkDialog")
|
dialog.dismissCurrentAndShow(fm, "checkmarkDialog")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,7 @@
|
|||||||
package org.isoron.uhabits.activities.habits.list.views
|
package org.isoron.uhabits.activities.habits.list.views
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.graphics.PointF
|
||||||
import android.graphics.text.LineBreaker.BREAK_STRATEGY_BALANCED
|
import android.graphics.text.LineBreaker.BREAK_STRATEGY_BALANCED
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Build.VERSION.SDK_INT
|
import android.os.Build.VERSION.SDK_INT
|
||||||
@@ -154,7 +155,17 @@ class HabitCardView(
|
|||||||
checkmarkPanel = checkmarkPanelFactory.create().apply {
|
checkmarkPanel = checkmarkPanelFactory.create().apply {
|
||||||
onToggle = { timestamp, value, notes ->
|
onToggle = { timestamp, value, notes ->
|
||||||
triggerRipple(timestamp)
|
triggerRipple(timestamp)
|
||||||
habit?.let { behavior.onToggle(it, timestamp, value, notes) }
|
val location = getAbsoluteButtonLocation(timestamp)
|
||||||
|
habit?.let {
|
||||||
|
behavior.onToggle(
|
||||||
|
it,
|
||||||
|
timestamp,
|
||||||
|
value,
|
||||||
|
notes,
|
||||||
|
location.x,
|
||||||
|
location.y
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
onEdit = { timestamp ->
|
onEdit = { timestamp ->
|
||||||
triggerRipple(timestamp)
|
triggerRipple(timestamp)
|
||||||
@@ -206,12 +217,27 @@ class HabitCardView(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun triggerRipple(timestamp: Timestamp) {
|
fun triggerRipple(timestamp: Timestamp) {
|
||||||
|
val location = getRelativeButtonLocation(timestamp)
|
||||||
|
triggerRipple(location.x, location.y)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getRelativeButtonLocation(timestamp: Timestamp): PointF {
|
||||||
val today = DateUtils.getTodayWithOffset()
|
val today = DateUtils.getTodayWithOffset()
|
||||||
val offset = timestamp.daysUntil(today) - dataOffset
|
val offset = timestamp.daysUntil(today) - dataOffset
|
||||||
val button = checkmarkPanel.buttons[offset]
|
val button = checkmarkPanel.buttons[offset]
|
||||||
val y = button.height / 2.0f
|
val y = button.height / 2.0f
|
||||||
val x = checkmarkPanel.x + button.x + (button.width / 2).toFloat()
|
val x = checkmarkPanel.x + button.x + (button.width / 2).toFloat()
|
||||||
triggerRipple(x, y)
|
return PointF(x, y)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getAbsoluteButtonLocation(timestamp: Timestamp): PointF {
|
||||||
|
val containerLocation = IntArray(2)
|
||||||
|
this.getLocationOnScreen(containerLocation)
|
||||||
|
val relButtonLocation = getRelativeButtonLocation(timestamp)
|
||||||
|
return PointF(
|
||||||
|
containerLocation[0].toFloat() + relButtonLocation.x,
|
||||||
|
containerLocation[1].toFloat() - relButtonLocation.y
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onAttachedToWindow() {
|
override fun onAttachedToWindow() {
|
||||||
|
|||||||
@@ -179,7 +179,7 @@ class ShowHabitActivity : AppCompatActivity(), CommandRunner.Listener {
|
|||||||
putDouble("value", value)
|
putDouble("value", value)
|
||||||
putString("notes", notes)
|
putString("notes", notes)
|
||||||
}
|
}
|
||||||
dialog.onToggle = { v, n -> callback.onNumberPicked(v, n) }
|
dialog.onToggle = { v, n, x, y -> callback.onNumberPicked(v, n, x, y) }
|
||||||
dialog.dismissCurrentAndShow(supportFragmentManager, "numberDialog")
|
dialog.dismissCurrentAndShow(supportFragmentManager, "numberDialog")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -196,7 +196,7 @@ class ShowHabitActivity : AppCompatActivity(), CommandRunner.Listener {
|
|||||||
putInt("value", selectedValue)
|
putInt("value", selectedValue)
|
||||||
putString("notes", notes)
|
putString("notes", notes)
|
||||||
}
|
}
|
||||||
dialog.onToggle = { v, n -> callback.onNotesSaved(v, n) }
|
dialog.onToggle = { v, n, x, y -> callback.onNotesSaved(v, n, x, y) }
|
||||||
dialog.dismissCurrentAndShow(supportFragmentManager, "checkmarkDialog")
|
dialog.dismissCurrentAndShow(supportFragmentManager, "checkmarkDialog")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -36,6 +36,13 @@ object ColorUtils {
|
|||||||
return a or r or g or b
|
return a or r or g or b
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun changeHue(color: Int, delta: Float): Int {
|
||||||
|
val hsv = FloatArray(3)
|
||||||
|
Color.colorToHSV(color, hsv)
|
||||||
|
hsv[0] = (hsv[0] + delta).mod(360f)
|
||||||
|
return Color.HSVToColor(hsv)
|
||||||
|
}
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun setAlpha(color: Int, newAlpha: Float): Int {
|
fun setAlpha(color: Int, newAlpha: Float): Int {
|
||||||
val intAlpha = (newAlpha * 255).toInt()
|
val intAlpha = (newAlpha * 255).toInt()
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import android.content.Intent
|
|||||||
import android.graphics.Canvas
|
import android.graphics.Canvas
|
||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
import android.graphics.Paint
|
import android.graphics.Paint
|
||||||
|
import android.graphics.PointF
|
||||||
import android.graphics.drawable.ColorDrawable
|
import android.graphics.drawable.ColorDrawable
|
||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
import android.os.SystemClock
|
import android.os.SystemClock
|
||||||
@@ -135,7 +136,11 @@ fun Activity.startActivitySafely(intent: Intent) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Activity.showSendEmailScreen(@StringRes toId: Int, @StringRes subjectId: Int, content: String?) {
|
fun Activity.showSendEmailScreen(
|
||||||
|
@StringRes toId: Int,
|
||||||
|
@StringRes subjectId: Int,
|
||||||
|
content: String?
|
||||||
|
) {
|
||||||
val to = this.getString(toId)
|
val to = this.getString(toId)
|
||||||
val subject = this.getString(subjectId)
|
val subject = this.getString(subjectId)
|
||||||
this.startActivity(
|
this.startActivity(
|
||||||
@@ -232,3 +237,11 @@ fun View.requestFocusWithKeyboard() {
|
|||||||
dispatchTouchEvent(MotionEvent.obtain(time, time, MotionEvent.ACTION_UP, 0f, 0f, 0))
|
dispatchTouchEvent(MotionEvent.obtain(time, time, MotionEvent.ACTION_UP, 0f, 0f, 0))
|
||||||
}, 250)
|
}, 250)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun View.getCenter(): PointF {
|
||||||
|
val viewLocation = IntArray(2)
|
||||||
|
this.getLocationOnScreen(viewLocation)
|
||||||
|
viewLocation[0] += this.width / 2
|
||||||
|
viewLocation[1] -= this.height / 2
|
||||||
|
return PointF(viewLocation[0].toFloat(), viewLocation[1].toFloat())
|
||||||
|
}
|
||||||
|
|||||||
@@ -68,13 +68,13 @@ class CheckmarkWidgetView : HabitWidgetView {
|
|||||||
val fgColor: Int
|
val fgColor: Int
|
||||||
setShadowAlpha(0x4f)
|
setShadowAlpha(0x4f)
|
||||||
when (entryState) {
|
when (entryState) {
|
||||||
YES_MANUAL, SKIP -> {
|
YES_MANUAL, SKIP, YES_AUTO -> {
|
||||||
bgColor = activeColor
|
bgColor = activeColor
|
||||||
fgColor = res.getColor(R.attr.contrast0)
|
fgColor = res.getColor(R.attr.contrast0)
|
||||||
backgroundPaint!!.color = bgColor
|
backgroundPaint!!.color = bgColor
|
||||||
frame!!.setBackgroundDrawable(background)
|
frame!!.setBackgroundDrawable(background)
|
||||||
}
|
}
|
||||||
YES_AUTO, NO, UNKNOWN -> {
|
NO, UNKNOWN -> {
|
||||||
bgColor = res.getColor(R.attr.cardBgColor)
|
bgColor = res.getColor(R.attr.cardBgColor)
|
||||||
fgColor = res.getColor(R.attr.contrast60)
|
fgColor = res.getColor(R.attr.contrast60)
|
||||||
}
|
}
|
||||||
@@ -87,12 +87,23 @@ class CheckmarkWidgetView : HabitWidgetView {
|
|||||||
ring.setColor(fgColor)
|
ring.setColor(fgColor)
|
||||||
ring.setBackgroundColor(bgColor)
|
ring.setBackgroundColor(bgColor)
|
||||||
ring.setText(text)
|
ring.setText(text)
|
||||||
|
ring.setIsStrokedTextEnabled(strokedTextEnabled)
|
||||||
label.text = name
|
label.text = name
|
||||||
label.setTextColor(fgColor)
|
label.setTextColor(fgColor)
|
||||||
requestLayout()
|
requestLayout()
|
||||||
postInvalidate()
|
postInvalidate()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val strokedTextEnabled: Boolean
|
||||||
|
get() = if (isNumerical) {
|
||||||
|
false
|
||||||
|
} else {
|
||||||
|
when (entryState) {
|
||||||
|
YES_AUTO -> true
|
||||||
|
else -> false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private val text: String
|
private val text: String
|
||||||
get() = if (isNumerical) {
|
get() = if (isNumerical) {
|
||||||
(max(0, entryValue) / 1000.0).toShortString()
|
(max(0, entryValue) / 1000.0).toShortString()
|
||||||
|
|||||||
@@ -44,10 +44,10 @@ kotlin {
|
|||||||
val jvmMain by getting {
|
val jvmMain by getting {
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation(kotlin("stdlib-jdk8"))
|
implementation(kotlin("stdlib-jdk8"))
|
||||||
compileOnly("com.google.dagger:dagger:2.48.1")
|
compileOnly("com.google.dagger:dagger:2.51.1")
|
||||||
implementation("com.google.guava:guava:32.1.3-android")
|
implementation("com.google.guava:guava:33.1.0-android")
|
||||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.7.3")
|
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.7.3")
|
||||||
implementation("androidx.annotation:annotation:1.7.0")
|
implementation("androidx.annotation:annotation:1.7.1")
|
||||||
implementation("com.google.code.findbugs:jsr305:3.0.2")
|
implementation("com.google.code.findbugs:jsr305:3.0.2")
|
||||||
implementation("com.opencsv:opencsv:5.9")
|
implementation("com.opencsv:opencsv:5.9")
|
||||||
implementation("commons-codec:commons-codec:1.16.0")
|
implementation("commons-codec:commons-codec:1.16.0")
|
||||||
@@ -59,10 +59,10 @@ kotlin {
|
|||||||
dependencies {
|
dependencies {
|
||||||
implementation(kotlin("test"))
|
implementation(kotlin("test"))
|
||||||
implementation(kotlin("test-junit"))
|
implementation(kotlin("test-junit"))
|
||||||
implementation("org.xerial:sqlite-jdbc:3.42.0.0")
|
implementation("org.xerial:sqlite-jdbc:3.45.1.0")
|
||||||
implementation("org.hamcrest:hamcrest:2.2")
|
implementation("org.hamcrest:hamcrest:2.2")
|
||||||
implementation("org.apache.commons:commons-io:1.3.2")
|
implementation("org.apache.commons:commons-io:1.3.2")
|
||||||
implementation("org.mockito.kotlin:mockito-kotlin:5.1.0")
|
implementation("org.mockito.kotlin:mockito-kotlin:5.2.1")
|
||||||
implementation("org.junit.jupiter:junit-jupiter:5.10.1")
|
implementation("org.junit.jupiter:junit-jupiter:5.10.1")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ data class Habit(
|
|||||||
return if (isNumerical) {
|
return if (isNumerical) {
|
||||||
when (targetType) {
|
when (targetType) {
|
||||||
NumericalHabitType.AT_LEAST -> value / 1000.0 >= targetValue
|
NumericalHabitType.AT_LEAST -> value / 1000.0 >= targetValue
|
||||||
NumericalHabitType.AT_MOST -> value / 1000.0 <= targetValue
|
NumericalHabitType.AT_MOST -> value != Entry.UNKNOWN && value / 1000.0 <= targetValue
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
value != Entry.NO && value != Entry.UNKNOWN
|
value != Entry.NO && value != Entry.UNKNOWN
|
||||||
|
|||||||
@@ -20,9 +20,12 @@ package org.isoron.uhabits.core.ui.screens.habits.list
|
|||||||
|
|
||||||
import org.isoron.uhabits.core.commands.CommandRunner
|
import org.isoron.uhabits.core.commands.CommandRunner
|
||||||
import org.isoron.uhabits.core.commands.CreateRepetitionCommand
|
import org.isoron.uhabits.core.commands.CreateRepetitionCommand
|
||||||
|
import org.isoron.uhabits.core.models.Entry.Companion.YES_MANUAL
|
||||||
import org.isoron.uhabits.core.models.Habit
|
import org.isoron.uhabits.core.models.Habit
|
||||||
import org.isoron.uhabits.core.models.HabitList
|
import org.isoron.uhabits.core.models.HabitList
|
||||||
import org.isoron.uhabits.core.models.HabitType
|
import org.isoron.uhabits.core.models.HabitType
|
||||||
|
import org.isoron.uhabits.core.models.NumericalHabitType.AT_LEAST
|
||||||
|
import org.isoron.uhabits.core.models.NumericalHabitType.AT_MOST
|
||||||
import org.isoron.uhabits.core.models.PaletteColor
|
import org.isoron.uhabits.core.models.PaletteColor
|
||||||
import org.isoron.uhabits.core.models.Timestamp
|
import org.isoron.uhabits.core.models.Timestamp
|
||||||
import org.isoron.uhabits.core.preferences.Preferences
|
import org.isoron.uhabits.core.preferences.Preferences
|
||||||
@@ -52,8 +55,16 @@ open class ListHabitsBehavior @Inject constructor(
|
|||||||
val entry = habit.computedEntries.get(timestamp!!)
|
val entry = habit.computedEntries.get(timestamp!!)
|
||||||
if (habit.type == HabitType.NUMERICAL) {
|
if (habit.type == HabitType.NUMERICAL) {
|
||||||
val oldValue = entry.value.toDouble() / 1000
|
val oldValue = entry.value.toDouble() / 1000
|
||||||
screen.showNumberPopup(oldValue, entry.notes) { newValue: Double, newNotes: String ->
|
screen.showNumberPopup(oldValue, entry.notes) { newValue: Double, newNotes: String, x: Float, y: Float ->
|
||||||
val value = (newValue * 1000).roundToInt()
|
val value = (newValue * 1000).roundToInt()
|
||||||
|
if (newValue != oldValue) {
|
||||||
|
if (
|
||||||
|
(habit.targetType == AT_LEAST && newValue >= habit.targetValue) ||
|
||||||
|
(habit.targetType == AT_MOST && newValue <= habit.targetValue)
|
||||||
|
) {
|
||||||
|
screen.showConfetti(habit.color, x, y)
|
||||||
|
}
|
||||||
|
}
|
||||||
commandRunner.run(CreateRepetitionCommand(habitList, habit, timestamp, value, newNotes))
|
commandRunner.run(CreateRepetitionCommand(habitList, habit, timestamp, value, newNotes))
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -61,7 +72,8 @@ open class ListHabitsBehavior @Inject constructor(
|
|||||||
entry.value,
|
entry.value,
|
||||||
entry.notes,
|
entry.notes,
|
||||||
habit.color
|
habit.color
|
||||||
) { newValue, newNotes ->
|
) { newValue: Int, newNotes: String, x: Float, y: Float ->
|
||||||
|
if (newValue != entry.value && newValue == YES_MANUAL) screen.showConfetti(habit.color, x, y)
|
||||||
commandRunner.run(CreateRepetitionCommand(habitList, habit, timestamp, newValue, newNotes))
|
commandRunner.run(CreateRepetitionCommand(habitList, habit, timestamp, newValue, newNotes))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -117,10 +129,11 @@ open class ListHabitsBehavior @Inject constructor(
|
|||||||
if (prefs.isFirstRun) onFirstRun()
|
if (prefs.isFirstRun) onFirstRun()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onToggle(habit: Habit, timestamp: Timestamp, value: Int, notes: String) {
|
fun onToggle(habit: Habit, timestamp: Timestamp, value: Int, notes: String, x: Float, y: Float) {
|
||||||
commandRunner.run(
|
commandRunner.run(
|
||||||
CreateRepetitionCommand(habitList, habit, timestamp, value, notes)
|
CreateRepetitionCommand(habitList, habit, timestamp, value, notes)
|
||||||
)
|
)
|
||||||
|
if (value == YES_MANUAL) screen.showConfetti(habit.color, x, y)
|
||||||
}
|
}
|
||||||
|
|
||||||
enum class Message {
|
enum class Message {
|
||||||
@@ -144,12 +157,22 @@ open class ListHabitsBehavior @Inject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun interface NumberPickerCallback {
|
fun interface NumberPickerCallback {
|
||||||
fun onNumberPicked(newValue: Double, notes: String)
|
fun onNumberPicked(
|
||||||
|
newValue: Double,
|
||||||
|
notes: String,
|
||||||
|
x: Float,
|
||||||
|
y: Float
|
||||||
|
)
|
||||||
fun onNumberPickerDismissed() {}
|
fun onNumberPickerDismissed() {}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun interface CheckMarkDialogCallback {
|
fun interface CheckMarkDialogCallback {
|
||||||
fun onNotesSaved(value: Int, notes: String)
|
fun onNotesSaved(
|
||||||
|
value: Int,
|
||||||
|
notes: String,
|
||||||
|
x: Float,
|
||||||
|
y: Float
|
||||||
|
)
|
||||||
fun onNotesDismissed() {}
|
fun onNotesDismissed() {}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -170,5 +193,6 @@ open class ListHabitsBehavior @Inject constructor(
|
|||||||
)
|
)
|
||||||
fun showSendBugReportToDeveloperScreen(log: String)
|
fun showSendBugReportToDeveloperScreen(log: String)
|
||||||
fun showSendFileScreen(filename: String)
|
fun showSendFileScreen(filename: String)
|
||||||
|
fun showConfetti(color: PaletteColor, x: Float, y: Float)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -98,7 +98,7 @@ class HistoryCardPresenter(
|
|||||||
entry.value,
|
entry.value,
|
||||||
entry.notes,
|
entry.notes,
|
||||||
habit.color
|
habit.color
|
||||||
) { newValue, newNotes ->
|
) { newValue, newNotes, _: Float, _: Float ->
|
||||||
commandRunner.run(
|
commandRunner.run(
|
||||||
CreateRepetitionCommand(
|
CreateRepetitionCommand(
|
||||||
habitList,
|
habitList,
|
||||||
@@ -135,7 +135,7 @@ class HistoryCardPresenter(
|
|||||||
screen.showNumberPopup(
|
screen.showNumberPopup(
|
||||||
value = oldValue / 1000.0,
|
value = oldValue / 1000.0,
|
||||||
notes = entry.notes
|
notes = entry.notes
|
||||||
) { newValue: Double, newNotes: String ->
|
) { newValue: Double, newNotes: String, _: Float, _: Float ->
|
||||||
val thousands = (newValue * 1000).roundToInt()
|
val thousands = (newValue * 1000).roundToInt()
|
||||||
commandRunner.run(
|
commandRunner.run(
|
||||||
CreateRepetitionCommand(
|
CreateRepetitionCommand(
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ class ListHabitsBehaviorTest : BaseUnitTest() {
|
|||||||
eq(""),
|
eq(""),
|
||||||
picker.capture()
|
picker.capture()
|
||||||
)
|
)
|
||||||
picker.lastValue.onNumberPicked(100.0, "")
|
picker.lastValue.onNumberPicked(100.0, "", 0f, 0f)
|
||||||
val today = getTodayWithOffset()
|
val today = getTodayWithOffset()
|
||||||
assertThat(habit2.computedEntries.get(today).value, equalTo(100000))
|
assertThat(habit2.computedEntries.get(today).value, equalTo(100000))
|
||||||
}
|
}
|
||||||
@@ -168,7 +168,9 @@ class ListHabitsBehaviorTest : BaseUnitTest() {
|
|||||||
habit = habit1,
|
habit = habit1,
|
||||||
timestamp = getToday(),
|
timestamp = getToday(),
|
||||||
value = Entry.NO,
|
value = Entry.NO,
|
||||||
notes = ""
|
notes = "",
|
||||||
|
x = 0f,
|
||||||
|
y = 0f
|
||||||
)
|
)
|
||||||
assertFalse(habit1.isCompletedToday())
|
assertFalse(habit1.isCompletedToday())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,9 +38,9 @@ application {
|
|||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
val ktorVersion = "1.6.8"
|
val ktorVersion = "1.6.8"
|
||||||
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.9.21")
|
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.9.22")
|
||||||
implementation("io.ktor:ktor-server-netty:$ktorVersion")
|
implementation("io.ktor:ktor-server-netty:$ktorVersion")
|
||||||
implementation("ch.qos.logback:logback-classic:1.4.13")
|
implementation("ch.qos.logback:logback-classic:1.4.14")
|
||||||
implementation("io.ktor:ktor-server-core:$ktorVersion")
|
implementation("io.ktor:ktor-server-core:$ktorVersion")
|
||||||
implementation("io.ktor:ktor-html-builder:$ktorVersion")
|
implementation("io.ktor:ktor-html-builder:$ktorVersion")
|
||||||
implementation("io.ktor:ktor-jackson:$ktorVersion")
|
implementation("io.ktor:ktor-jackson:$ktorVersion")
|
||||||
@@ -49,7 +49,7 @@ dependencies {
|
|||||||
implementation("io.prometheus:simpleclient_httpserver:0.16.0")
|
implementation("io.prometheus:simpleclient_httpserver:0.16.0")
|
||||||
implementation("io.prometheus:simpleclient_hotspot:0.16.0")
|
implementation("io.prometheus:simpleclient_hotspot:0.16.0")
|
||||||
testImplementation("io.ktor:ktor-server-tests:$ktorVersion")
|
testImplementation("io.ktor:ktor-server-tests:$ktorVersion")
|
||||||
testImplementation("org.mockito.kotlin:mockito-kotlin:5.1.0")
|
testImplementation("org.mockito.kotlin:mockito-kotlin:5.2.1")
|
||||||
testImplementation(kotlin("test"))
|
testImplementation(kotlin("test"))
|
||||||
testImplementation(kotlin("test-junit"))
|
testImplementation(kotlin("test-junit"))
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user