Skip to content

Commit

Permalink
Merge pull request #1060 from hyperskill/release/1.59
Browse files Browse the repository at this point in the history
Release 1.59
  • Loading branch information
ivan-magda authored May 29, 2024
2 parents 45547fc + ee37451 commit 8a0897e
Show file tree
Hide file tree
Showing 170 changed files with 2,859 additions and 708 deletions.
1 change: 1 addition & 0 deletions androidHyperskillApp/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.DETECT_SCREEN_CAPTURE" />

<uses-permission
android:name="android.permission.WAKE_LOCK"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,13 @@ open class DefaultTapUpListener : GestureDetector.OnGestureListener {
override fun onSingleTapUp(e: MotionEvent): Boolean =
false

override fun onScroll(e1: MotionEvent, e2: MotionEvent, distanceX: Float, distanceY: Float): Boolean =
override fun onScroll(e1: MotionEvent?, e2: MotionEvent, distanceX: Float, distanceY: Float): Boolean =
false

override fun onLongPress(e: MotionEvent) {
// no op
}

override fun onFling(e1: MotionEvent, e2: MotionEvent, velocityX: Float, velocityY: Float): Boolean =
override fun onFling(e1: MotionEvent?, e2: MotionEvent, velocityX: Float, velocityY: Float): Boolean =
false
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package org.hyperskill.app.android.core.extensions

import android.app.Activity
import android.os.Build
import androidx.fragment.app.Fragment
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver

/**
* Sets a callback to be invoked when a screenshot is captured on the screen.
* Works only on Android 14 and higher.
*
* @param block The code block to be executed when a screenshot is captured.
*/
inline fun Fragment.doOnScreenShootCaptured(
crossinline block: () -> Unit
) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
val callBack = Activity.ScreenCaptureCallback {
block()
}
lifecycle.addObserver(
LifecycleEventObserver { _, event ->
when (event) {
Lifecycle.Event.ON_START -> {
requireActivity().registerScreenCaptureCallback(
/* executor = */ requireActivity().mainExecutor,
/* callback = */ callBack
)
}
Lifecycle.Event.ON_STOP -> {
requireActivity().unregisterScreenCaptureCallback(callBack)
}
else -> {
// no op
}
}
}
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package org.hyperskill.app.android.core.view.ui.widget.compose

import androidx.compose.animation.core.AnimationSpec
import androidx.compose.animation.core.Easing
import androidx.compose.animation.core.FastOutSlowInEasing
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.tween
import androidx.compose.runtime.Stable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
import androidx.compose.ui.draw.drawWithContent
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color

/**
* Applies shimmer animation to the target Composable.
* Animation is playing one time.
* To start animation call [ShimmerState.runShimmerAnimation] on the [ShimmerState] instance.
*/
fun Modifier.shimmerShot(shimmerState: ShimmerState): Modifier =
composed {
val startOffsetX by animateFloatAsState(
targetValue = shimmerState.targetValue,
animationSpec = shimmerState.startOffsetXAnimationSpec,
label = "shimmer"
)
drawWithContent {
val width = size.width
val height = size.height
val offset = startOffsetX * width

drawContent()
val brush = Brush.linearGradient(
colors = shimmerState.colors,
start = Offset(offset, 0f),
end = Offset(offset + width, height)
)
drawRect(brush)
}
}

@Stable
class ShimmerState(
val colors: List<Color> = listOf(
Color.Transparent,
Color.White.copy(alpha = 0.7f),
Color.Transparent
),
durationMillis: Int = 1200,
easing: Easing = FastOutSlowInEasing
) {

companion object {
private const val INITIAL_VALUE = -2f
private const val TARGET_VALUE = 2f
}

var targetValue: Float by mutableStateOf(INITIAL_VALUE)
private set

val startOffsetXAnimationSpec: AnimationSpec<Float> = tween(
durationMillis = durationMillis,
easing = easing
)

fun runShimmerAnimation() {
targetValue = TARGET_VALUE
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package org.hyperskill.app.android.core.view.ui.widget.compose

import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.setValue
import kotlin.random.Random
import kotlin.time.Duration.Companion.milliseconds
import kotlinx.coroutines.delay

/**
* A composable function that displays a text with a typewriter-like effect, revealing characters in chunks.
*
* @param text The input text to be displayed with the typewriter effect.
* @param minDelayInMillis The minimum delay in milliseconds between revealing character chunks, defaults to 30ms.
* @param maxDelayInMillis The maximum delay in milliseconds between revealing character chunks, defaults to 80ms.
* @param minCharacterChunk The minimum number of characters to reveal at once, defaults to 1.
* @param maxCharacterChunk The maximum number of characters to reveal at once, defaults to 3.
* @param onEffectCompleted A callback function invoked when the entire text has been revealed.
* @param displayTextComposable A composable function that receives the text to display with the typewriter effect.
*
* @throws IllegalArgumentException if [minDelayInMillis] is greater than [maxDelayInMillis].
* @throws IllegalArgumentException if [minCharacterChunk] is greater than [maxCharacterChunk].
*/
@Suppress("MaxLineLength")
@Composable
fun TypewriterTextEffect(
text: String,
startTypingDelayInMillis: Int? = null,
minDelayInMillis: Long = 30,
maxDelayInMillis: Long = 80,
minCharacterChunk: Int = 1,
maxCharacterChunk: Int = 3,
onEffectCompleted: () -> Unit = {},
displayTextComposable: @Composable (displayedText: String) -> Unit
) {
// Ensure minDelayInMillis is less than or equal to maxDelayInMillis
require(minDelayInMillis <= maxDelayInMillis) {
"TypewriterTextEffect: Invalid delay range. minDelayInMillis ($minDelayInMillis) must be less than or equal to maxDelayInMillis ($maxDelayInMillis)." //ktlint-disable
}

// Ensure minCharacterChunk is less than or equal to maxCharacterChunk
require(minCharacterChunk <= maxCharacterChunk) {
"TypewriterTextEffect: Invalid character chunk range. minCharacterChunk ($minCharacterChunk) must be less than or equal to maxCharacterChunk ($maxCharacterChunk)." //ktlint-disable
}

val currentOnEffectCompleted by rememberUpdatedState(newValue = onEffectCompleted)

// Initialize and remember the displayedText
var displayedText by remember { mutableStateOf("") }

// Call the displayTextComposable with the current displayedText value
displayTextComposable(displayedText)

// Launch the effect to update the displayedText value over time
LaunchedEffect(text) {
if (startTypingDelayInMillis != null) {
delay(startTypingDelayInMillis.milliseconds)
}

val textLength = text.length
var endIndex = 0

while (endIndex < textLength) {
endIndex = minOf(
endIndex + Random.nextInt(minCharacterChunk, maxCharacterChunk + 1),
textLength
)
displayedText = text.substring(startIndex = 0, endIndex = endIndex)
delay(Random.nextLong(minDelayInMillis, maxDelayInMillis))
}
currentOnEffectCompleted()
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
package org.hyperskill.app.android.latex.view.model.block

object DataMobileHiddenBlock : ContentBlock {

private const val DATA_MOBILE_HIDDEN_TAG = "data-mobile-hidden"

override val header: String = """
<script type="text/javascript" src="file:///android_asset/scripts/remove_data_mobile_hidden_elements.js"></script>
""".trimIndent()

override fun isEnabled(content: String): Boolean =
DATA_MOBILE_HIDDEN_TAG in content
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
package org.hyperskill.app.android.latex.view.model.block

object RemoveIFrameElementsInjection : ContentBlock {

private const val I_FRAME_TAG = "iframe"

override val header: String = """
<script type="text/javascript" src="file:///android_asset/scripts/remove_iframes.js"></script>
""".trimIndent()

override fun isEnabled(content: String): Boolean =
I_FRAME_TAG in content
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import org.hyperskill.app.android.step.view.model.StepCompletionHost
import org.hyperskill.app.android.step.view.model.StepCompletionView
import org.hyperskill.app.android.step_practice.view.fragment.StepPracticeDetailsFragment
import org.hyperskill.app.android.step_quiz.view.factory.StepQuizFragmentFactory
import org.hyperskill.app.android.topic_completion.fragment.TopicCompletedDialogFragment
import org.hyperskill.app.step.domain.model.Step
import org.hyperskill.app.step.domain.model.StepRoute
import org.hyperskill.app.step.presentation.StepFeature
Expand All @@ -41,7 +42,8 @@ class StageStepWrapperFragment :
Fragment(R.layout.fragment_stage_step_wrapper),
ReduxView<StepFeature.ViewState, StepFeature.Action.ViewAction>,
StepCompletionHost,
ShareStreakDialogFragment.Callback {
ShareStreakDialogFragment.Callback,
TopicCompletedDialogFragment.Callback {

companion object {
private const val STEP_DESCRIPTION_FRAGMENT_TAG = "step_content"
Expand Down Expand Up @@ -165,4 +167,12 @@ class StageStepWrapperFragment :
override fun onRefuseStreakSharingClick(streak: Int) {
stepViewModel.onRefuseStreakSharingClick(streak)
}

override fun navigateToStudyPlan() {
onNewMessage(StepCompletionFeature.Message.TopicCompletedModalGoToStudyPlanClicked)
}

override fun navigateToNextTopic() {
onNewMessage(StepCompletionFeature.Message.TopicCompletedModalContinueNextTopicClicked)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,10 @@ import org.hyperskill.app.android.main.view.ui.navigation.Tabs
import org.hyperskill.app.android.main.view.ui.navigation.switch
import org.hyperskill.app.android.request_review.dialog.RequestReviewDialogFragment
import org.hyperskill.app.android.share_streak.fragment.ShareStreakDialogFragment
import org.hyperskill.app.android.step.view.dialog.TopicPracticeCompletedBottomSheet
import org.hyperskill.app.android.step.view.model.StepHost
import org.hyperskill.app.android.step.view.navigation.requireStepRouter
import org.hyperskill.app.android.step_quiz.view.dialog.CompletedStepOfTheDayDialogFragment
import org.hyperskill.app.android.topic_completion.fragment.TopicCompletedDialogFragment
import org.hyperskill.app.android.view.base.ui.extension.snackbar
import org.hyperskill.app.step.presentation.StepFeature
import org.hyperskill.app.step_completion.presentation.StepCompletionFeature
Expand Down Expand Up @@ -54,7 +54,9 @@ object StepDelegate {
fragment: TFragment,
mainScreenRouter: MainScreenRouter,
action: StepFeature.Action.ViewAction
) where TFragment : Fragment, TFragment : ShareStreakDialogFragment.Callback {
) where TFragment : Fragment,
TFragment : ShareStreakDialogFragment.Callback,
TFragment : TopicCompletedDialogFragment.Callback {
when (action) {
is StepFeature.Action.ViewAction.StepCompletionViewAction -> {
when (val stepCompletionAction = action.viewAction) {
Expand All @@ -76,14 +78,11 @@ object StepDelegate {
}

is StepCompletionFeature.Action.ViewAction.ShowTopicCompletedModal -> {
TopicPracticeCompletedBottomSheet
.newInstance(
stepCompletionAction.modalText,
stepCompletionAction.isNextStepAvailable
)
TopicCompletedDialogFragment
.newInstance(stepCompletionAction.params)
.showIfNotExists(
fragment.childFragmentManager,
TopicPracticeCompletedBottomSheet.Tag
TopicCompletedDialogFragment.TAG
)
}
is StepCompletionFeature.Action.ViewAction.ShowProblemOfDaySolvedModal -> {
Expand Down
Loading

0 comments on commit 8a0897e

Please sign in to comment.