diff --git a/app/build.gradle b/app/build.gradle index 111722c4..ca4f5463 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -197,6 +197,8 @@ dependencies { implementation 'com.github.PhilJay:MPAndroidChart:v3.1.0' + implementation 'com.github.sephiroth74:NumberSlidingPicker:v1.0.3' + testImplementation 'com.nhaarman:mockito-kotlin-kt1.1:1.6.0' testImplementation 'junit:junit:4.13.2' testImplementation "io.insert-koin:koin-test-junit4:$koin_version" diff --git a/app/src/main/java/io/zenandroid/onlinego/data/model/ogs/SeekGraphChallenge.kt b/app/src/main/java/io/zenandroid/onlinego/data/model/ogs/SeekGraphChallenge.kt index 3c95b86d..55ef6ec2 100644 --- a/app/src/main/java/io/zenandroid/onlinego/data/model/ogs/SeekGraphChallenge.kt +++ b/app/src/main/java/io/zenandroid/onlinego/data/model/ogs/SeekGraphChallenge.kt @@ -1,12 +1,14 @@ package io.zenandroid.onlinego.data.model.ogs +import io.zenandroid.onlinego.data.ogs.TimeControl import io.zenandroid.onlinego.utils.formatRank /** * Created by alex on 08/12/2017. */ data class SeekGraphChallenge ( - var challenge_id: Int? = null, + var challenge_id: Long? = null, + var game_started: Boolean = false, var delete: Int? = null, var name: String = "", var username: String = "", @@ -15,10 +17,10 @@ data class SeekGraphChallenge ( var min_rank: Double = 0.0, var max_rank: Double = 100.0, var handicap: Int = 0, - var timePerMove: Int = 0, + var time_per_move: Double? = null, var width: Int = 0, - var height: Int = 0//, -// var timeControlParameters: JSONObject? = null + var height: Int = 0, + var time_control_parameters: TimeControl? = null ) { override fun toString(): String { return "$username (${formatRank(rank)})" diff --git a/app/src/main/java/io/zenandroid/onlinego/data/ogs/OGSRestAPI.kt b/app/src/main/java/io/zenandroid/onlinego/data/ogs/OGSRestAPI.kt index b240d720..ddd464de 100644 --- a/app/src/main/java/io/zenandroid/onlinego/data/ogs/OGSRestAPI.kt +++ b/app/src/main/java/io/zenandroid/onlinego/data/ogs/OGSRestAPI.kt @@ -86,6 +86,9 @@ interface OGSRestAPI { @Query("ended__gt") ended: String, @Query("page") page: Int = 1): Single> + @POST("/api/v1/challenges/{challenge_id}/accept") + fun acceptOpenChallenge(@Path("challenge_id") id: Long): Completable + @GET("/api/v1/me/challenges?page_size=100") fun fetchChallenges(): Single> diff --git a/app/src/main/java/io/zenandroid/onlinego/data/ogs/OGSRestService.kt b/app/src/main/java/io/zenandroid/onlinego/data/ogs/OGSRestService.kt index 179e9d46..996e55f0 100644 --- a/app/src/main/java/io/zenandroid/onlinego/data/ogs/OGSRestService.kt +++ b/app/src/main/java/io/zenandroid/onlinego/data/ogs/OGSRestService.kt @@ -183,6 +183,9 @@ class OGSRestService( } } + fun acceptOpenChallenge(id: Long): Completable = + restApi.acceptOpenChallenge(id) + fun acceptChallenge(id: Long): Completable = restApi.acceptChallenge(id) diff --git a/app/src/main/java/io/zenandroid/onlinego/data/repositories/SeekGraphRepository.kt b/app/src/main/java/io/zenandroid/onlinego/data/repositories/SeekGraphRepository.kt new file mode 100644 index 00000000..8a2d8f63 --- /dev/null +++ b/app/src/main/java/io/zenandroid/onlinego/data/repositories/SeekGraphRepository.kt @@ -0,0 +1,39 @@ +package io.zenandroid.onlinego.data.repositories + +import android.util.Log +import io.reactivex.disposables.CompositeDisposable +import io.reactivex.schedulers.Schedulers +import io.reactivex.subjects.BehaviorSubject +import io.zenandroid.onlinego.data.model.ogs.SeekGraphChallenge +import io.zenandroid.onlinego.data.ogs.OGSWebSocketService +import io.zenandroid.onlinego.utils.addToDisposable + +class SeekGraphRepository( + private val socketService: OGSWebSocketService +): SocketConnectedRepository { + + private val subscriptions = CompositeDisposable() + private var challenges = emptyMap() + val challengesSubject = BehaviorSubject.create>() + + override fun onSocketConnected() { + socketService.connectToChallenges() + .subscribeOn(Schedulers.io()) + .subscribe(this::storeChallenge) { Log.e("SeekGraphRepository", it.toString()) } + .addToDisposable(subscriptions) + } + + private fun storeChallenge(challenge: SeekGraphChallenge) { + if (challenge.game_started) { + } else if (challenge.delete != null) { + challenges -= challenge.challenge_id!! + } else { + challenges += Pair(challenge.challenge_id!!, challenge) + } + challengesSubject.onNext(challenges.values.toList()) + } + + override fun onSocketDisconnected() { + subscriptions.clear() + } +} diff --git a/app/src/main/java/io/zenandroid/onlinego/di/Modules.kt b/app/src/main/java/io/zenandroid/onlinego/di/Modules.kt index 3aa1b383..f5c708a1 100644 --- a/app/src/main/java/io/zenandroid/onlinego/di/Modules.kt +++ b/app/src/main/java/io/zenandroid/onlinego/di/Modules.kt @@ -25,6 +25,7 @@ import io.zenandroid.onlinego.data.repositories.FinishedGamesRepository import io.zenandroid.onlinego.data.repositories.JosekiRepository import io.zenandroid.onlinego.data.repositories.PlayersRepository import io.zenandroid.onlinego.data.repositories.PuzzleRepository +import io.zenandroid.onlinego.data.repositories.SeekGraphRepository import io.zenandroid.onlinego.data.repositories.ServerNotificationsRepository import io.zenandroid.onlinego.data.repositories.SettingsRepository import io.zenandroid.onlinego.data.repositories.TutorialsRepository @@ -88,6 +89,7 @@ private val repositoriesModule = module { get(), get(), get(), + get(), get(), get(), get() @@ -103,6 +105,7 @@ private val repositoriesModule = module { single { JosekiRepository(get(), get()) } single { PuzzleRepository(get(), get()) } single { PlayersRepository(get(), get(), get()) } + single { SeekGraphRepository(get()) } single { ServerNotificationsRepository(get()) } single { SettingsRepository() } single { UserSessionRepository() } diff --git a/app/src/main/java/io/zenandroid/onlinego/ui/screens/automatch/NewAutomatchChallengeBottomSheet.kt b/app/src/main/java/io/zenandroid/onlinego/ui/screens/automatch/NewAutomatchChallengeBottomSheet.kt index b8f51d3e..378d320d 100644 --- a/app/src/main/java/io/zenandroid/onlinego/ui/screens/automatch/NewAutomatchChallengeBottomSheet.kt +++ b/app/src/main/java/io/zenandroid/onlinego/ui/screens/automatch/NewAutomatchChallengeBottomSheet.kt @@ -12,6 +12,7 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding @@ -24,6 +25,7 @@ import androidx.compose.material.MaterialTheme import androidx.compose.material.Surface import androidx.compose.material.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.State import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -40,13 +42,55 @@ import io.zenandroid.onlinego.data.model.ogs.Size import io.zenandroid.onlinego.data.model.ogs.Speed import io.zenandroid.onlinego.ui.screens.main.MainActivity import io.zenandroid.onlinego.ui.theme.OnlineGoTheme +import io.zenandroid.onlinego.ui.views.ClickableBubbleChart import io.zenandroid.onlinego.utils.rememberStateWithLifecycle import org.koin.androidx.viewmodel.ext.android.viewModel import java.util.Locale -class NewAutomatchChallengeBottomSheet : BottomSheetDialogFragment() { +import android.graphics.Color +import android.util.Log +import android.view.ViewGroup.LayoutParams +import androidx.compose.material.Divider +import androidx.compose.ui.viewinterop.AndroidView +import androidx.constraintlayout.widget.ConstraintLayout +import androidx.constraintlayout.widget.ConstraintSet +import androidx.core.content.res.ResourcesCompat +import androidx.fragment.app.FragmentActivity +import io.zenandroid.onlinego.data.model.ogs.SeekGraphChallenge +import io.zenandroid.onlinego.R +import androidx.navigation.findNavController +import androidx.navigation.NavOptions +import androidx.core.os.bundleOf +import androidx.core.view.marginTop +import io.zenandroid.onlinego.ui.screens.stats.PLAYER_ID +import com.github.mikephil.charting.charts.BubbleChart +import com.github.mikephil.charting.components.Legend +import com.github.mikephil.charting.components.LimitLine +import com.github.mikephil.charting.components.LimitLine.* +import com.github.mikephil.charting.components.XAxis +import com.github.mikephil.charting.data.BubbleData +import com.github.mikephil.charting.data.BubbleDataSet +import com.github.mikephil.charting.data.BubbleEntry +import com.github.mikephil.charting.data.Entry +import com.github.mikephil.charting.data.* +import com.github.mikephil.charting.formatter.ValueFormatter +import com.github.mikephil.charting.highlight.Highlight +import com.github.mikephil.charting.interfaces.datasets.IBubbleDataSet +import com.github.mikephil.charting.listener.OnChartValueSelectedListener +import com.github.mikephil.charting.utils.ColorTemplate +import io.zenandroid.onlinego.ui.screens.newchallenge.ChallengeMarkerView +import io.zenandroid.onlinego.utils.setMargins +import io.zenandroid.onlinego.utils.setMarginsDP +import kotlin.math.abs +import kotlin.math.log10 + +private const val TAG = "NewAutomatchChallengeBS" + +class NewAutomatchChallengeBottomSheet : BottomSheetDialogFragment(), OnChartValueSelectedListener { private val viewModel: NewAutomatchChallengeViewModel by viewModel() + private lateinit var chart: BubbleChart + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { val dialog = super.onCreateDialog(savedInstanceState) @@ -69,9 +113,11 @@ class NewAutomatchChallengeBottomSheet : BottomSheetDialogFragment() { return ComposeView(requireContext()).apply { setContent { val state by rememberStateWithLifecycle(viewModel.state) + OnlineGoTheme { NewAutomatchChallengeBottomSheetContent( state = state, + chart = @Composable { modifier -> Chart(state, modifier) }, onSmallCheckChanged = { viewModel.onSmallCheckChanged(it) }, onMediumCheckChanged = { viewModel.onMediumCheckChanged(it) }, onLargeCheckChanged = { viewModel.onLargeCheckChanged(it) }, @@ -95,11 +141,221 @@ class NewAutomatchChallengeBottomSheet : BottomSheetDialogFragment() { } } } + + @Composable + private fun Chart(state: AutomatchState, modifier: Modifier) { + val challenges = state.challenges + AndroidView( + modifier = Modifier + .fillMaxWidth() + .aspectRatio(ratio = 5f/4f), + factory = { context -> + ClickableBubbleChart(context).apply { + id = R.id.chart + layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT) + setMarginsDP(top = 4) + description.isEnabled = false + setOnChartValueSelectedListener(this@NewAutomatchChallengeBottomSheet) + setDrawGridBackground(false) + setTouchEnabled(true) + + // enable scaling and dragging + isDragEnabled = true + setScaleEnabled(true) + + setMaxVisibleValueCount(200) + setPinchZoom(true) + + // create a dataset and give it a type + val set1 = BubbleDataSet(ArrayList(), "19x19") + //set1.setDrawIcons(false) + set1.setColor(ColorTemplate.COLORFUL_COLORS[0], 130) + set1.setDrawValues(true) + set1.isNormalizeSizeEnabled = false + + val set2 = BubbleDataSet(ArrayList(), "13x13") + //set2.setDrawIcons(false) + //set2.setIconsOffset(MPPointF(0f, 15f)) + set2.setColor(ColorTemplate.COLORFUL_COLORS[1], 130) + set2.setDrawValues(true) + set2.isNormalizeSizeEnabled = false + + val set3 = BubbleDataSet(ArrayList(), "9x9") + set3.setColor(ColorTemplate.COLORFUL_COLORS[2], 130) + set3.setDrawValues(true) + set3.isNormalizeSizeEnabled = false + + val set4 = BubbleDataSet(ArrayList(), "?x?") + set4.setColor(ColorTemplate.COLORFUL_COLORS[3], 130) + set4.setDrawValues(true) + set4.isNormalizeSizeEnabled = false + + val set5 = BubbleDataSet(ArrayList(), "Eligible") + set5.setDrawIcons(false) + set5.setColor(Color.BLUE, 130) + set5.setDrawValues(true) + set5.isNormalizeSizeEnabled = false + + val dataSets = ArrayList() + dataSets.add(set1) // add the data sets + dataSets.add(set2) + dataSets.add(set3) + dataSets.add(set4) + dataSets.add(set5) + + // create a data object with the data sets + val data = BubbleData(dataSets) + data.setDrawValues(false) + data.setValueTextSize(8f) + data.setValueTextColor(Color.WHITE) + data.setHighlightCircleWidth(1.5f) + + this.data = data + this.invalidate() + + legend.apply { + verticalAlignment = Legend.LegendVerticalAlignment.TOP + horizontalAlignment = Legend.LegendHorizontalAlignment.RIGHT + orientation = Legend.LegendOrientation.VERTICAL + setDrawInside(false) + textColor = ResourcesCompat.getColor(resources, R.color.colorText, context.theme) + } + + axisLeft.apply { + spaceTop = 30f + spaceBottom = 30f + setDrawZeroLine(false) + setLabelCount(10, true) + setAxisMinValue(-1f) + setAxisMaxValue(38f) + isGranularityEnabled = true + granularity = 1f + valueFormatter = object : ValueFormatter() { + override fun getFormattedValue(value: Float): String { + val rank = value.toInt() + return when { + rank < 30 -> "${30 - rank}k" + else -> "${rank - 29}d" + } + } + } + textColor = ResourcesCompat.getColor(resources, R.color.colorText, context.theme) + state.rating.toFloat().let { + addLimitLine(LimitLine(it, "").apply { + lineWidth = .5f + lineColor = Color.WHITE + labelPosition = LimitLabelPosition.RIGHT_TOP + textSize = 10f + }) + } + } + + axisRight.isEnabled = false + + xAxis.apply { + position = XAxis.XAxisPosition.BOTTOM + valueFormatter = object : ValueFormatter() { + override fun getFormattedValue(value: Float): String = when(value.toInt()) { + 0 -> "Blitz" + 3 -> "Live" + 6 -> "Correspondence" + else -> "" + } + } + //setLabelCount(4, true) + setCenterAxisLabels(true) + labelRotationAngle = 9f + setAxisMinValue(0f) + setAxisMaxValue(8f) + isGranularityEnabled = true + granularity = 1f + textColor = ResourcesCompat.getColor(resources, R.color.colorText, context.theme) + addLimitLine(LimitLine(2f, "").apply { + lineWidth = 1.5f + lineColor = Color.GRAY + labelPosition = LimitLabelPosition.RIGHT_TOP + textSize = 10f + }) + addLimitLine(LimitLine(5f, "").apply { + lineWidth = 1.5f + lineColor = Color.GRAY + labelPosition = LimitLabelPosition.RIGHT_TOP + textSize = 10f + }) + } + + setNoDataTextColor(ResourcesCompat.getColor(resources, R.color.colorActionableText, context.theme)) + + let { chart -> + // create a custom MarkerView (extend MarkerView) and specify the layout to use for it + val mv = ChallengeMarkerView(context, { + dismiss() + (context as FragmentActivity).findNavController(R.id.fragment_container).navigate( + R.id.stats, + bundleOf(PLAYER_ID to it.id), + NavOptions.Builder() + .setLaunchSingleTop(true) + .setPopUpTo(R.id.myGames, false, false) + .build()) + }, { + dismiss() + }) + mv.chartView = chart + chart.marker = mv + } + }.also { this@NewAutomatchChallengeBottomSheet.chart = it } + }, + update = { chart -> + (chart as BubbleChart).apply { + for(i in 0..4) + data.getDataSetByIndex(i).clear() + + data.also { + challenges.forEach { challenge: SeekGraphChallenge -> + val rankDiff = (challenge.rank ?: 0.0) - state.rating.toDouble() + val drawable = when { + challenge.ranked && abs(rankDiff) > 9 -> null + state.rating < challenge.min_rank -> null + state.rating > challenge.max_rank -> null + else -> resources.getDrawable(R.drawable.ic_star) + } + val dataset = when { + drawable != null -> 4 + challenge.width == 19 -> 0 + challenge.width == 13 -> 1 + challenge.width == 9 -> 2 + else -> 3 + } + val entry = BubbleEntry( + log10((challenge.time_per_move ?: 0.0) + 1).toFloat(), + challenge.rank?.toFloat() ?: 0f, + .2f, drawable, challenge) + data.addEntry(entry, dataset) + } + + data.notifyDataChanged() + } + + notifyDataSetChanged() + invalidate() + } + } + ) + } + + override fun onValueSelected(e: Entry, h: Highlight) { + Log.d(TAG, "Val selected: " + chart.axisLeft.valueFormatter.getFormattedValue(e.y) + ", " + e.x + " - " + chart.data.getDataSetByIndex(h.dataSetIndex).label + " " + e.data) + } + + override fun onNothingSelected() { + Log.d(TAG, "Val unselected") + } } @Composable private fun NewAutomatchChallengeBottomSheetContent( state: AutomatchState, + chart: @Composable (modifier: Modifier) -> Unit, onSmallCheckChanged: (Boolean) -> Unit, onMediumCheckChanged: (Boolean) -> Unit, onLargeCheckChanged: (Boolean) -> Unit, @@ -159,6 +415,24 @@ private fun NewAutomatchChallengeBottomSheetContent( } } } + Divider( + //color = ResourcesCompat.getColor(this.context, android.R.color.black), + thickness = 1.dp, + modifier = Modifier + .fillMaxWidth() + .padding(0.dp, 10.dp) + ) + Column( + modifier = Modifier.padding(0.dp, 4.dp) + ) { + Text( + text = "Challenges", + fontWeight = FontWeight.Bold, + color = MaterialTheme.colors.primary, + modifier = Modifier.padding(top = 16.dp) + ) + chart(modifier = Modifier.fillMaxWidth()) + } Button( modifier = Modifier .fillMaxWidth() @@ -197,6 +471,6 @@ private fun RowScope.SizeCheckbox(checked: Boolean, text: String, onClick: (Bool private fun NewAutomatchChallengeBottomSheetPreview() { OnlineGoTheme { Box(modifier = Modifier.fillMaxSize()) - NewAutomatchChallengeBottomSheetContent(AutomatchState(), {}, {}, {}, {}, {}) + NewAutomatchChallengeBottomSheetContent(AutomatchState(), {}, {}, {}, {}, {}, {}) } -} \ No newline at end of file +} diff --git a/app/src/main/java/io/zenandroid/onlinego/ui/screens/automatch/NewAutomatchChallengeViewModel.kt b/app/src/main/java/io/zenandroid/onlinego/ui/screens/automatch/NewAutomatchChallengeViewModel.kt index f7d2d5ea..81735b75 100644 --- a/app/src/main/java/io/zenandroid/onlinego/ui/screens/automatch/NewAutomatchChallengeViewModel.kt +++ b/app/src/main/java/io/zenandroid/onlinego/ui/screens/automatch/NewAutomatchChallengeViewModel.kt @@ -1,13 +1,33 @@ package io.zenandroid.onlinego.ui.screens.automatch import android.preference.PreferenceManager +import android.util.Log +import com.github.mikephil.charting.listener.OnChartValueSelectedListener import androidx.lifecycle.ViewModel +import com.github.mikephil.charting.data.BubbleEntry +import com.github.mikephil.charting.data.Entry +import com.github.mikephil.charting.highlight.Highlight +import io.reactivex.Observable +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.disposables.CompositeDisposable +import io.reactivex.schedulers.Schedulers import io.zenandroid.onlinego.OnlineGoApplication +import io.zenandroid.onlinego.data.model.ogs.SeekGraphChallenge import io.zenandroid.onlinego.data.model.ogs.Speed +import io.zenandroid.onlinego.data.repositories.SeekGraphRepository +import io.zenandroid.onlinego.data.repositories.UserSessionRepository +import io.zenandroid.onlinego.utils.addToDisposable +import io.zenandroid.onlinego.utils.recordException import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.update -class NewAutomatchChallengeViewModel : ViewModel() { +private const val TAG = "NewAutomatchChallengeVM" + +class NewAutomatchChallengeViewModel( + private val userSessionRepository: UserSessionRepository, + private val seekGraphRepository: SeekGraphRepository, +) : ViewModel() { companion object { private const val SEARCH_GAME_SMALL = "SEARCH_GAME_SMALL" private const val SEARCH_GAME_MEDIUM = "SEARCH_GAME_MEDIUM" @@ -15,36 +35,76 @@ class NewAutomatchChallengeViewModel : ViewModel() { private const val SEARCH_GAME_SPEED = "SEARCH_GAME_SPEED" } + private val subscriptions = CompositeDisposable() + private val prefs = PreferenceManager.getDefaultSharedPreferences(OnlineGoApplication.instance) - val state = MutableStateFlow( + private val _state = MutableStateFlow( AutomatchState( small = prefs.getBoolean(SEARCH_GAME_SMALL, true), medium = prefs.getBoolean(SEARCH_GAME_MEDIUM, false), large = prefs.getBoolean(SEARCH_GAME_LARGE, false), - speed = Speed.valueOf(prefs.getString(SEARCH_GAME_SPEED, Speed.NORMAL.name)!!) + speed = Speed.valueOf(prefs.getString(SEARCH_GAME_SPEED, Speed.NORMAL.name)!!), + rating = userSessionRepository.uiConfig?.user?.ranking ?: 0 ) ) + val state: StateFlow = _state + + init { + seekGraphRepository.challengesSubject + .observeOn(Schedulers.single()) + .subscribeOn(Schedulers.io()) + .map { + it.sortedBy { challenge -> challenge.time_per_move } + } + .distinctUntilChanged() + .subscribe(this::setSeekGraph, this::onError) + .addToDisposable(subscriptions) + } + + fun onChallenges(body: (List) -> Unit) { + seekGraphRepository.challengesSubject + .observeOn(Schedulers.single()) + .subscribeOn(Schedulers.io()) + .subscribe({ challenges -> + body(challenges) + Log.d(TAG, "$challenges") + }, { + Log.e(TAG, it.toString()) + }) + .addToDisposable(subscriptions) + } + fun onSmallCheckChanged(checked: Boolean) { - state.update { it.copy(small = checked) } + _state.update { it.copy(small = checked) } prefs.edit().putBoolean(SEARCH_GAME_SMALL, checked).apply() } fun onMediumCheckChanged(checked: Boolean) { - state.update { it.copy(medium = checked) } + _state.update { it.copy(medium = checked) } prefs.edit().putBoolean(SEARCH_GAME_MEDIUM, checked).apply() } fun onLargeCheckChanged(checked: Boolean) { - state.update { it.copy(large = checked) } + _state.update { it.copy(large = checked) } prefs.edit().putBoolean(SEARCH_GAME_LARGE, checked).apply() } fun onSpeedChanged(speed: Speed) { - state.update { it.copy(speed = speed) } + _state.update { it.copy(speed = speed) } prefs.edit().putString(SEARCH_GAME_SPEED, speed.toString()).apply() } + private fun setSeekGraph(challenges: List) { + _state.update { + it.copy(challenges = challenges) + } + } + + private fun onError(t: Throwable) { + Log.e(this::class.java.canonicalName, t.message, t) + recordException(t) + } } data class AutomatchState( @@ -52,7 +112,9 @@ data class AutomatchState( val medium: Boolean = false, val large: Boolean = false, val speed: Speed = Speed.NORMAL, + val challenges: List = emptyList(), + val rating: Int = 0, ) { val isAnySizeSelected: Boolean get() = small || medium || large -} \ No newline at end of file +} diff --git a/app/src/main/java/io/zenandroid/onlinego/ui/screens/mygames/MyGamesViewModel.kt b/app/src/main/java/io/zenandroid/onlinego/ui/screens/mygames/MyGamesViewModel.kt index 08d96adf..22816c41 100644 --- a/app/src/main/java/io/zenandroid/onlinego/ui/screens/mygames/MyGamesViewModel.kt +++ b/app/src/main/java/io/zenandroid/onlinego/ui/screens/mygames/MyGamesViewModel.kt @@ -78,6 +78,7 @@ class MyGamesViewModel( headerMainText = "Hi ${userSessionRepository.uiConfig?.user?.username},", userImageURL = userSessionRepository.uiConfig?.user?.icon, boardTheme = settingsRepository.boardTheme, + ranking = userSessionRepository.uiConfig?.user?.ranking )) val state: StateFlow = _state private val subscriptions = CompositeDisposable() @@ -434,6 +435,7 @@ data class MyGamesState( val boardTheme: BoardTheme, val online: Boolean = true, val challengeDetailsStatus: ChallengeDialogStatus? = null, + val ranking: Int? = null, ) diff --git a/app/src/main/java/io/zenandroid/onlinego/ui/screens/newchallenge/ChallengeMarkerView.kt b/app/src/main/java/io/zenandroid/onlinego/ui/screens/newchallenge/ChallengeMarkerView.kt new file mode 100644 index 00000000..ab8298b6 --- /dev/null +++ b/app/src/main/java/io/zenandroid/onlinego/ui/screens/newchallenge/ChallengeMarkerView.kt @@ -0,0 +1,130 @@ +package io.zenandroid.onlinego.ui.screens.newchallenge + +import android.app.Activity +import android.content.Context +import android.graphics.Typeface +import android.os.Handler +import android.os.Looper +import android.util.Log +import android.view.MotionEvent +import android.view.View +import android.widget.LinearLayout +import android.widget.Toast +import android.widget.TextView +import androidx.fragment.app.DialogFragment +import androidx.fragment.app.Fragment +import androidx.fragment.app.FragmentManager +import com.github.mikephil.charting.components.MarkerView +import com.github.mikephil.charting.data.Entry +import com.github.mikephil.charting.highlight.Highlight +import com.github.mikephil.charting.utils.MPPointF +import com.google.android.material.button.MaterialButton +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.disposables.CompositeDisposable +import io.reactivex.schedulers.Schedulers +import io.zenandroid.onlinego.data.model.local.Player +import io.zenandroid.onlinego.data.model.ogs.SeekGraphChallenge +import io.zenandroid.onlinego.data.repositories.PlayersRepository +import io.zenandroid.onlinego.data.repositories.UserSessionRepository +import io.zenandroid.onlinego.data.ogs.OGSRestService +import io.zenandroid.onlinego.ui.screens.game.GameFragment +import io.zenandroid.onlinego.ui.views.ClickableMarkerView +import io.zenandroid.onlinego.utils.addToDisposable +import io.zenandroid.onlinego.utils.formatMillis +import io.zenandroid.onlinego.utils.formatRank +import io.zenandroid.onlinego.utils.timeControlDescription +import io.zenandroid.onlinego.R +import org.koin.core.context.GlobalContext.get + +class ChallengeMarkerView(context: Context, onProfile: (Player) -> Unit, onAccept: (Long) -> Unit) : ClickableMarkerView(context, R.layout.challenge_markerview) { + + private val subscriptions = CompositeDisposable() + private val containerView: LinearLayout = findViewById(R.id.containerView) + private val rankTextView: TextView = findViewById(R.id.rankTextView) + private val tpmTextView: TextView = findViewById(R.id.tpmTextView) + private val userTextView: TextView = findViewById(R.id.userTextView) + private val profileButton: MaterialButton = findViewById(R.id.profileButton) + private val acceptButton: MaterialButton = findViewById(R.id.acceptButton) + + private val playersRepository = get().get() + private val restService = get().get() + private val currentRating = get().get().uiConfig?.user?.ranking ?: 0 + + lateinit var challenge: SeekGraphChallenge + private set + + init { + listOf(profileButton, acceptButton).forEach { + it.setOnTouchListener { view, event -> + if (event.action == MotionEvent.ACTION_UP) { + view.performClick() + } + true + } + } + profileButton.setOnClickListener { + playersRepository.searchPlayers(this.challenge.username) + .observeOn(Schedulers.single()) + .subscribeOn(Schedulers.io()) + .subscribe({ + Handler(Looper.getMainLooper()).post { + it.firstOrNull()?.let { onProfile(it) } + } + }, { + Log.e("ChallengeMarkerView", it.toString()) + }) + .addToDisposable(subscriptions) + } + acceptButton.setOnClickListener { + restService.acceptOpenChallenge(this.challenge.challenge_id!!) + .observeOn(Schedulers.single()) + .subscribe({ + Handler(Looper.getMainLooper()).post { + onAccept(this.challenge.challenge_id!!) + } + }, { + Handler(Looper.getMainLooper()).post { + Toast.makeText(context, "Not Eligible", Toast.LENGTH_SHORT).show() + } + }) + .addToDisposable(subscriptions) + } + } + + override fun onClick(event: MotionEvent) = containerView.dispatchTouchEvent(event) + + // runs every time the MarkerView is redrawn + override fun refreshContent(e: Entry, highlight: Highlight) { + (e.data as? SeekGraphChallenge)?.let { challenge = it } + challenge.let { + val timePerMove = it.time_per_move?.toLong()?.times(1000)?.let(::formatMillis) ?: "" + val params = it.time_control_parameters?.let(::timeControlDescription) + val size = "${it.width}x${it.height}" + val ranked = "${if (it.ranked) "R" else "Unr"}anked" + val handicap = "${if (it.handicap == 0) "no" else it.handicap.toString()} handicap" + val minRank = formatRank(it.min_rank).let { if (it == "") null else ">=$it" } + val maxRank = formatRank(it.max_rank).let { if (it == "") null else "<=$it" } + val ranks = when { + minRank != null && maxRank != null -> "$minRank and $maxRank" + minRank != null -> maxRank + maxRank != null -> minRank + else -> "any" + } + acceptButton.visibility = when { + it.ranked && (e.y - currentRating) > 9 -> View.INVISIBLE + it.min_rank > currentRating -> View.INVISIBLE + it.max_rank < currentRating -> View.INVISIBLE + else -> View.VISIBLE + } + rankTextView.text = "${it.username} [${formatRank(it.rank)}]" + tpmTextView.text = "~$timePerMove / move" + userTextView.text = "\"${it.name}\": $ranked $size, $handicap, $params\nRanks: $ranks" + } + + super.refreshContent(e, highlight) + } + + override fun getOffset(): MPPointF { + return MPPointF(-(width.toFloat() / 2), -height.toFloat() - 10) + } +} diff --git a/app/src/main/java/io/zenandroid/onlinego/ui/screens/newchallenge/NameValuePicker.kt b/app/src/main/java/io/zenandroid/onlinego/ui/screens/newchallenge/NameValuePicker.kt new file mode 100644 index 00000000..6bac38a0 --- /dev/null +++ b/app/src/main/java/io/zenandroid/onlinego/ui/screens/newchallenge/NameValuePicker.kt @@ -0,0 +1,60 @@ +package io.zenandroid.onlinego.ui.screens.newchallenge + +import android.app.AlertDialog +import android.content.Context +import android.util.AttributeSet +import android.view.View +import android.widget.EditText +import android.widget.FrameLayout +import android.widget.TextView +import androidx.core.content.res.ResourcesCompat +import io.zenandroid.onlinego.R +import it.sephiroth.android.library.numberpicker.* + +class NameValuePicker : FrameLayout { + + var name: String? = null + set(value) { + field = value + findViewById(R.id.nameView).text = value + } + + var icon: String? = null + set(value) { + field = value + findViewById(R.id.nameView).text = value + } + + var value: Int = 0 + set(value) { + field = value + findViewById(R.id.pickerView).apply { + setProgress(value) + } + } + + var valuesCallback: (() -> List)? = null + + constructor(context: Context) : this(context, null) + constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0) + + constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { + init() + } + + private fun init() { + val view = View.inflate(context, R.layout.view_name_value_picker, this) + val subviews = ArrayList() + view.apply { + findViewsWithText(subviews, value.toString(), View.FIND_VIEWS_WITH_TEXT) + } + subviews.filterIsInstance().forEach { + val color = ResourcesCompat.getColor(this.resources, R.color.colorActionableText, null) + it.setTextColor(color) + } + findViewById(R.id.pickerView).doOnProgressChanged { _, progress, _ -> + this.value = progress + } + } + +} diff --git a/app/src/main/java/io/zenandroid/onlinego/ui/views/ClickableBubbleChart.kt b/app/src/main/java/io/zenandroid/onlinego/ui/views/ClickableBubbleChart.kt new file mode 100644 index 00000000..cdcee439 --- /dev/null +++ b/app/src/main/java/io/zenandroid/onlinego/ui/views/ClickableBubbleChart.kt @@ -0,0 +1,70 @@ +package io.zenandroid.onlinego.ui.views + +import android.content.Context +import android.graphics.Canvas +import android.graphics.Point +import android.graphics.Rect +import android.os.Build +import android.util.AttributeSet +import android.view.MotionEvent +import androidx.annotation.RequiresApi +import com.github.mikephil.charting.charts.BubbleChart +import com.github.mikephil.charting.components.MarkerView +import java.time.Instant.now + +class ClickableBubbleChart : BubbleChart { + constructor(context: Context) : super(context) + constructor(context: Context, attrs: AttributeSet) : super(context, attrs) + constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) + + override fun onTouchEvent(event: MotionEvent): Boolean { + // if there is no marker view or drawing marker is disabled + val markerView = this.marker as? ClickableMarkerView + return if (markerView != null && isDrawMarkersEnabled && valuesToHighlight()) { + val rect = Rect(markerView.drawingPosX.toInt(), markerView.drawingPosY.toInt(), + markerView.drawingPosX.toInt() + markerView.width, markerView.drawingPosY.toInt() + markerView.height) + if (rect.contains(event.x.toInt(), event.y.toInt())) { + // touch on marker -> dispatch touch event in to marker + event.offsetLocation(-markerView.drawingPosX, -markerView.drawingPosY) + markerView.dispatchTouchEvent(event) + true + } else { + super.onTouchEvent(event) + } + } else { + super.onTouchEvent(event) + } + } +} + +abstract class ClickableMarkerView(context: Context, layoutResource: Int) : MarkerView(context, layoutResource) { + var drawingPosX: Float = 0f + var drawingPosY: Float = 0f + private val MAX_CLICK_DURATION = 500 + private var startClickTime: Long = 0 + + abstract fun onClick(event: MotionEvent): Boolean + + @RequiresApi(Build.VERSION_CODES.O) + override fun onTouchEvent(event: MotionEvent): Boolean { + when(event.action) { + MotionEvent.ACTION_DOWN -> { + startClickTime = now().toEpochMilli() + } + MotionEvent.ACTION_UP -> { + val clickDuration = now().toEpochMilli() - startClickTime + if(clickDuration < MAX_CLICK_DURATION) { + return onClick(event) + } + } + } + return super.onTouchEvent(event) + } + + override fun draw(canvas: Canvas, posX: Float, posY: Float) { + super.draw(canvas, posX, posY) + val offset = getOffsetForDrawingAtPoint(posX, posY) + this.drawingPosX = posX + offset.x + this.drawingPosY = posY + offset.y + } +} diff --git a/app/src/main/res/drawable/rounded_border.xml b/app/src/main/res/drawable/rounded_border.xml new file mode 100644 index 00000000..c9be3731 --- /dev/null +++ b/app/src/main/res/drawable/rounded_border.xml @@ -0,0 +1,14 @@ + + + + + + + + + diff --git a/app/src/main/res/layout/challenge_markerview.xml b/app/src/main/res/layout/challenge_markerview.xml new file mode 100644 index 00000000..00caf62f --- /dev/null +++ b/app/src/main/res/layout/challenge_markerview.xml @@ -0,0 +1,119 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/view_name_value_picker.xml b/app/src/main/res/layout/view_name_value_picker.xml new file mode 100644 index 00000000..3f9ac241 --- /dev/null +++ b/app/src/main/res/layout/view_name_value_picker.xml @@ -0,0 +1,33 @@ + + + + + + + diff --git a/app/src/main/res/values/ids.xml b/app/src/main/res/values/ids.xml new file mode 100644 index 00000000..55ff1e38 --- /dev/null +++ b/app/src/main/res/values/ids.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file