Skip to content
This repository has been archived by the owner on Dec 22, 2024. It is now read-only.

Fix ConfirmationDialogFragment countdown problems completely (I guess) #10

Merged
merged 4 commits into from
Jul 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import androidx.fragment.app.Fragment
import androidx.lifecycle.Lifecycle
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import top.jiecs.screener.R

/**
Expand Down Expand Up @@ -50,7 +51,19 @@ class DisplayModeFragment : Fragment() {
// Handle the menu selection
return when (menuItem.itemId) {
R.id.new_display_mode -> {
// dialog to add a new display mode
// create dialog to add a new display mode with custom layout
context?.let {
MaterialAlertDialogBuilder(it)
.setTitle(R.string.new_display_mode)
.setView(R.layout.dialog_resolution_content)
.setPositiveButton(R.string.apply) { _, _ ->
// Respond to positive button press
}
.setNegativeButton(R.string.cancel) { _, _ ->
// Respond to negative button press
}
.show()
}
true
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,13 @@ package top.jiecs.screener.ui.resolution
import android.app.Dialog
import android.content.DialogInterface
import android.os.Bundle
import android.widget.Button
import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.DialogFragment
import androidx.lifecycle.MutableLiveData
import androidx.fragment.app.viewModels
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import top.jiecs.screener.R
Expand All @@ -19,6 +19,9 @@ class ConfirmationDialogFragment : DialogFragment() {

private lateinit var apiCaller: ApiCaller
private lateinit var dialog: AlertDialog
private lateinit var negativeButton: Button

private val confirmationDialogViewModel: ConfirmationDialogViewModel by viewModels()

override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
isCancelable = false
Expand All @@ -31,41 +34,40 @@ class ConfirmationDialogFragment : DialogFragment() {
apiCaller = ApiCaller()

dialog.setOnShowListener {
val negativeButton = dialog.getButton(DialogInterface.BUTTON_NEGATIVE)
negativeButton = dialog.getButton(DialogInterface.BUTTON_NEGATIVE)

confirmCountdown.observe(this) {
negativeButton.text = getString(R.string.undo_changes, "${it}s")
negativeButton.setOnClickListener {
negativeButton.text = getString(R.string.undo_changes, getString(R.string.undone))
apiCaller.resetResolution()
}
startConfirmCountdownTo {
if (isAdded && dialog.isShowing) {
negativeButton.performClick()
}

if (confirmationDialogViewModel.confirmCountdownJob == null) {
startConfirmCountdown()
} else {
confirmationDialogViewModel.confirmCountdown.postValue(0)
}
}

negativeButton.setOnClickListener {
confirmCountdownJob.value?.cancel()
apiCaller.resetResolution()
negativeButton.text =
getString(R.string.undo_changes, getString(R.string.undone))
confirmationDialogViewModel.confirmCountdown.observe(this) {
if (!::negativeButton.isInitialized) return@observe
if (it == 0) {
confirmationDialogViewModel.confirmCountdownJob?.cancel()
negativeButton.performClick()
} else {
negativeButton.text = getString(R.string.undo_changes, "${it}s")
}
}
return dialog
}

private val confirmCountdown: MutableLiveData<Int> by lazy {
MutableLiveData<Int>()
}
private val confirmCountdownJob: MutableLiveData<Job> by lazy {
MutableLiveData<Job>()
return dialog
}

private fun startConfirmCountdownTo(callback: () -> Unit) {
confirmCountdownJob.value = CoroutineScope(Dispatchers.Main).launch {
for (countdown in 3 downTo 0) {
confirmCountdown.postValue(countdown)
delay(1000)
private fun startConfirmCountdown() {
confirmationDialogViewModel.confirmCountdownJob =
CoroutineScope(Dispatchers.Main).launch {
for (countdown in 3 downTo 0) {
confirmationDialogViewModel.confirmCountdown.postValue(countdown)
delay(1000)
}
}
callback()
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package top.jiecs.screener.ui.resolution

import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import kotlinx.coroutines.Job

class ConfirmationDialogViewModel : ViewModel() {
val confirmCountdown: MutableLiveData<Int> by lazy {
MutableLiveData<Int>()
}
var confirmCountdownJob: Job? = null


}
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
package top.jiecs.screener.ui.resolution

import android.app.Dialog
import android.os.Bundle
import android.text.Editable
import android.view.LayoutInflater
import android.view.View
import androidx.core.widget.doAfterTextChanged
import androidx.fragment.app.DialogFragment
import androidx.fragment.app.activityViewModels
import androidx.fragment.app.viewModels
import androidx.navigation.fragment.NavHostFragment
import com.google.android.material.chip.Chip
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import top.jiecs.screener.MainViewModel
import top.jiecs.screener.R
import top.jiecs.screener.databinding.DialogResolutionContentBinding
import top.jiecs.screener.units.ApiCaller
import kotlin.math.pow
import kotlin.math.roundToInt
import kotlin.math.sqrt


class ResolutionDialogFragment : DialogFragment() {

private var _binding: DialogResolutionContentBinding? = null

// This property is only valid between onCreateView and
// onDestroyView.
private val binding get() = _binding!!

private val resolutionDialogViewModel: ResolutionDialogViewModel by viewModels()
private val mainViewModel by activityViewModels<MainViewModel>()

private lateinit var apiCaller: ApiCaller

// determine the most accurate original scale value required by the user
// The real-time changing ScaleSlider value will be changed
// when scaling cannot be performed at the original value
// Apply resolution still according to real-time value
private var stuckScaleValue = 0

override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
_binding = DialogResolutionContentBinding.inflate(LayoutInflater.from(context))

val dialog = MaterialAlertDialogBuilder(requireContext())
.setView(binding.root)
.create()

apiCaller = ApiCaller()

mainViewModel.shizukuPermissionGranted.observe(this) {
if (it) {
resolutionDialogViewModel.fetchScreenResolution()
}
}

resolutionDialogViewModel.physicalResolutionMap.observe(this) {
binding.textResolution.text = "Physical ${it?.get("height").toString()}x${
it?.get("width").toString()
}; DPI ${it?.get("dpi").toString()}"
}
val textHeight = binding.resolutionEditor.textHeight.editText!!
val textWidth = binding.resolutionEditor.textWidth.editText!!
val textDpi = binding.resolutionEditor.textDpi.editText!!
resolutionDialogViewModel.resolutionMap.observe(this) {
textHeight.setText(it?.get("height")?.toInt()?.toString())
textWidth.setText(it?.get("width")?.toInt()?.toString())
textDpi.setText(it?.get("dpi")?.toInt()?.toString())
}
val chipGroup = binding.resolutionEditor.chipGroup
resolutionDialogViewModel.usersList.observe(this) {
if (it.isEmpty()) return@observe
// for each user, create a chip to chip group
chipGroup.removeAllViews()
for (user in it) {
chipGroup.addView(Chip(chipGroup.context).apply {
text = "${user?.get("name")} (${user?.get("id")})"
isCheckable = true
})
}
// set default checked chip
val firstChip = chipGroup.getChildAt(0) as Chip
firstChip.isChecked = true
}

binding.resolutionEditor.sliderScale.addOnChangeListener { _, value, fromUser ->
value.toInt().let {
if (fromUser) stuckScaleValue = it
updateDpiEditorOrScaleSlider(scaleValue = it)
}
}
textWidth.doAfterTextChanged { s: Editable? ->
// auto calculate the other dimension when one dimension is changed
if (s.isNullOrBlank()) return@doAfterTextChanged
val physical =
resolutionDialogViewModel.physicalResolutionMap.value ?: return@doAfterTextChanged
val aspectRatio = physical["height"]!! / physical["width"]!!

when (s.hashCode()) {
textHeight.text.hashCode() -> {
val equalRatioWidth = s.toString().toInt() / aspectRatio
textWidth.setText(equalRatioWidth.roundToInt().toString())
}

textWidth.text.hashCode() -> {
val equalRatioHeight = s.toString().toInt() * aspectRatio
textHeight.setText(equalRatioHeight.roundToInt().toString())
}
}
updateDpiEditorOrScaleSlider(scaleValue = stuckScaleValue)
}
textDpi.doAfterTextChanged { s: Editable? ->
if (s.isNullOrBlank()) return@doAfterTextChanged
updateDpiEditorOrScaleSlider(scaleValue = null)
}
binding.btApply.setOnClickListener { v: View? ->
apiCaller.applyResolution(
textHeight.text.toString().toInt(),
textWidth.text.toString().toInt(),
textDpi.text.toString().toInt()
)
val navController = NavHostFragment.findNavController(this)
navController.navigate(R.id.nav_resolution_confirmation)
}
binding.btReset.setOnClickListener {
apiCaller.resetResolution()
}

return dialog
}

private fun updateDpiEditorOrScaleSlider(scaleValue: Int?) {
val scaledHeight =
binding.resolutionEditor.textHeight.editText!!.text.toString().toFloatOrNull() ?: return
val scaledWidth =
binding.resolutionEditor.textWidth.editText!!.text.toString().toFloatOrNull() ?: return
val physical = resolutionDialogViewModel.physicalResolutionMap.value ?: return

// Calculate the DPI that keeps the display size proportionally scaled
// Get the ratio of virtual to physical resolution diagonal (pythagorean theorem)
// physical_adj_ratio = √(h²+w²/ph²+pw²)
val physicalAdjRatio = sqrt(
(scaledHeight.pow(2) + scaledWidth.pow(2)) /
(physical["height"]!!.pow(2) + physical["width"]!!.pow(2))
)

val baseDpi = physical["dpi"]!! * physicalAdjRatio
if (scaleValue === null) {
val scaledDpi =
binding.resolutionEditor.textDpi.editText!!.text.toString().toFloatOrNull()
?: baseDpi
// scale_ratio = scaled_dpi / (physical_dpi * physical_adj_ratio)
val scaleRatio = scaledDpi / baseDpi
// 0.5 -> -50 ; 1 -> 0 ; 1.25 -> 25
val scaledValue = (scaleRatio - 1) * 100
// Round to two decimal places
// scale_ratio = ((scale_ratio * 100).roundToInt()) / 100
if (scaledValue < -50 || scaledValue > 50) {
binding.resolutionEditor.textDpi.error = "over limit"
return
} else {
binding.resolutionEditor.sliderScale.value =
((scaledValue / 5).roundToInt() * 5).toFloat()
binding.resolutionEditor.textDpi.error = null
}
} else {
// -50 -> 0.5 ; 0 -> 1 ; 25 -> 1.25
val scaleRatio = (scaleValue * 0.01 + 1).toFloat()
// scaled_dpi = physical_dpi * physical_adj_ratio * scale_ratio
val scaledDpi = (baseDpi * scaleRatio).roundToInt()
binding.resolutionEditor.textDpi.editText!!.setText(scaledDpi.toString())
}
}

override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package top.jiecs.screener.ui.resolution

import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel

import top.jiecs.screener.units.ApiCaller

class ResolutionDialogViewModel : ViewModel() {

val physicalResolutionMap: MutableLiveData<Map<String, Float>?> by lazy {
MutableLiveData<Map<String, Float>?>()
}
val resolutionMap: MutableLiveData<Map<String, Float>?> by lazy {
MutableLiveData<Map<String, Float>?>()
}

fun fetchScreenResolution() {
val resolution = ApiCaller().fetchScreenResolution()
physicalResolutionMap.value = resolution["physical"]
resolutionMap.value = resolution["override"]
}

val usersList: MutableLiveData<List<Map<String, Any>?>> by lazy {
MutableLiveData<List<Map<String, Any>?>>()
}

fun fetchUsers() {
usersList.value = ApiCaller().fetchUsers()
}
}
Loading
Loading