Skip to content
Draft
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
3 changes: 2 additions & 1 deletion features/pdf/src/main/kotlin/app/ss/pdf/PdfReaderImpl.kt
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import com.pspdfkit.document.download.DownloadJob
import com.pspdfkit.document.download.DownloadRequest
import com.pspdfkit.ui.PdfActivityIntentBuilder
import dagger.hilt.android.qualifiers.ApplicationContext
import io.adventech.blockkit.model.resource.PdfAux
import kotlinx.coroutines.withContext
import ss.foundation.coroutines.DispatcherProvider
import ss.libraries.circuit.navigation.PdfScreen
Expand Down Expand Up @@ -115,7 +116,7 @@ internal class PdfReaderImpl @Inject constructor(
}
}

override fun isDownloaded(pdf: LessonPdf): Boolean {
override fun isDownloaded(pdf: PdfAux): Boolean {
return File(context.getDir(FILE_DIRECTORY, Context.MODE_PRIVATE), "${pdf.id}.pdf").exists()
}

Expand Down
1 change: 1 addition & 0 deletions features/resource/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ android {

foundry {
features { compose() }
android { features { snapshotTests() } }
}

ksp {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import app.ss.models.OfflineState
import com.slack.circuit.codegen.annotations.CircuitInject
import com.slack.circuit.retained.produceRetainedState
import com.slack.circuit.retained.rememberRetained
Expand Down Expand Up @@ -68,9 +69,10 @@ class ResourcePresenter @AssistedInject constructor(
val ctaScreen = resourceCtaScreenProducer(resource)
val readDocumentTitle = rememberRetained(resource, ctaScreen) {
(ctaScreen as? CtaScreenState.Default)?.title?.takeIf {
resource?.progressTracking in setOf(ProgressTracking.AUTOMATIC, ProgressTracking.AUTOMATIC) == true
resource?.progressTracking in setOf(ProgressTracking.AUTOMATIC, ProgressTracking.AUTOMATIC)
} ?: ""
}
val offlineState by rememberOfflineState()

var overlayState by rememberRetained { mutableStateOf<ResourceOverlayState?>(null) }

Expand Down Expand Up @@ -103,6 +105,7 @@ class ResourcePresenter @AssistedInject constructor(
features = features,
fontFamilyProvider = fontFamilyProvider,
overlayState = overlayState,
offlineState = offlineState,
eventSink = eventSink,
)

Expand All @@ -118,6 +121,11 @@ class ResourcePresenter @AssistedInject constructor(
resourcesRepository.resource(screen.index).collect { value = it }
}

@Composable
private fun rememberOfflineState() = produceRetainedState(OfflineState.NONE) {
resourcesRepository.resourceOfflineState(screen.index).collect { value = it }
}

@CircuitInject(ResourceScreen::class, SingletonComponent::class)
@AssistedFactory
interface Factory {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@

package ss.resource

import app.ss.models.OfflineState
import com.slack.circuit.runtime.CircuitUiEvent
import com.slack.circuit.runtime.CircuitUiState
import io.adventech.blockkit.model.resource.Resource
Expand Down Expand Up @@ -51,6 +52,7 @@ sealed interface State: CircuitUiState {
val features: ImmutableList<FeatureSpec>,
val fontFamilyProvider: FontFamilyProvider,
val overlayState: ResourceOverlayState?,
val offlineState: OfflineState,
override val eventSink: (Event) -> Unit
): State

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@ fun ResourceUi(state: State, modifier: Modifier = Modifier) {
readMoreClick = {
state.eventSink(Event.OnReadMoreClick)
},
offlineState = state.offlineState,
)
}
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,14 @@

package ss.resource.components

import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
Expand All @@ -32,27 +38,43 @@ import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.ElevatedButton
import androidx.compose.material3.FilledIconButton
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.semantics.role
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.PreviewLightDark
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import app.ss.design.compose.extensions.color.parse
import app.ss.design.compose.theme.SsTheme
import app.ss.design.compose.theme.color.SsColors
import app.ss.design.compose.widget.button.SsButtonDefaults
import app.ss.design.compose.widget.icon.IconBox
import app.ss.design.compose.widget.icon.ResIcon
import app.ss.design.compose.widget.text.ReadMoreText
import app.ss.models.OfflineState
import io.adventech.blockkit.model.resource.Resource
import io.adventech.blockkit.ui.MarkdownText
import io.adventech.blockkit.ui.style.Styler
Expand All @@ -65,6 +87,7 @@ internal fun ColumnScope.CoverContent(
resource: Resource,
documentTitle: String?,
type: CoverContentType,
offlineState: OfflineState,
ctaOnClick: () -> Unit,
readMoreClick: () -> Unit
) {
Expand Down Expand Up @@ -142,6 +165,8 @@ internal fun ColumnScope.CoverContent(
text = resource.cta?.text,
documentTitle = documentTitle,
alignment = alignment,
offlineState = offlineState,
offlineStateClick = { }
)
}
}
Expand Down Expand Up @@ -218,11 +243,23 @@ private fun ColumnScope.CtaButton(
color: Color,
text: String?,
documentTitle: String?,
alignment: Alignment.Horizontal
alignment: Alignment.Horizontal,
offlineState: OfflineState,
offlineStateClick: () -> Unit
) {
val buttonColors = SsButtonDefaults.colors(
containerColor = color
)
val offlineStateIcon = remember(offlineState) {
when (offlineState) {
OfflineState.PARTIAL,
OfflineState.NONE -> ResIcon.Download

OfflineState.IN_PROGRESS -> null
OfflineState.COMPLETE -> ResIcon.Downloaded
}
}

Row(
modifier = Modifier
.defaultMinSize(minWidth = 140.dp)
Expand All @@ -244,6 +281,7 @@ private fun ColumnScope.CtaButton(
) {
Text(
text = text ?: stringResource(id = L10n.string.ss_lessons_read).uppercase(),
modifier = Modifier,
style = SsTheme.typography.titleMedium.copy(
fontWeight = FontWeight.Bold
)
Expand All @@ -264,15 +302,113 @@ private fun ColumnScope.CtaButton(
}
}
}

Spacer(
Modifier
.size(width = 0.5.dp, height = 48.dp)
.padding(vertical = 4.dp)
.background(color),
)

val progressAlpha by animateFloatAsState(
if (offlineState == OfflineState.IN_PROGRESS) 1f else 0f,
label = "progress"
)

FilledIconButton(
onClick = offlineStateClick,
modifier = Modifier
.graphicsLayer { translationX = -12f },
containerColor = color,
contentColor = downloadButtonIconColor
) {
AnimatedContent(
targetState = offlineStateIcon,
label = "download-icon"
) { targetIcon ->

Box(modifier = Modifier, Alignment.Center) {
CircularProgressIndicator(
modifier = Modifier
.padding(4.dp)
.alpha(progressAlpha),
color = downloadButtonIconColor
)

targetIcon?.let { IconBox(icon = it) }
}
}
}

}
}

private val readButtonElevation = 4.dp
private val readButtonCornerRadius = 20.dp
private val readButtonShape = RoundedCornerShape(readButtonCornerRadius)
private val readButtonShape = RoundedCornerShape(topStart = readButtonCornerRadius, bottomStart = readButtonCornerRadius)
private val downloadButtonIconColor = Color(0XFFFFFFFF)
private val downloadButtonShape = RoundedCornerShape(topEnd = readButtonCornerRadius, bottomEnd = readButtonCornerRadius)

@Composable
private fun FilledIconButton(
onClick: () -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true,
shape: Shape = downloadButtonShape,
containerColor: Color = Color.Unspecified,
contentColor: Color = Color.Unspecified,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
content: @Composable () -> Unit
) = Surface(
onClick = onClick,
modifier = modifier.semantics { role = Role.Button },
enabled = enabled,
shape = shape,
color = containerColor,
contentColor = contentColor,
shadowElevation = readButtonElevation,
interactionSource = interactionSource
) {
Box(
modifier = Modifier.size(40.dp),
contentAlignment = Alignment.Center
) {
content()
}
}

enum class CoverContentType {
PRIMARY,
SECONDARY,
SECONDARY_LARGE
}

@PreviewLightDark
@Composable
internal fun CtaButtonPreview() {
SsTheme {
Surface {
Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(16.dp)) {
CtaButton(
onClick = {},
color = Color(0xFF6200EE),
text = "Read".uppercase(),
documentTitle = "Document Title",
alignment = Alignment.CenterHorizontally,
offlineState = OfflineState.NONE,
offlineStateClick = {}
)

CtaButton(
onClick = {},
color = Color(0xFF6200EE),
text = "Read".uppercase(),
documentTitle = "Document Title",
alignment = Alignment.CenterHorizontally,
offlineState = OfflineState.COMPLETE,
offlineStateClick = {}
)
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/*
* Copyright (c) 2025. Adventech <info@adventech.io>
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/

package ss.resource.components

import app.ss.testing.roborazzi.BaseScreenshotTest
import app.ss.testing.roborazzi.TestLightDark
import org.junit.Test

class ResourceCtaTest : BaseScreenshotTest() {
@Test
fun testSnapShot() {
snapshot(TestLightDark.LIGHT) { CtaButtonPreview() }
}

@Test
fun testSnapShotDark() {
snapshot(TestLightDark.DARK) { CtaButtonPreview() }
}
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,10 @@ interface ResourcesApi {
suspend fun languages(): Response<List<LanguageResponse>>

@GET("api/v3/{language}/{type}/index.json")
suspend fun feed(@Path("language", encoded = true) language: String, @Path("type", encoded = true) type: String): Response<FeedResponse>
suspend fun feed(
@Path("language", encoded = true) language: String,
@Path("type", encoded = true) type: String,
): Response<FeedResponse>

@GET("api/v3/{language}/{type}/feeds/{groupId}/index.json")
suspend fun feedGroup(
Expand All @@ -71,7 +74,7 @@ interface ResourcesApi {
@Path("inputType", encoded = true) inputType: String,
@Path("documentId", encoded = true) documentId: String,
@Path("blockId", encoded = true) blockId: String,
@Body userInput: UserInputRequest
@Body userInput: UserInputRequest,
)

@GET("api/v3/{index}/audio.json")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@
package ss.libraries.pdf.api

import android.content.Intent
import app.ss.models.LessonPdf
import app.ss.models.PDFAux
import io.adventech.blockkit.model.resource.PdfAux
import ss.libraries.circuit.navigation.PdfScreen

/** API for handling pdf lessons. */
Expand All @@ -37,5 +37,5 @@ interface PdfReader {
suspend fun downloadFiles(pdfs: List<PDFAux>): Result<List<LocalFile>>

/** Returns true if this [pdf] file is downloaded. */
fun isDownloaded(pdf: LessonPdf): Boolean
fun isDownloaded(pdf: PdfAux): Boolean
}
Loading