From aeeafa7689faf76b9b28c0394cc08eca5d2dfae1 Mon Sep 17 00:00:00 2001 From: Julian Waluschyk Date: Wed, 18 Dec 2024 12:02:00 +0100 Subject: [PATCH 01/12] added linkResolver architecture --- .../native_app/android/ui/MainActivity.kt | 4 +- core/ui/build.gradle.kts | 3 +- .../core/ui/markdown/LocalMarkwon.kt | 6 +- .../core/ui/markdown/MarkdownText.kt | 16 ++++- .../link_resolving/MarkdownLinkResolver.kt | 9 +++ .../MarkdownLinkResolverImpl.kt | 65 +++++++++++++++++++ .../artemis/native_app/core/ui/ui_module.kt | 3 + 7 files changed, 99 insertions(+), 7 deletions(-) create mode 100644 core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/markdown/link_resolving/MarkdownLinkResolver.kt create mode 100644 core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/markdown/link_resolving/MarkdownLinkResolverImpl.kt diff --git a/app/src/main/java/de/tum/informatics/www1/artemis/native_app/android/ui/MainActivity.kt b/app/src/main/java/de/tum/informatics/www1/artemis/native_app/android/ui/MainActivity.kt index e2d84904e..51088a045 100644 --- a/app/src/main/java/de/tum/informatics/www1/artemis/native_app/android/ui/MainActivity.kt +++ b/app/src/main/java/de/tum/informatics/www1/artemis/native_app/android/ui/MainActivity.kt @@ -37,6 +37,7 @@ import de.tum.informatics.www1.artemis.native_app.core.ui.LocalLinkOpener import de.tum.informatics.www1.artemis.native_app.core.ui.LocalWindowSizeClassProvider import de.tum.informatics.www1.artemis.native_app.core.ui.WindowSizeClassProvider import de.tum.informatics.www1.artemis.native_app.core.ui.alert.TextAlertDialog +import de.tum.informatics.www1.artemis.native_app.core.ui.markdown.link_resolving.LocalMarkdownLinkResolver import de.tum.informatics.www1.artemis.native_app.core.ui.remote_images.LocalArtemisImageProvider import de.tum.informatics.www1.artemis.native_app.feature.courseregistration.courseRegistration import de.tum.informatics.www1.artemis.native_app.feature.courseregistration.navigateToCourseRegistration @@ -255,7 +256,8 @@ class MainActivity : AppCompatActivity(), CompositionLocalProvider( LocalWindowSizeClassProvider provides windowSizeClassProvider, LocalLinkOpener provides linkOpener, - LocalArtemisImageProvider provides koinInject() + LocalArtemisImageProvider provides koinInject(), + LocalMarkdownLinkResolver provides koinInject() ) { // Use jetpack compose navigation for the navigation logic. NavHost(navController = navController, startDestination = startDestination) { diff --git a/core/ui/build.gradle.kts b/core/ui/build.gradle.kts index 6e3c5708a..2196ddec5 100644 --- a/core/ui/build.gradle.kts +++ b/core/ui/build.gradle.kts @@ -21,6 +21,7 @@ dependencies { api(libs.androidx.compose.material.iconsExtended) api(libs.androidx.compose.material3) api(libs.androidx.compose.material3.windowsizeclass) + implementation(libs.androidx.browser) debugApi(libs.androidx.compose.ui.tooling) api(libs.androidx.compose.ui.tooling.preview) api(libs.androidx.compose.ui.util) @@ -36,7 +37,7 @@ dependencies { implementation(libs.kotlinx.datetime) implementation(libs.kotlinx.coroutines.android) - implementation(libs.noties.markwon.core) + api(libs.noties.markwon.core) implementation(libs.noties.markwon.ext.strikethrough) implementation(libs.noties.markwon.ext.tables) implementation(libs.noties.markwon.html) diff --git a/core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/markdown/LocalMarkwon.kt b/core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/markdown/LocalMarkwon.kt index faddae433..782c4a884 100644 --- a/core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/markdown/LocalMarkwon.kt +++ b/core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/markdown/LocalMarkwon.kt @@ -6,6 +6,7 @@ import androidx.compose.runtime.ProvidableCompositionLocal import androidx.compose.runtime.compositionLocalOf import androidx.compose.runtime.remember import androidx.compose.ui.platform.LocalContext +import de.tum.informatics.www1.artemis.native_app.core.ui.markdown.link_resolving.LocalMarkdownLinkResolver import de.tum.informatics.www1.artemis.native_app.core.ui.remote_images.LocalArtemisImageProvider import io.noties.markwon.Markwon @@ -15,11 +16,12 @@ val LocalMarkwon: ProvidableCompositionLocal = @Composable fun ProvideMarkwon(content: @Composable () -> Unit) { val imageLoader = LocalArtemisImageProvider.current.rememberArtemisImageLoader() + val linkResolver = LocalMarkdownLinkResolver.current.rememberMarkdownLinkResolver() val context = LocalContext.current val imageWidth = context.resources.displayMetrics.widthPixels - val markdownRender: Markwon = remember(imageLoader) { - createMarkdownRender(context, imageLoader, imageWidth) + val markdownRender: Markwon = remember(imageLoader, linkResolver) { + createMarkdownRender(context, imageLoader, linkResolver, imageWidth) } CompositionLocalProvider(LocalMarkwon provides markdownRender) { diff --git a/core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/markdown/MarkdownText.kt b/core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/markdown/MarkdownText.kt index 7dc0cb905..f63527f1d 100644 --- a/core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/markdown/MarkdownText.kt +++ b/core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/markdown/MarkdownText.kt @@ -38,7 +38,9 @@ import de.tum.informatics.www1.artemis.native_app.core.common.R import de.tum.informatics.www1.artemis.native_app.core.common.markdown.ArtemisMarkdownTransformer import de.tum.informatics.www1.artemis.native_app.core.common.markdown.TYPE_ICON_RESOURCE_PATH import io.noties.markwon.AbstractMarkwonPlugin +import io.noties.markwon.LinkResolver import io.noties.markwon.Markwon +import io.noties.markwon.MarkwonConfiguration import io.noties.markwon.core.MarkwonTheme import io.noties.markwon.ext.strikethrough.StrikethroughPlugin import io.noties.markwon.ext.tables.TablePlugin @@ -93,14 +95,15 @@ fun MarkdownText( onClick: (() -> Unit)? = null, onLongClick: (() -> Unit)? = null, imageLoader: ImageLoader? = null, + linkResolver: LinkResolver? = null ) { val defaultColor: Color = LocalContentColor.current val context: Context = LocalContext.current val localMarkwon = LocalMarkwon.current val imageWidth = context.resources.displayMetrics.widthPixels - val markdownRender: Markwon = localMarkwon ?: remember(imageLoader) { - createMarkdownRender(context, imageLoader, imageWidth) + val markdownRender: Markwon = localMarkwon ?: remember(imageLoader, linkResolver) { + createMarkdownRender(context, imageLoader, linkResolver, imageWidth) } val markdownTransformer = LocalMarkdownTransformer.current @@ -224,7 +227,7 @@ private fun TextView.applyStyleAndColor( } } -fun createMarkdownRender(context: Context, imageLoader: ImageLoader?, imageWidth: Int): Markwon { +fun createMarkdownRender(context: Context, imageLoader: ImageLoader?, linkResolver: LinkResolver?, imageWidth: Int): Markwon { val imagePlugin: CoilImagesPlugin? = if (imageLoader != null) { CoilImagesPlugin.create( @@ -268,6 +271,13 @@ fun createMarkdownRender(context: Context, imageLoader: ImageLoader?, imageWidth if (imagePlugin != null) { usePlugin(imagePlugin) } + if (linkResolver != null) { + usePlugin(object : AbstractMarkwonPlugin() { + override fun configureConfiguration(builder: MarkwonConfiguration.Builder) { + builder.linkResolver(linkResolver) + } + }) + } } .build() } \ No newline at end of file diff --git a/core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/markdown/link_resolving/MarkdownLinkResolver.kt b/core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/markdown/link_resolving/MarkdownLinkResolver.kt new file mode 100644 index 000000000..084e75ddc --- /dev/null +++ b/core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/markdown/link_resolving/MarkdownLinkResolver.kt @@ -0,0 +1,9 @@ +package de.tum.informatics.www1.artemis.native_app.core.ui.markdown.link_resolving + +import androidx.compose.runtime.Composable +import io.noties.markwon.LinkResolver + +interface MarkdownLinkResolver { + @Composable + fun rememberMarkdownLinkResolver(): LinkResolver +} \ No newline at end of file diff --git a/core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/markdown/link_resolving/MarkdownLinkResolverImpl.kt b/core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/markdown/link_resolving/MarkdownLinkResolverImpl.kt new file mode 100644 index 000000000..3038f3ab8 --- /dev/null +++ b/core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/markdown/link_resolving/MarkdownLinkResolverImpl.kt @@ -0,0 +1,65 @@ +package de.tum.informatics.www1.artemis.native_app.core.ui.markdown.link_resolving + +import android.content.Context +import android.view.View +import android.widget.Toast +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.compositionLocalOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.platform.LocalContext +import de.tum.informatics.www1.artemis.native_app.core.datastore.AccountService +import de.tum.informatics.www1.artemis.native_app.core.datastore.ServerConfigurationService +import de.tum.informatics.www1.artemis.native_app.core.datastore.authToken +import io.noties.markwon.LinkResolver + + +val LocalMarkdownLinkResolver = compositionLocalOf { error("No MarkdownLinkResolver provided") } + +class MarkdownLinkResolverImpl( + private val accountService: AccountService, + private val serverConfigurationService: ServerConfigurationService, +): MarkdownLinkResolver { + @Composable + override fun rememberMarkdownLinkResolver(): LinkResolver { + val serverUrl by serverConfigurationService.serverUrl.collectAsState(initial = "") + val authToken by accountService.authToken.collectAsState(initial = "") + + val context = LocalContext.current + + return remember(context, authToken, serverUrl) { + BaseMarkdownLinkResolver(context, authToken, serverUrl) + } + } +} + +class BaseMarkdownLinkResolver( + private val context: Context, + private val authorizationToken: String, + private val serverUrl: String = "", +) : LinkResolver { + + override fun resolve(view: View, link: String) { + println(serverUrl) + when { + link.startsWith(serverUrl) -> { + + } + +// link.startsWith("customScheme://") -> { +// // Handle custom scheme links +// handleCustomScheme(link) +// } + + else -> { + Toast.makeText(context, "Unsupported link: $link", Toast.LENGTH_SHORT).show() + } + } + } + + private fun handleCustomScheme(link: String) { + // Parse and handle custom scheme as needed + Toast.makeText(context, "Handling custom scheme: $link", Toast.LENGTH_SHORT).show() + } +} diff --git a/core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/ui_module.kt b/core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/ui_module.kt index 8ad2090fa..35fd00843 100644 --- a/core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/ui_module.kt +++ b/core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/ui_module.kt @@ -1,9 +1,12 @@ package de.tum.informatics.www1.artemis.native_app.core.ui +import de.tum.informatics.www1.artemis.native_app.core.ui.markdown.link_resolving.MarkdownLinkResolver +import de.tum.informatics.www1.artemis.native_app.core.ui.markdown.link_resolving.MarkdownLinkResolverImpl import de.tum.informatics.www1.artemis.native_app.core.ui.remote_images.ArtemisImageProvider import de.tum.informatics.www1.artemis.native_app.core.ui.remote_images.impl.ArtemisImageProviderImpl import org.koin.dsl.module val uiModule = module { single { ArtemisImageProviderImpl(get(), get()) } + single { MarkdownLinkResolverImpl(get(), get()) } } \ No newline at end of file From edce871ea42ad168af7e7dbcb9d39c3d8eb4314d Mon Sep 17 00:00:00 2001 From: Julian Waluschyk Date: Wed, 18 Dec 2024 15:44:49 +0100 Subject: [PATCH 02/12] moved WebView and added LinkBottomSheet --- .../core/ui/compose}/ArtemisWebView.kt | 85 ++----------------- .../core/ui/compose/LinkBottomSheet.kt | 78 +++++++++++++++++ .../MarkdownLinkResolverImpl.kt | 52 +++++++----- .../exerciseview/ExerciseWebViewUtils.kt | 83 ++++++++++++++++++ .../home/overview/ExerciseOverviewTab.kt | 2 +- .../TextExerciseParticipationScreen.kt | 2 +- .../viewresult/ViewResultScreen.kt | 2 +- 7 files changed, 201 insertions(+), 103 deletions(-) rename {feature/exercise-view/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/exerciseview => core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/compose}/ArtemisWebView.kt (58%) create mode 100644 core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/compose/LinkBottomSheet.kt create mode 100644 feature/exercise-view/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/exerciseview/ExerciseWebViewUtils.kt diff --git a/feature/exercise-view/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/exerciseview/ArtemisWebView.kt b/core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/compose/ArtemisWebView.kt similarity index 58% rename from feature/exercise-view/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/exerciseview/ArtemisWebView.kt rename to core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/compose/ArtemisWebView.kt index 7da78614e..e7e73ee63 100644 --- a/feature/exercise-view/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/exerciseview/ArtemisWebView.kt +++ b/core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/compose/ArtemisWebView.kt @@ -1,4 +1,4 @@ -package de.tum.informatics.www1.artemis.native_app.feature.exerciseview +package de.tum.informatics.www1.artemis.native_app.core.ui.compose import android.annotation.SuppressLint import android.content.Context @@ -14,93 +14,16 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material3.CircularProgressIndicator import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.derivedStateOf -import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import com.google.accompanist.web.AccompanistWebViewClient -import com.google.accompanist.web.WebContent import com.google.accompanist.web.WebView import com.google.accompanist.web.WebViewState -import de.tum.informatics.www1.artemis.native_app.core.ui.exercise.ResultTemplateStatus -import io.ktor.http.* - -@Composable -internal fun getProblemStatementWebViewState( - serverUrl: String, - courseId: Long?, - exerciseId: Long?, - participationId: Long? -): WebViewState? { - val url by remember(serverUrl, courseId, exerciseId) { - derivedStateOf { - if (courseId != null && exerciseId != null) { - URLBuilder(serverUrl).apply { - appendPathSegments( - "courses", - courseId.toString(), - "exercises", - exerciseId.toString(), - "problem-statement" - ) - - if (participationId != null) { - appendPathSegments(participationId.toString()) - } - } - .buildString() - } else null - } - } - - return remember(url) { - derivedStateOf { - url?.let { - WebViewState(WebContent.Url(url = it)) - } - } - }.value -} - -@Composable -internal fun getFeedbackViewWebViewState( - serverUrl: String, - courseId: Long, - exerciseId: Long, - participationId: Long, - resultId: Long, - templateStatus: ResultTemplateStatus -): WebViewState { - val url by remember(serverUrl, courseId, exerciseId, resultId, templateStatus) { - derivedStateOf { - URLBuilder(serverUrl).apply { - appendPathSegments( - "courses", - courseId.toString(), - "exercises", - exerciseId.toString(), - "feedback", - participationId.toString(), - resultId.toString(), - (templateStatus == ResultTemplateStatus.Missing).toString() - ) - }.buildString() - } - } - - return remember(url) { - derivedStateOf { - url.let { - WebViewState(WebContent.Url(url = it)) - } - } - }.value -} @SuppressLint("SetJavaScriptEnabled") @Composable -internal fun ArtemisWebView( +fun ArtemisWebView( modifier: Modifier, webViewState: WebViewState, webView: WebView?, @@ -183,4 +106,6 @@ private class ThemeClient( super.onPageFinished(view, url) } -} \ No newline at end of file +} + + diff --git a/core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/compose/LinkBottomSheet.kt b/core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/compose/LinkBottomSheet.kt new file mode 100644 index 000000000..4ab9db3e5 --- /dev/null +++ b/core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/compose/LinkBottomSheet.kt @@ -0,0 +1,78 @@ +package de.tum.informatics.www1.artemis.native_app.core.ui.compose + +import android.annotation.SuppressLint +import android.webkit.WebView +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.ModalBottomSheet +import androidx.compose.runtime.Composable +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.google.accompanist.web.WebContent +import com.google.accompanist.web.WebViewState + +enum class LinkBottomSheetState { + PDFVIEWSTATE, + WEBVIEWSTATE +} + +@SuppressLint("SetJavaScriptEnabled") +@Composable +fun LinkBottomSheet( + modifier: Modifier, + serverUrl: String, + authToken: String, + link: String, + state: LinkBottomSheetState, + onDismissRequest: () -> Unit +) { + var webView: WebView? by remember { mutableStateOf(null) } + val webViewState = getWebViewState(link) + + ModalBottomSheet ( + modifier = modifier, + onDismissRequest = onDismissRequest + ) { + Box( + modifier = Modifier + .fillMaxHeight() + .padding(8.dp) + ) { + when (state) { + LinkBottomSheetState.PDFVIEWSTATE -> { + + } + LinkBottomSheetState.WEBVIEWSTATE -> { + ArtemisWebView( + modifier = Modifier + .fillMaxHeight() + .padding(8.dp), + webViewState = webViewState, + webView = webView, + setWebView = { webView = it }, + serverUrl = serverUrl, + authToken = authToken + ) + } + } + } + + } +} + +@Composable +private fun getWebViewState( + link: String +): WebViewState { + return remember(link) { + derivedStateOf { + WebViewState(WebContent.Url(url = link)) + } + }.value +} \ No newline at end of file diff --git a/core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/markdown/link_resolving/MarkdownLinkResolverImpl.kt b/core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/markdown/link_resolving/MarkdownLinkResolverImpl.kt index 3038f3ab8..30f032067 100644 --- a/core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/markdown/link_resolving/MarkdownLinkResolverImpl.kt +++ b/core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/markdown/link_resolving/MarkdownLinkResolverImpl.kt @@ -1,20 +1,25 @@ package de.tum.informatics.www1.artemis.native_app.core.ui.markdown.link_resolving import android.content.Context +import android.net.Uri import android.view.View -import android.widget.Toast +import androidx.browser.customtabs.CustomTabsIntent +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.compositionLocalOf import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import de.tum.informatics.www1.artemis.native_app.core.datastore.AccountService import de.tum.informatics.www1.artemis.native_app.core.datastore.ServerConfigurationService import de.tum.informatics.www1.artemis.native_app.core.datastore.authToken +import de.tum.informatics.www1.artemis.native_app.core.ui.compose.LinkBottomSheet +import de.tum.informatics.www1.artemis.native_app.core.ui.compose.LinkBottomSheetState import io.noties.markwon.LinkResolver - val LocalMarkdownLinkResolver = compositionLocalOf { error("No MarkdownLinkResolver provided") } class MarkdownLinkResolverImpl( @@ -25,41 +30,48 @@ class MarkdownLinkResolverImpl( override fun rememberMarkdownLinkResolver(): LinkResolver { val serverUrl by serverConfigurationService.serverUrl.collectAsState(initial = "") val authToken by accountService.authToken.collectAsState(initial = "") - val context = LocalContext.current + val (bottomSheetLink, setLinkToShow) = remember { mutableStateOf(null) } + val (bottomSheetState, setBottomSheetState) = remember { mutableStateOf(LinkBottomSheetState.WEBVIEWSTATE) } + + if (bottomSheetLink != null) { + LinkBottomSheet( + modifier = Modifier.fillMaxSize(), + serverUrl = serverUrl, + authToken = authToken, + link = bottomSheetLink, + state = bottomSheetState, + onDismissRequest = { setLinkToShow(null) } + ) + } + return remember(context, authToken, serverUrl) { - BaseMarkdownLinkResolver(context, authToken, serverUrl) + BaseMarkdownLinkResolver(context, serverUrl, setLinkToShow, setBottomSheetState) } } } class BaseMarkdownLinkResolver( private val context: Context, - private val authorizationToken: String, private val serverUrl: String = "", + private val showModalBottomSheet: (String) -> Unit, + private val setBottomSheetState: (LinkBottomSheetState) -> Unit ) : LinkResolver { - override fun resolve(view: View, link: String) { - println(serverUrl) when { + link.endsWith(".pdf") -> { + setBottomSheetState(LinkBottomSheetState.PDFVIEWSTATE) + showModalBottomSheet(link) + } link.startsWith(serverUrl) -> { - + setBottomSheetState(LinkBottomSheetState.WEBVIEWSTATE) + showModalBottomSheet(link) } - -// link.startsWith("customScheme://") -> { -// // Handle custom scheme links -// handleCustomScheme(link) -// } - else -> { - Toast.makeText(context, "Unsupported link: $link", Toast.LENGTH_SHORT).show() + val customTabsIntent = CustomTabsIntent.Builder().build() + customTabsIntent.launchUrl(context, Uri.parse(link)) } } } - - private fun handleCustomScheme(link: String) { - // Parse and handle custom scheme as needed - Toast.makeText(context, "Handling custom scheme: $link", Toast.LENGTH_SHORT).show() - } } diff --git a/feature/exercise-view/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/exerciseview/ExerciseWebViewUtils.kt b/feature/exercise-view/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/exerciseview/ExerciseWebViewUtils.kt new file mode 100644 index 000000000..ee2b30aff --- /dev/null +++ b/feature/exercise-view/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/exerciseview/ExerciseWebViewUtils.kt @@ -0,0 +1,83 @@ +package de.tum.informatics.www1.artemis.native_app.feature.exerciseview + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import com.google.accompanist.web.WebContent +import com.google.accompanist.web.WebViewState +import de.tum.informatics.www1.artemis.native_app.core.ui.exercise.ResultTemplateStatus +import io.ktor.http.URLBuilder +import io.ktor.http.appendPathSegments + +@Composable +internal fun getProblemStatementWebViewState( + serverUrl: String, + courseId: Long?, + exerciseId: Long?, + participationId: Long? +): WebViewState? { + val url by remember(serverUrl, courseId, exerciseId) { + derivedStateOf { + if (courseId != null && exerciseId != null) { + URLBuilder(serverUrl).apply { + appendPathSegments( + "courses", + courseId.toString(), + "exercises", + exerciseId.toString(), + "problem-statement" + ) + + if (participationId != null) { + appendPathSegments(participationId.toString()) + } + } + .buildString() + } else null + } + } + + return remember(url) { + derivedStateOf { + url?.let { + WebViewState(WebContent.Url(url = it)) + } + } + }.value +} + +@Composable +internal fun getFeedbackViewWebViewState( + serverUrl: String, + courseId: Long, + exerciseId: Long, + participationId: Long, + resultId: Long, + templateStatus: ResultTemplateStatus +): WebViewState { + val url by remember(serverUrl, courseId, exerciseId, resultId, templateStatus) { + derivedStateOf { + URLBuilder(serverUrl).apply { + appendPathSegments( + "courses", + courseId.toString(), + "exercises", + exerciseId.toString(), + "feedback", + participationId.toString(), + resultId.toString(), + (templateStatus == ResultTemplateStatus.Missing).toString() + ) + }.buildString() + } + } + + return remember(url) { + derivedStateOf { + url.let { + WebViewState(WebContent.Url(url = it)) + } + } + }.value +} \ No newline at end of file diff --git a/feature/exercise-view/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/exerciseview/home/overview/ExerciseOverviewTab.kt b/feature/exercise-view/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/exerciseview/home/overview/ExerciseOverviewTab.kt index 3ba65fbb0..12a2ef7ae 100644 --- a/feature/exercise-view/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/exerciseview/home/overview/ExerciseOverviewTab.kt +++ b/feature/exercise-view/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/exerciseview/home/overview/ExerciseOverviewTab.kt @@ -32,6 +32,7 @@ import com.google.accompanist.web.WebViewState import de.tum.informatics.www1.artemis.native_app.core.model.exercise.Exercise import de.tum.informatics.www1.artemis.native_app.core.model.exercise.QuizExercise import de.tum.informatics.www1.artemis.native_app.core.model.exercise.currentUserPoints +import de.tum.informatics.www1.artemis.native_app.core.ui.compose.ArtemisWebView import de.tum.informatics.www1.artemis.native_app.core.ui.date.getRelativeTime import de.tum.informatics.www1.artemis.native_app.core.ui.date.hasPassed import de.tum.informatics.www1.artemis.native_app.core.ui.exercise.ExerciseActions @@ -39,7 +40,6 @@ import de.tum.informatics.www1.artemis.native_app.core.ui.exercise.ExerciseCateg import de.tum.informatics.www1.artemis.native_app.core.ui.exercise.ExerciseInfoChip import de.tum.informatics.www1.artemis.native_app.core.ui.exercise.ExerciseInfoChipTextHorizontalPadding import de.tum.informatics.www1.artemis.native_app.core.ui.exercise.ExercisePointsDecimalFormat -import de.tum.informatics.www1.artemis.native_app.feature.exerciseview.ArtemisWebView import de.tum.informatics.www1.artemis.native_app.feature.exerciseview.R import kotlinx.datetime.Instant diff --git a/feature/exercise-view/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/exerciseview/participate/textexercise/TextExerciseParticipationScreen.kt b/feature/exercise-view/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/exerciseview/participate/textexercise/TextExerciseParticipationScreen.kt index 15cc73b6d..931abb3b3 100644 --- a/feature/exercise-view/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/exerciseview/participate/textexercise/TextExerciseParticipationScreen.kt +++ b/feature/exercise-view/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/exerciseview/participate/textexercise/TextExerciseParticipationScreen.kt @@ -41,9 +41,9 @@ import de.tum.informatics.www1.artemis.native_app.core.model.exercise.Exercise import de.tum.informatics.www1.artemis.native_app.core.model.exercise.participation.Participation import de.tum.informatics.www1.artemis.native_app.core.model.exercise.participation.isInitializationAfterDueDate import de.tum.informatics.www1.artemis.native_app.core.model.exercise.submission.Result +import de.tum.informatics.www1.artemis.native_app.core.ui.compose.ArtemisWebView import de.tum.informatics.www1.artemis.native_app.core.ui.date.isInFuture import de.tum.informatics.www1.artemis.native_app.core.ui.getWindowSizeClass -import de.tum.informatics.www1.artemis.native_app.feature.exerciseview.ArtemisWebView import de.tum.informatics.www1.artemis.native_app.feature.exerciseview.R import de.tum.informatics.www1.artemis.native_app.feature.exerciseview.getProblemStatementWebViewState diff --git a/feature/exercise-view/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/exerciseview/viewresult/ViewResultScreen.kt b/feature/exercise-view/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/exerciseview/viewresult/ViewResultScreen.kt index 03cf98a73..20a1fdac8 100644 --- a/feature/exercise-view/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/exerciseview/viewresult/ViewResultScreen.kt +++ b/feature/exercise-view/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/exerciseview/viewresult/ViewResultScreen.kt @@ -22,9 +22,9 @@ import de.tum.informatics.www1.artemis.native_app.core.data.join import de.tum.informatics.www1.artemis.native_app.core.model.exercise.latestParticipation import de.tum.informatics.www1.artemis.native_app.core.ui.Spacings import de.tum.informatics.www1.artemis.native_app.core.ui.common.EmptyDataStateUi +import de.tum.informatics.www1.artemis.native_app.core.ui.compose.ArtemisWebView import de.tum.informatics.www1.artemis.native_app.core.ui.exercise.LocalTemplateStatusProvider import de.tum.informatics.www1.artemis.native_app.core.ui.exercise.ProvideDefaultExerciseTemplateStatus -import de.tum.informatics.www1.artemis.native_app.feature.exerciseview.ArtemisWebView import de.tum.informatics.www1.artemis.native_app.feature.exerciseview.ExerciseViewModel import de.tum.informatics.www1.artemis.native_app.feature.exerciseview.R import de.tum.informatics.www1.artemis.native_app.feature.exerciseview.getFeedbackViewWebViewState From c5802c1c02baae80de753544221b85dd714f1159 Mon Sep 17 00:00:00 2001 From: Julian Waluschyk Date: Wed, 18 Dec 2024 19:05:51 +0100 Subject: [PATCH 03/12] added PdfViewer --- core/ui/build.gradle.kts | 1 + .../core/ui/compose/ArtemisPdfView.kt | 168 ++++++++++++++++++ .../core/ui/compose/LinkBottomSheet.kt | 9 +- .../MarkdownLinkResolverImpl.kt | 2 + gradle/libs.versions.toml | 3 + 5 files changed, 182 insertions(+), 1 deletion(-) create mode 100644 core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/compose/ArtemisPdfView.kt diff --git a/core/ui/build.gradle.kts b/core/ui/build.gradle.kts index 2196ddec5..28399d59a 100644 --- a/core/ui/build.gradle.kts +++ b/core/ui/build.gradle.kts @@ -44,4 +44,5 @@ dependencies { implementation(libs.noties.markwon.simple.ext) implementation(libs.noties.markwon.linkify) implementation(libs.noties.markwon.image.coil) + implementation(libs.girzzi91.bouquet) } \ No newline at end of file diff --git a/core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/compose/ArtemisPdfView.kt b/core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/compose/ArtemisPdfView.kt new file mode 100644 index 000000000..56c864ec1 --- /dev/null +++ b/core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/compose/ArtemisPdfView.kt @@ -0,0 +1,168 @@ +package de.tum.informatics.www1.artemis.native_app.core.ui.compose + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.RotateLeft +import androidx.compose.material.icons.automirrored.filled.RotateRight +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.FloatingActionButton +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.rizzi.bouquet.HorizontalPDFReader +import com.rizzi.bouquet.HorizontalPdfReaderState +import com.rizzi.bouquet.ResourceType +import com.rizzi.bouquet.VerticalPDFReader +import com.rizzi.bouquet.VerticalPdfReaderState +import com.rizzi.bouquet.rememberHorizontalPdfReaderState +import com.rizzi.bouquet.rememberVerticalPdfReaderState +import io.ktor.http.HttpHeaders + +// Inspired by: https://github.com/GRizzi91/bouquet +/* + * Copyright [2022] [Graziano Rizzi] + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@Composable +fun ArtemisPdfView( + modifier: Modifier, + filename: String? = null, + url: String, + authToken: String, +) { + val isVertical = remember { mutableStateOf(true) } + val resource = ResourceType.Remote( + url = url, + headers = hashMapOf(HttpHeaders.Cookie to "jwt=$authToken") + ) + + val verticalPdfState = rememberVerticalPdfReaderState( + resource = resource, + isZoomEnable = true + ) + val horizontalPdfState = rememberHorizontalPdfReaderState( + resource = resource, + isZoomEnable = true + ) + + val pdfState = if (isVertical.value) verticalPdfState else horizontalPdfState + + Box( + modifier = modifier.padding(8.dp), + ) { + Column { + pdfState.file?.let { + Text( + text = filename ?: it.name, + modifier = Modifier.padding(8.dp), + style = MaterialTheme.typography.titleMedium + ) + } + if (isVertical.value) { + VerticalPDFReader( + state = pdfState as VerticalPdfReaderState, + modifier = Modifier + .background(color = MaterialTheme.colorScheme.surface) + .fillMaxSize() + ) + } else { + HorizontalPDFReader( + state = pdfState as HorizontalPdfReaderState, + modifier = Modifier + .fillMaxSize() + .background( + color = MaterialTheme.colorScheme.surface, + shape = MaterialTheme.shapes.medium + ) + ) + } + } + + FloatingActionButton( + onClick = { + isVertical.value = !isVertical.value + }, + modifier = Modifier + .align(Alignment.BottomEnd) + .padding(16.dp) + ) { + Icon( + imageVector = if (isVertical.value) Icons.AutoMirrored.Filled.RotateLeft else Icons.AutoMirrored.Filled.RotateRight, + contentDescription = null + ) + } + +// FloatingActionButton( +// onClick = { /* Share logic */ }, +// modifier = Modifier +// .align(Alignment.BottomEnd) +// .zIndex(1f) +// .padding(16.dp) +// ) { +// Icon( +// imageVector = Icons.Default.Share, +// contentDescription = null +// ) +// } + + if (!pdfState.isLoaded) { + Box( + modifier = Modifier + .fillMaxSize(), + contentAlignment = Alignment.Center + ) { + CircularProgressIndicator( + progress = { pdfState.loadPercent / 100f }, + modifier = Modifier.height(48.dp), + color = MaterialTheme.colorScheme.primary + ) + } + } + + Box( + modifier = Modifier + .fillMaxWidth() + .align(Alignment.TopStart).padding(top = 58.dp, end = 8.dp) + .padding(16.dp), + contentAlignment = Alignment.TopEnd + ) { + Column( + modifier = Modifier + .background( + color = MaterialTheme.colorScheme.surface.copy(alpha = 0.5f), + shape = MaterialTheme.shapes.medium + ) + .padding(8.dp) + ) { + Text( + text = "Page: ${pdfState.currentPage}/${pdfState.pdfPageCount}", + style = MaterialTheme.typography.bodyMedium + ) + } + } + } +} \ No newline at end of file diff --git a/core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/compose/LinkBottomSheet.kt b/core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/compose/LinkBottomSheet.kt index 4ab9db3e5..72cb79b97 100644 --- a/core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/compose/LinkBottomSheet.kt +++ b/core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/compose/LinkBottomSheet.kt @@ -4,6 +4,7 @@ import android.annotation.SuppressLint import android.webkit.WebView import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.material3.ModalBottomSheet import androidx.compose.runtime.Composable @@ -29,6 +30,7 @@ fun LinkBottomSheet( serverUrl: String, authToken: String, link: String, + fileName: String?, state: LinkBottomSheetState, onDismissRequest: () -> Unit ) { @@ -46,7 +48,12 @@ fun LinkBottomSheet( ) { when (state) { LinkBottomSheetState.PDFVIEWSTATE -> { - + ArtemisPdfView( + modifier = Modifier.fillMaxSize(), + url = link, + authToken = authToken, + filename = fileName + ) } LinkBottomSheetState.WEBVIEWSTATE -> { ArtemisWebView( diff --git a/core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/markdown/link_resolving/MarkdownLinkResolverImpl.kt b/core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/markdown/link_resolving/MarkdownLinkResolverImpl.kt index 30f032067..30f630258 100644 --- a/core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/markdown/link_resolving/MarkdownLinkResolverImpl.kt +++ b/core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/markdown/link_resolving/MarkdownLinkResolverImpl.kt @@ -36,11 +36,13 @@ class MarkdownLinkResolverImpl( val (bottomSheetState, setBottomSheetState) = remember { mutableStateOf(LinkBottomSheetState.WEBVIEWSTATE) } if (bottomSheetLink != null) { + val filename = if (bottomSheetState == LinkBottomSheetState.PDFVIEWSTATE) bottomSheetLink.substringAfterLast("/") else null LinkBottomSheet( modifier = Modifier.fillMaxSize(), serverUrl = serverUrl, authToken = authToken, link = bottomSheetLink, + fileName = filename, state = bottomSheetState, onDismissRequest = { setLinkToShow(null) } ) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 57972ba76..47efcff5c 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -34,6 +34,7 @@ placeholderMaterial = "2.0.0" room = "2.6.1" sentry-android = "7.19.0" work = "2.10.0" +composeMaterial3 = "1.0.0-alpha30" [libraries] accompanist-placeholder-material = { group = "com.google.accompanist", name = "accompanist-placeholder-material", version.ref = "accompanist" } @@ -84,6 +85,7 @@ coil-network = { group = "io.coil-kt.coil3", name = "coil-network-okhttp", versi coil2-base = { group = "io.coil-kt", name = "coil-base", version.ref = "coil2" } google-firebase-bom = { group = "com.google.firebase", name = "firebase-bom", version = "33.7.0" } google-firebase-messaging = { group = "com.google.firebase", name = "firebase-messaging" } +girzzi91-bouquet = { group = "io.github.grizzi91", name = "bouquet", version = "1.1.2"} kotlinx-coroutines-android = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-android", version.ref = "kotlinxCoroutines" } kotlinx-coroutines-core = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-core", version.ref = "kotlinxCoroutines" } kotlinx-coroutines-core-jvm = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-core-jvm", version.ref = "kotlinxCoroutines" } @@ -131,6 +133,7 @@ toolbar-compose = { group = "me.onebone", name = "toolbar-compose", version = "2 android-gradlePlugin = { group = "com.android.tools.build", name = "gradle", version.ref = "androidGradlePlugin" } kotlin-gradlePlugin = { group = "org.jetbrains.kotlin", name = "kotlin-gradle-plugin", version.ref = "kotlin" } kotlin-kover = { group = "org.jetbrains.kotlinx", name = "kover-gradle-plugin", version.ref = "kover" } +compose-material3 = { group = "androidx.wear.compose", name = "compose-material3", version.ref = "composeMaterial3" } [plugins] android-application = { id = "com.android.application", version.ref = "androidGradlePlugin" } From b2e77b892fe0c49f61a271ef645ee987c9e52ef7 Mon Sep 17 00:00:00 2001 From: Julian Waluschyk Date: Thu, 19 Dec 2024 16:10:50 +0100 Subject: [PATCH 04/12] added third party pdf viewer implementation and applied modifications --- core/ui/build.gradle.kts | 1 - .../core/ui/compose/ArtemisPdfView.kt | 71 ++-- .../artemis/native_app/core/ui/pdf/Notice.md | 19 ++ .../artemis/native_app/core/ui/pdf/PdfFile.kt | 121 +++++++ .../core/ui/pdf/render/PdfRendering.kt | 157 +++++++++ .../native_app/core/ui/pdf/render/PdfView.kt | 318 ++++++++++++++++++ .../render/state/HorizontalPdfReaderState.kt | 51 +++ .../ui/pdf/render/state/PdfReaderState.kt | 56 +++ .../render/state/VerticalPdfReaderState.kt | 83 +++++ gradle/libs.versions.toml | 3 - 10 files changed, 840 insertions(+), 40 deletions(-) create mode 100644 core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/pdf/Notice.md create mode 100644 core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/pdf/PdfFile.kt create mode 100644 core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/pdf/render/PdfRendering.kt create mode 100644 core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/pdf/render/PdfView.kt create mode 100644 core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/pdf/render/state/HorizontalPdfReaderState.kt create mode 100644 core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/pdf/render/state/PdfReaderState.kt create mode 100644 core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/pdf/render/state/VerticalPdfReaderState.kt diff --git a/core/ui/build.gradle.kts b/core/ui/build.gradle.kts index 28399d59a..2196ddec5 100644 --- a/core/ui/build.gradle.kts +++ b/core/ui/build.gradle.kts @@ -44,5 +44,4 @@ dependencies { implementation(libs.noties.markwon.simple.ext) implementation(libs.noties.markwon.linkify) implementation(libs.noties.markwon.image.coil) - implementation(libs.girzzi91.bouquet) } \ No newline at end of file diff --git a/core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/compose/ArtemisPdfView.kt b/core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/compose/ArtemisPdfView.kt index 56c864ec1..903954c23 100644 --- a/core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/compose/ArtemisPdfView.kt +++ b/core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/compose/ArtemisPdfView.kt @@ -1,5 +1,7 @@ package de.tum.informatics.www1.artemis.native_app.core.ui.compose +import android.net.Uri +import android.widget.Toast import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -20,32 +22,15 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp -import com.rizzi.bouquet.HorizontalPDFReader -import com.rizzi.bouquet.HorizontalPdfReaderState -import com.rizzi.bouquet.ResourceType -import com.rizzi.bouquet.VerticalPDFReader -import com.rizzi.bouquet.VerticalPdfReaderState -import com.rizzi.bouquet.rememberHorizontalPdfReaderState -import com.rizzi.bouquet.rememberVerticalPdfReaderState -import io.ktor.http.HttpHeaders - -// Inspired by: https://github.com/GRizzi91/bouquet -/* - * Copyright [2022] [Graziano Rizzi] - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ +import de.tum.informatics.www1.artemis.native_app.core.ui.pdf.render.HorizontalPdfView +import de.tum.informatics.www1.artemis.native_app.core.ui.pdf.render.VerticalPdfView +import de.tum.informatics.www1.artemis.native_app.core.ui.pdf.render.state.HorizontalPdfReaderState +import de.tum.informatics.www1.artemis.native_app.core.ui.pdf.render.state.VerticalPdfReaderState +import de.tum.informatics.www1.artemis.native_app.core.ui.pdf.render.state.rememberHorizontalPdfReaderState +import de.tum.informatics.www1.artemis.native_app.core.ui.pdf.render.state.rememberVerticalPdfReaderState +import org.hildan.krossbow.stomp.ConnectionException @Composable fun ArtemisPdfView( @@ -55,22 +40,36 @@ fun ArtemisPdfView( authToken: String, ) { val isVertical = remember { mutableStateOf(true) } - val resource = ResourceType.Remote( - url = url, - headers = hashMapOf(HttpHeaders.Cookie to "jwt=$authToken") - ) val verticalPdfState = rememberVerticalPdfReaderState( - resource = resource, - isZoomEnable = true + uri = Uri.parse(url), + isZoomEnabled = true, + authToken = authToken ) val horizontalPdfState = rememberHorizontalPdfReaderState( - resource = resource, - isZoomEnable = true + uri = Uri.parse(url), + isZoomEnabled = true, + authToken = authToken ) val pdfState = if (isVertical.value) verticalPdfState else horizontalPdfState + if (pdfState.mError != null) { + when (pdfState.mError) { + is ConnectionException -> { + //TODO + } + else -> { + //TODO + } + } + Toast.makeText( + LocalContext.current, + "Error loading PDF: ${pdfState.mError?.message}", + Toast.LENGTH_LONG + ).show() + } + Box( modifier = modifier.padding(8.dp), ) { @@ -83,14 +82,14 @@ fun ArtemisPdfView( ) } if (isVertical.value) { - VerticalPDFReader( + VerticalPdfView( state = pdfState as VerticalPdfReaderState, modifier = Modifier .background(color = MaterialTheme.colorScheme.surface) .fillMaxSize() ) } else { - HorizontalPDFReader( + HorizontalPdfView( state = pdfState as HorizontalPdfReaderState, modifier = Modifier .fillMaxSize() @@ -146,7 +145,7 @@ fun ArtemisPdfView( Box( modifier = Modifier .fillMaxWidth() - .align(Alignment.TopStart).padding(top = 58.dp, end = 8.dp) + .align(Alignment.TopStart).padding(top = 54.dp, end = 4.dp) .padding(16.dp), contentAlignment = Alignment.TopEnd ) { diff --git a/core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/pdf/Notice.md b/core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/pdf/Notice.md new file mode 100644 index 000000000..912e972f6 --- /dev/null +++ b/core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/pdf/Notice.md @@ -0,0 +1,19 @@ +### Disclaimer + +The files included in this package have been heavily influenced by: + +https://github.com/GRizzi91/bouquet + +Copyright [2022] [Graziano Rizzi] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. \ No newline at end of file diff --git a/core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/pdf/PdfFile.kt b/core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/pdf/PdfFile.kt new file mode 100644 index 000000000..47fda1784 --- /dev/null +++ b/core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/pdf/PdfFile.kt @@ -0,0 +1,121 @@ +package de.tum.informatics.www1.artemis.native_app.core.ui.pdf + +import android.content.Context +import android.os.ParcelFileDescriptor +import android.util.Log +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.ImageBitmap +import androidx.compose.ui.layout.ContentScale +import de.tum.informatics.www1.artemis.native_app.core.ui.pdf.render.PdfRendering +import de.tum.informatics.www1.artemis.native_app.core.ui.pdf.render.state.PdfReaderState +import io.ktor.http.HttpHeaders +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import okhttp3.OkHttpClient +import okhttp3.Request +import java.io.File +import java.util.Date + +class PdfFile { + + private fun generateFileName(): String = "${Date().time}.pdf" + + fun load( + coroutineScope: CoroutineScope, + context: Context, + state: PdfReaderState, + width: Int, + height: Int, + authToken: String, + portrait: Boolean + ) { + val client = OkHttpClient() + val request = Request.Builder() + .url(state.uri.toString()) + .header(HttpHeaders.Cookie, "jwt=$authToken") + .build() + + runCatching { + if (state.isLoaded) { + coroutineScope.launch(Dispatchers.IO) { + runCatching { + val pFD = + ParcelFileDescriptor.open( + state.mFile, + ParcelFileDescriptor.MODE_READ_ONLY + ) + state.pdfRender = + PdfRendering(pFD, width, height, portrait) + }.onFailure { + state.mError = it + } + } + } else { + coroutineScope.launch(Dispatchers.IO) { + runCatching { + + val bufferSize = 8192 + var downloaded = 0 + val file = File(context.cacheDir, generateFileName()) + val response = client.newCall(request).execute() + if (!response.isSuccessful) { + state.mError = Exception("Failed to download PDF: ${response.code}") + Log.e("PdfView", "Failed to download PDF: ${response.code}") + } + + val byteStream = response.body?.byteStream() + byteStream.use { input -> + file.outputStream().use { output -> + val totalBytes = response.body?.contentLength() + var data = ByteArray(bufferSize) + var count = input?.read(data) + while (count != -1) { + if (totalBytes != null) { + if (totalBytes > 0) { + downloaded += bufferSize + state.mLoadPercent = + (downloaded * (100 / totalBytes.toFloat())).toInt() + } + } + if (count != null) { + output.write(data, 0, count) + } + data = ByteArray(bufferSize) + count = input?.read(data) + } + } + } + val pFD = ParcelFileDescriptor.open( + file, + ParcelFileDescriptor.MODE_READ_ONLY + ) + state.pdfRender = + PdfRendering(pFD, width, height, portrait) + state.mFile = file + }.onFailure { + state.mError = it + } + } + } + }.onFailure { + state.mError = it + } + } +} + +@Composable +internal fun PdfImage( + bitmap: () -> ImageBitmap, + contentDescription: String = "", +) { + Image( + bitmap = bitmap(), + contentDescription = contentDescription, + contentScale = ContentScale.FillWidth, + modifier = Modifier.fillMaxWidth() + ) +} \ No newline at end of file diff --git a/core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/pdf/render/PdfRendering.kt b/core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/pdf/render/PdfRendering.kt new file mode 100644 index 000000000..db32aa180 --- /dev/null +++ b/core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/pdf/render/PdfRendering.kt @@ -0,0 +1,157 @@ +package de.tum.informatics.www1.artemis.native_app.core.ui.pdf.render + +import android.graphics.Bitmap +import android.graphics.Canvas +import android.graphics.pdf.PdfRenderer +import android.os.ParcelFileDescriptor +import androidx.core.graphics.createBitmap +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancelAndJoin +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.launch +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock + +internal class PdfRendering( + private val fileDescriptor: ParcelFileDescriptor, + val width: Int, + val height: Int, + private val portrait: Boolean +) { + private val pdfRenderer = PdfRenderer(fileDescriptor) + val pageCount get() = pdfRenderer.pageCount + private val mutex: Mutex = Mutex() + private val coroutineScope = CoroutineScope(Dispatchers.Default + SupervisorJob()) + + val pageLists: List = List(pdfRenderer.pageCount) { + Page( + mutex = mutex, + index = it, + pdfRenderer = pdfRenderer, + coroutineScope = coroutineScope, + width = width, + height = height, + portrait = portrait + ) + } + + fun close() { + coroutineScope.launch { + pageLists.forEach { + it.job?.cancelAndJoin() + it.recycle() + } + pdfRenderer.close() + fileDescriptor.close() + } + } + + class Page( + val mutex: Mutex, + val index: Int, + val pdfRenderer: PdfRenderer, + val coroutineScope: CoroutineScope, + width: Int, + height: Int, + portrait: Boolean + ) { + val dimension = pdfRenderer.openPage(index).use { + if (portrait) { + val h = it.height * (width.toFloat() / it.width) + val dim = Dimension( + height = h.toInt(), + width = width + ) + dim + } else { + val w = it.width * (height.toFloat() / it.height) + val dim = Dimension( + height = height, + width = w.toInt() + ) + dim + } + } + + var job: Job? = null + + val stateFlow = MutableStateFlow( + PageContentInt.BlankPage( + width = dimension.width, + height = dimension.height + ) + ) + + private var isLoaded = false + + fun load() { + if (!isLoaded) { + job = coroutineScope.launch { + mutex.withLock { + val newBitmap: Bitmap + pdfRenderer.openPage(index).use { currentPage -> + newBitmap = createBlankBitmap( + width = dimension.width, + height = dimension.height + ) + currentPage.render( + newBitmap, + null, + null, + PdfRenderer.Page.RENDER_MODE_FOR_DISPLAY + ) + } + isLoaded = true + stateFlow.emit(PageContentInt.PageContent(newBitmap)) + } + } + } + } + + fun recycle() { + isLoaded = false + val oldBitmap = stateFlow.value as? PageContentInt.PageContent + stateFlow.tryEmit( + PageContentInt.BlankPage( + width = dimension.width, + height = dimension.height + ) + ) + oldBitmap?.bitmap?.recycle() + } + + private fun createBlankBitmap( + width: Int, + height: Int + ): Bitmap { + return createBitmap( + width, + height, + Bitmap.Config.ARGB_8888 + ).apply { + val canvas = Canvas(this) + canvas.drawColor(android.graphics.Color.WHITE) + canvas.drawBitmap(this, 0f, 0f, null) + } + } + + data class Dimension( + val height: Int, + val width: Int + ) + } +} + +sealed interface PageContentInt { + data class PageContent( + val bitmap: Bitmap + ) : PageContentInt + + data class BlankPage( + val width: Int, + val height: Int + ) : PageContentInt +} \ No newline at end of file diff --git a/core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/pdf/render/PdfView.kt b/core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/pdf/render/PdfView.kt new file mode 100644 index 000000000..c16075496 --- /dev/null +++ b/core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/pdf/render/PdfView.kt @@ -0,0 +1,318 @@ +package de.tum.informatics.www1.artemis.native_app.core.ui.pdf.render + +import androidx.compose.foundation.background +import androidx.compose.foundation.gestures.detectTapGestures +import androidx.compose.foundation.gestures.detectTransformGestures +import androidx.compose.foundation.gestures.scrollBy +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxWithConstraints +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.pager.HorizontalPager +import androidx.compose.foundation.pager.rememberPagerState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.snapshotFlow +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.composed +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.asImageBitmap +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.debugInspectorInfo +import androidx.compose.ui.unit.Constraints +import androidx.compose.ui.unit.dp +import de.tum.informatics.www1.artemis.native_app.core.ui.pdf.PdfFile +import de.tum.informatics.www1.artemis.native_app.core.ui.pdf.PdfImage +import de.tum.informatics.www1.artemis.native_app.core.ui.pdf.render.state.HorizontalPdfReaderState +import de.tum.informatics.www1.artemis.native_app.core.ui.pdf.render.state.VerticalPdfReaderState +import kotlinx.coroutines.launch + +@Composable +fun VerticalPdfView( + state: VerticalPdfReaderState, + modifier: Modifier +) { + BoxWithConstraints( + modifier = modifier, + contentAlignment = Alignment.TopCenter + ) { + val ctx = LocalContext.current + val coroutineScope = rememberCoroutineScope() + val lazyState = state.lazyState + DisposableEffect(key1 = Unit) { + PdfFile().load( + coroutineScope, + ctx, + state, + constraints.maxWidth, + constraints.maxHeight, + authToken = state.authToken, + true + ) + onDispose { + state.close() + } + } + state.pdfRender?.let { pdf -> + LazyColumn( + modifier = Modifier + .fillMaxSize() + .tapToZoomVertical(state, constraints), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(16.dp), + state = lazyState + ) { + items(pdf.pageCount) { + val pageContent = pdf.pageLists[it].stateFlow.collectAsState().value + DisposableEffect(key1 = Unit) { + pdf.pageLists[it].load() + onDispose { + // This caused issues and has therefore been temporarily disabled + //pdf.pageLists[it].recycle() + } + } + when (pageContent) { + is PageContentInt.PageContent -> { + PdfImage( + bitmap = { pageContent.bitmap.asImageBitmap() } + ) + } + + is PageContentInt.BlankPage -> BlackPage( + width = pageContent.width, + height = pageContent.height + ) + } + } + } + } + } +} + +@Composable +fun HorizontalPdfView( + state: HorizontalPdfReaderState, + modifier: Modifier +) { + BoxWithConstraints( + modifier = modifier, + contentAlignment = Alignment.TopCenter + ) { + val ctx = LocalContext.current + val coroutineScope = rememberCoroutineScope() + DisposableEffect(key1 = Unit) { + PdfFile().load( + coroutineScope, + ctx, + state, + constraints.maxWidth, + constraints.maxHeight, + authToken = state.authToken, + constraints.maxHeight > constraints.maxWidth + ) + onDispose { + state.close() + } + } + val pagerState = rememberPagerState( state.currentPage ) { state.pdfPageCount } + + state.pdfRender?.let { pdf -> + HorizontalPager( + modifier = Modifier + .fillMaxSize() + .tapToZoomHorizontal(state, constraints), + state = pagerState, + userScrollEnabled = state.scale == 1f + ) { page -> + val pageContent = pdf.pageLists[page].stateFlow.collectAsState().value + + LaunchedEffect(pagerState) { + snapshotFlow { pagerState.currentPage }.collect { page -> + state.currentPage = page + } + snapshotFlow { pagerState.isScrollInProgress }.collect { isScrolling -> + state.isScrolling = isScrolling + } + } + + DisposableEffect(key1 = Unit) { + pdf.pageLists[page].load() + onDispose { + pdf.pageLists[page].recycle() + } + } + when (pageContent) { + is PageContentInt.PageContent -> { + PdfImage( + bitmap = { pageContent.bitmap.asImageBitmap() } + ) + } + + is PageContentInt.BlankPage -> BlackPage( + width = pageContent.width, + height = pageContent.height + ) + } + } + } + } +} + +fun Modifier.tapToZoomVertical( + state: VerticalPdfReaderState, + constraints: Constraints +): Modifier = composed( + inspectorInfo = debugInspectorInfo { + name = "verticalTapToZoom" + properties["state"] = state + } +) { + val coroutineScope = rememberCoroutineScope() + this + .pointerInput(Unit) { + detectTapGestures( + onDoubleTap = { tapCenter -> + if (!state.isZoomEnabled) return@detectTapGestures + if (state.mScale > 1.0f) { + state.mScale = 1.0f + state.offset = Offset(0f, 0f) + } else { + state.mScale = 3.0f + val center = Pair(constraints.maxWidth / 2, constraints.maxHeight / 2) + val xDiff = (tapCenter.x - center.first) * state.scale + val yDiff = ((tapCenter.y - center.second) * state.scale).coerceIn( + minimumValue = -(center.second * 2f), + maximumValue = (center.second * 2f) + ) + state.offset = Offset(-xDiff, -yDiff) + } + } + ) + } + .pointerInput(Unit) { + detectTransformGestures(true) { centroid, pan, zoom, rotation -> + val pair = if (pan.y > 0) { + if (state.lazyState.canScrollBackward) { + Pair(0f, pan.y) + } else { + Pair(pan.y, 0f) + } + } else { + if (state.lazyState.canScrollForward) { + Pair(0f, pan.y) + } else { + Pair(pan.y, 0f) + } + } + val nOffset = if (state.scale > 1f) { + val maxT = (constraints.maxWidth * state.scale) - constraints.maxWidth + val maxY = (constraints.maxHeight * state.scale) - constraints.maxHeight + Offset( + x = (state.offset.x + pan.x).coerceIn( + minimumValue = (-maxT / 2) * 1.3f, + maximumValue = (maxT / 2) * 1.3f + ), + y = (state.offset.y + pair.first).coerceIn( + minimumValue = (-maxY / 2), + maximumValue = (maxY / 2) + ) + ) + } else { + Offset(0f, 0f) + } + state.offset = nOffset + coroutineScope.launch { + state.lazyState.scrollBy((-pair.second / state.scale)) + } + } + } + .graphicsLayer { + scaleX = state.scale + scaleY = state.scale + translationX = state.offset.x + translationY = state.offset.y + } +} + +fun Modifier.tapToZoomHorizontal( + state: HorizontalPdfReaderState, + constraints: Constraints +): Modifier = composed( + inspectorInfo = debugInspectorInfo { + name = "horizontalTapToZoom" + properties["state"] = state + } +) { + this + .pointerInput(Unit) { + detectTapGestures( + onDoubleTap = { tapCenter -> + if (!state.isZoomEnabled) return@detectTapGestures + if (state.mScale > 1.0f) { + state.mScale = 1.0f + state.offset = Offset(0f, 0f) + } else { + state.mScale = 3.0f + val center = Pair(constraints.maxWidth / 2, constraints.maxHeight / 2) + val xDiff = (tapCenter.x - center.first) * state.scale + val yDiff = ((tapCenter.y - center.second) * state.scale).coerceIn( + minimumValue = -(center.second * 2f), + maximumValue = (center.second * 2f) + ) + state.offset = Offset(-xDiff, -yDiff) + } + } + ) + } + .pointerInput(Unit) { + detectTransformGestures(true) { centroid, pan, zoom, rotation -> + val nOffset = if (state.scale > 1f) { + val maxT = (constraints.maxWidth * state.scale) - constraints.maxWidth + val maxY = (constraints.maxHeight * state.scale) - constraints.maxHeight + Offset( + x = (state.offset.x + pan.x).coerceIn( + minimumValue = (-maxT / 2) * 1.3f, + maximumValue = (maxT / 2) * 1.3f + ), + y = (state.offset.y + pan.y).coerceIn( + minimumValue = (-maxY / 2) * 1.3f, + maximumValue = (maxY / 2) * 1.3f + ) + ) + } else { + Offset(0f, 0f) + } + state.offset = nOffset + } + } + .graphicsLayer { + scaleX = state.scale + scaleY = state.scale + translationX = state.offset.x + translationY = state.offset.y + } +} + +@Composable +fun BlackPage( + width: Int, + height: Int +) { + Box( + modifier = Modifier + .size( + width = width.dp, + height = height.dp + ) + .background(color = Color.White) + ) +} \ No newline at end of file diff --git a/core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/pdf/render/state/HorizontalPdfReaderState.kt b/core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/pdf/render/state/HorizontalPdfReaderState.kt new file mode 100644 index 000000000..3e6420525 --- /dev/null +++ b/core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/pdf/render/state/HorizontalPdfReaderState.kt @@ -0,0 +1,51 @@ +package de.tum.informatics.www1.artemis.native_app.core.ui.pdf.render.state + +import android.net.Uri +import androidx.compose.runtime.Composable +import androidx.compose.runtime.saveable.Saver +import androidx.compose.runtime.saveable.listSaver +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.core.net.toUri + +class HorizontalPdfReaderState( + uri: Uri, + authToken: String, + isZoomEnabled: Boolean = false +) : PdfReaderState(uri, authToken, isZoomEnabled) { + + companion object { + val Saver: Saver = listSaver( + save = { state -> + listOf( + state.file?.toUri() ?: state.uri, + state.authToken, + state.isZoomEnabled, + state.currentPage + ) + }, + restore = { restoredList -> + val uri = restoredList[0] as Uri + val authToken = restoredList[1] as String + val isZoomEnabled = restoredList[2] as Boolean + + HorizontalPdfReaderState( + uri = uri, + authToken = authToken, + isZoomEnabled = isZoomEnabled + ) + } + ) + } +} + +@Composable +fun rememberHorizontalPdfReaderState( + uri: Uri, + authToken: String, + isZoomEnabled: Boolean = true, +): HorizontalPdfReaderState { + return rememberSaveable(saver = HorizontalPdfReaderState.Saver) { + HorizontalPdfReaderState(uri, authToken, isZoomEnabled) + } +} + diff --git a/core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/pdf/render/state/PdfReaderState.kt b/core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/pdf/render/state/PdfReaderState.kt new file mode 100644 index 000000000..1d9daa831 --- /dev/null +++ b/core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/pdf/render/state/PdfReaderState.kt @@ -0,0 +1,56 @@ +package de.tum.informatics.www1.artemis.native_app.core.ui.pdf.render.state + +import android.net.Uri +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableFloatStateOf +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.compose.ui.geometry.Offset +import de.tum.informatics.www1.artemis.native_app.core.ui.pdf.render.PdfRendering +import java.io.File + +abstract class PdfReaderState( + val uri: Uri, + val authToken: String, + isZoomEnabled: Boolean = false +) { + internal var mError by mutableStateOf(null) + val error: Throwable? + get() = mError + + private var mIsZoomEnable by mutableStateOf(isZoomEnabled) + val isZoomEnabled: Boolean + get() = mIsZoomEnable + + internal var mScale by mutableFloatStateOf(1f) + val scale: Float + get() = mScale + + internal var offset by mutableStateOf(Offset(0f, 0f)) + + internal var mFile by mutableStateOf(null) + val file: File? + get() = mFile + + internal var pdfRender by mutableStateOf(null) + + internal var mLoadPercent by mutableIntStateOf(0) + val loadPercent: Int + get() = mLoadPercent + + val pdfPageCount: Int + get() = pdfRender?.pageCount ?: 0 + + open var currentPage by mutableIntStateOf(1) + + val isLoaded + get() = mFile != null + + open var isScrolling by mutableStateOf(false) + + fun close() { + pdfRender?.close() + pdfRender = null + } +} \ No newline at end of file diff --git a/core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/pdf/render/state/VerticalPdfReaderState.kt b/core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/pdf/render/state/VerticalPdfReaderState.kt new file mode 100644 index 000000000..5f2c41403 --- /dev/null +++ b/core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/pdf/render/state/VerticalPdfReaderState.kt @@ -0,0 +1,83 @@ +package de.tum.informatics.www1.artemis.native_app.core.ui.pdf.render.state + +import android.net.Uri +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.saveable.Saver +import androidx.compose.runtime.saveable.listSaver +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.core.net.toUri + +class VerticalPdfReaderState( + uri: Uri, + authToken: String, + isZoomEnabled: Boolean = false, +) : PdfReaderState(uri, authToken, isZoomEnabled) { + + internal var lazyState: LazyListState = LazyListState() + private set + + override var currentPage: Int = currentPage() + get() = currentPage() + + override var isScrolling: Boolean = lazyState.isScrollInProgress + + private fun currentPage(): Int { + return pdfRender?.let { pdfRender -> + val currentMinIndex = lazyState.firstVisibleItemIndex + var lastVisibleIndex = currentMinIndex + var totalVisiblePortion = + (pdfRender.pageLists[currentMinIndex].dimension.height * scale) - lazyState.firstVisibleItemScrollOffset + for (i in currentMinIndex + 1 until pdfPageCount) { + val newTotalVisiblePortion = + totalVisiblePortion + (pdfRender.pageLists[i].dimension.height * scale) + if (newTotalVisiblePortion <= pdfRender.height) { + lastVisibleIndex = i + totalVisiblePortion = newTotalVisiblePortion + } else { + break + } + } + lastVisibleIndex + 1 + } ?: 0 + } + + companion object { + val Saver: Saver = listSaver( + save = { + val resourceUri = it.file?.toUri() ?: it.uri + val authToken = it.authToken + listOf( + resourceUri, + authToken, + it.isZoomEnabled, + it.lazyState.firstVisibleItemIndex, + it.lazyState.firstVisibleItemScrollOffset + ) + }, + restore = { + VerticalPdfReaderState( + it[0] as Uri, + it[1] as String, + it[2] as Boolean, + ).apply { + lazyState = LazyListState( + firstVisibleItemIndex = it[3] as Int, + firstVisibleItemScrollOffset = it[4] as Int + ) + } + } + ) + } +} + +@Composable +fun rememberVerticalPdfReaderState( + uri: Uri, + authToken: String, + isZoomEnabled: Boolean = true, +): VerticalPdfReaderState { + return rememberSaveable(saver = VerticalPdfReaderState.Saver) { + VerticalPdfReaderState(uri, authToken, isZoomEnabled) + } +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 47efcff5c..57972ba76 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -34,7 +34,6 @@ placeholderMaterial = "2.0.0" room = "2.6.1" sentry-android = "7.19.0" work = "2.10.0" -composeMaterial3 = "1.0.0-alpha30" [libraries] accompanist-placeholder-material = { group = "com.google.accompanist", name = "accompanist-placeholder-material", version.ref = "accompanist" } @@ -85,7 +84,6 @@ coil-network = { group = "io.coil-kt.coil3", name = "coil-network-okhttp", versi coil2-base = { group = "io.coil-kt", name = "coil-base", version.ref = "coil2" } google-firebase-bom = { group = "com.google.firebase", name = "firebase-bom", version = "33.7.0" } google-firebase-messaging = { group = "com.google.firebase", name = "firebase-messaging" } -girzzi91-bouquet = { group = "io.github.grizzi91", name = "bouquet", version = "1.1.2"} kotlinx-coroutines-android = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-android", version.ref = "kotlinxCoroutines" } kotlinx-coroutines-core = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-core", version.ref = "kotlinxCoroutines" } kotlinx-coroutines-core-jvm = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-core-jvm", version.ref = "kotlinxCoroutines" } @@ -133,7 +131,6 @@ toolbar-compose = { group = "me.onebone", name = "toolbar-compose", version = "2 android-gradlePlugin = { group = "com.android.tools.build", name = "gradle", version.ref = "androidGradlePlugin" } kotlin-gradlePlugin = { group = "org.jetbrains.kotlin", name = "kotlin-gradle-plugin", version.ref = "kotlin" } kotlin-kover = { group = "org.jetbrains.kotlinx", name = "kover-gradle-plugin", version.ref = "kover" } -compose-material3 = { group = "androidx.wear.compose", name = "compose-material3", version.ref = "composeMaterial3" } [plugins] android-application = { id = "com.android.application", version.ref = "androidGradlePlugin" } From d477d35b98ae77a906358422feea3cca4b5127d4 Mon Sep 17 00:00:00 2001 From: Julian Waluschyk Date: Thu, 19 Dec 2024 17:11:19 +0100 Subject: [PATCH 05/12] minor changes --- .../www1/artemis/native_app/core/ui/pdf/render/PdfView.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/pdf/render/PdfView.kt b/core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/pdf/render/PdfView.kt index c16075496..80797842c 100644 --- a/core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/pdf/render/PdfView.kt +++ b/core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/pdf/render/PdfView.kt @@ -123,7 +123,7 @@ fun HorizontalPdfView( state.close() } } - val pagerState = rememberPagerState( state.currentPage ) { state.pdfPageCount } + val pagerState = rememberPagerState( state.currentPage - 1 ) { state.pdfPageCount } state.pdfRender?.let { pdf -> HorizontalPager( @@ -137,7 +137,7 @@ fun HorizontalPdfView( LaunchedEffect(pagerState) { snapshotFlow { pagerState.currentPage }.collect { page -> - state.currentPage = page + state.currentPage = page + 1 } snapshotFlow { pagerState.isScrollInProgress }.collect { isScrolling -> state.isScrolling = isScrolling From 83347209faa8d30de675cc793c1a32133cb681e7 Mon Sep 17 00:00:00 2001 From: Julian Waluschyk Date: Thu, 19 Dec 2024 20:58:49 +0100 Subject: [PATCH 06/12] added pdf sharing and downloading, added Dropdown Menu and introduced strings for pdf view --- core/ui/src/main/AndroidManifest.xml | 13 +++ .../core/ui/compose/ArtemisPdfView.kt | 110 +++++++++++++----- .../artemis/native_app/core/ui/pdf/PdfFile.kt | 79 ++++++++++++- .../native_app/core/ui/pdf/render/PdfView.kt | 11 +- .../render/state/HorizontalPdfReaderState.kt | 24 ++-- .../ui/pdf/render/state/PdfReaderState.kt | 5 +- .../render/state/VerticalPdfReaderState.kt | 39 +++---- .../main/res/values/pdf_viewer_strings.xml | 15 +++ core/ui/src/main/res/xml/file_paths.xml | 4 + 9 files changed, 223 insertions(+), 77 deletions(-) create mode 100644 core/ui/src/main/AndroidManifest.xml create mode 100644 core/ui/src/main/res/values/pdf_viewer_strings.xml create mode 100644 core/ui/src/main/res/xml/file_paths.xml diff --git a/core/ui/src/main/AndroidManifest.xml b/core/ui/src/main/AndroidManifest.xml new file mode 100644 index 000000000..0fdec3e1a --- /dev/null +++ b/core/ui/src/main/AndroidManifest.xml @@ -0,0 +1,13 @@ + + + + + + + \ No newline at end of file diff --git a/core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/compose/ArtemisPdfView.kt b/core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/compose/ArtemisPdfView.kt index 903954c23..c1a726985 100644 --- a/core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/compose/ArtemisPdfView.kt +++ b/core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/compose/ArtemisPdfView.kt @@ -1,10 +1,10 @@ package de.tum.informatics.www1.artemis.native_app.core.ui.compose -import android.net.Uri import android.widget.Toast import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height @@ -12,7 +12,12 @@ import androidx.compose.foundation.layout.padding import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.RotateLeft import androidx.compose.material.icons.automirrored.filled.RotateRight +import androidx.compose.material.icons.filled.Download +import androidx.compose.material.icons.filled.MoreHoriz +import androidx.compose.material.icons.filled.Share import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.DropdownMenu +import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.FloatingActionButton import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme @@ -23,7 +28,10 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp +import de.tum.informatics.www1.artemis.native_app.core.ui.R +import de.tum.informatics.www1.artemis.native_app.core.ui.pdf.PdfFile import de.tum.informatics.www1.artemis.native_app.core.ui.pdf.render.HorizontalPdfView import de.tum.informatics.www1.artemis.native_app.core.ui.pdf.render.VerticalPdfView import de.tum.informatics.www1.artemis.native_app.core.ui.pdf.render.state.HorizontalPdfReaderState @@ -40,32 +48,35 @@ fun ArtemisPdfView( authToken: String, ) { val isVertical = remember { mutableStateOf(true) } + val context = LocalContext.current + val pdfFile = PdfFile(url, authToken, filename) val verticalPdfState = rememberVerticalPdfReaderState( - uri = Uri.parse(url), + pdfFile = pdfFile, isZoomEnabled = true, - authToken = authToken ) val horizontalPdfState = rememberHorizontalPdfReaderState( - uri = Uri.parse(url), + pdfFile = pdfFile, isZoomEnabled = true, - authToken = authToken ) val pdfState = if (isVertical.value) verticalPdfState else horizontalPdfState + val showMenu = remember { mutableStateOf(false) } + if (pdfState.mError != null) { when (pdfState.mError) { is ConnectionException -> { //TODO } + else -> { //TODO } } Toast.makeText( LocalContext.current, - "Error loading PDF: ${pdfState.mError?.message}", + stringResource(id = R.string.pdf_view_error_loading), Toast.LENGTH_LONG ).show() } @@ -101,32 +112,68 @@ fun ArtemisPdfView( } } - FloatingActionButton( - onClick = { - isVertical.value = !isVertical.value - }, + Column( modifier = Modifier .align(Alignment.BottomEnd) .padding(16.dp) ) { - Icon( - imageVector = if (isVertical.value) Icons.AutoMirrored.Filled.RotateLeft else Icons.AutoMirrored.Filled.RotateRight, - contentDescription = null - ) - } + if (showMenu.value) { + Spacer(modifier = Modifier.height(16.dp)) + } -// FloatingActionButton( -// onClick = { /* Share logic */ }, -// modifier = Modifier -// .align(Alignment.BottomEnd) -// .zIndex(1f) -// .padding(16.dp) -// ) { -// Icon( -// imageVector = Icons.Default.Share, -// contentDescription = null -// ) -// } + FloatingActionButton( + onClick = { showMenu.value = !showMenu.value }, + modifier = Modifier + ) { + Icon(imageVector = Icons.Default.MoreHoriz, contentDescription = null) + } + + DropdownMenu( + modifier = Modifier, + expanded = showMenu.value, + onDismissRequest = { showMenu.value = false } + ) { + DropdownMenuItem( + leadingIcon = { + Icon( + imageVector = Icons.Default.Download, + contentDescription = null + ) + }, + text = { Text(stringResource(R.string.pdf_view_download_menu_item)) }, + onClick = { + showMenu.value = false + verticalPdfState.file?.let { pdfFile.downloadPdf(context) } + } + ) + DropdownMenuItem( + leadingIcon = { + Icon( + imageVector = Icons.Default.Share, + contentDescription = null + ) + }, + text = { Text(stringResource(R.string.pdf_view_share_menu_item)) }, + onClick = { + showMenu.value = false + verticalPdfState.file?.let { pdfFile.sharePdf(context, it) } + } + ) + DropdownMenuItem( + leadingIcon = { + Icon( + imageVector = if (isVertical.value) Icons.AutoMirrored.Filled.RotateLeft else Icons.AutoMirrored.Filled.RotateRight, + contentDescription = null + ) + }, + text = { Text(stringResource(R.string.pdf_view_rotate_menu_item)) }, + onClick = { + showMenu.value = false + isVertical.value = !isVertical.value + } + ) + } + } if (!pdfState.isLoaded) { Box( @@ -145,7 +192,8 @@ fun ArtemisPdfView( Box( modifier = Modifier .fillMaxWidth() - .align(Alignment.TopStart).padding(top = 54.dp, end = 4.dp) + .align(Alignment.TopStart) + .padding(top = 54.dp, end = 4.dp) .padding(16.dp), contentAlignment = Alignment.TopEnd ) { @@ -158,7 +206,11 @@ fun ArtemisPdfView( .padding(8.dp) ) { Text( - text = "Page: ${pdfState.currentPage}/${pdfState.pdfPageCount}", + text = stringResource( + R.string.pdf_view_page_count, + pdfState.currentPage, + pdfState.pdfPageCount + ), style = MaterialTheme.typography.bodyMedium ) } diff --git a/core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/pdf/PdfFile.kt b/core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/pdf/PdfFile.kt index 47fda1784..253fee733 100644 --- a/core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/pdf/PdfFile.kt +++ b/core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/pdf/PdfFile.kt @@ -1,14 +1,23 @@ package de.tum.informatics.www1.artemis.native_app.core.ui.pdf +import android.app.DownloadManager +import android.content.ClipData import android.content.Context +import android.content.Intent +import android.net.Uri +import android.os.Environment import android.os.ParcelFileDescriptor import android.util.Log +import android.widget.Toast import androidx.compose.foundation.Image import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.ImageBitmap import androidx.compose.ui.layout.ContentScale +import androidx.core.content.ContextCompat.getString +import androidx.core.content.FileProvider +import de.tum.informatics.www1.artemis.native_app.core.ui.R import de.tum.informatics.www1.artemis.native_app.core.ui.pdf.render.PdfRendering import de.tum.informatics.www1.artemis.native_app.core.ui.pdf.render.state.PdfReaderState import io.ktor.http.HttpHeaders @@ -20,7 +29,12 @@ import okhttp3.Request import java.io.File import java.util.Date -class PdfFile { + +class PdfFile( + val link: String, + val authToken: String, + val filename: String? = null +) { private fun generateFileName(): String = "${Date().time}.pdf" @@ -30,12 +44,11 @@ class PdfFile { state: PdfReaderState, width: Int, height: Int, - authToken: String, portrait: Boolean ) { val client = OkHttpClient() val request = Request.Builder() - .url(state.uri.toString()) + .url(link) .header(HttpHeaders.Cookie, "jwt=$authToken") .build() @@ -60,7 +73,7 @@ class PdfFile { val bufferSize = 8192 var downloaded = 0 - val file = File(context.cacheDir, generateFileName()) + val file = File(context.cacheDir, filename ?: generateFileName()) val response = client.newCall(request).execute() if (!response.isSuccessful) { state.mError = Exception("Failed to download PDF: ${response.code}") @@ -105,6 +118,64 @@ class PdfFile { state.mError = it } } + + fun downloadPdf(context: Context) { + try { + val downloadManager: DownloadManager = + context.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager + val downloadUri = Uri.parse(link) + + downloadManager + .enqueue( + DownloadManager.Request(downloadUri) + .addRequestHeader(HttpHeaders.Cookie, "jwt=$authToken") + .setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED) + .setTitle( + filename ?: generateFileName() + ) + .setDestinationInExternalPublicDir( + Environment.DIRECTORY_DOWNLOADS, + downloadUri.lastPathSegment + ) + ) + + Toast.makeText( + context, + getString(context, R.string.pdf_view_downloading_toast), + Toast.LENGTH_SHORT + ).show() + + } catch (e: Exception) { + Toast.makeText(context, "Error downloading file: ${e.message}", Toast.LENGTH_LONG) + .show() + e.printStackTrace() + } + } + + fun sharePdf(context: Context, pdfFile: File) { + val pdfUri = FileProvider.getUriForFile( + context, + "${context.packageName}.fileprovider", + pdfFile + ) + + val shareIntent = Intent(Intent.ACTION_SEND).apply { + type = "application/pdf" + clipData = ClipData( + "pdf", + arrayOf("application/pdf"), + ClipData.Item(pdfUri) + ) + putExtra(Intent.EXTRA_STREAM, pdfUri) // to support sharing to older applications + addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + } + context.startActivity( + Intent.createChooser( + shareIntent, + getString(context, R.string.pdf_view_share_title) + ) + ) + } } @Composable diff --git a/core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/pdf/render/PdfView.kt b/core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/pdf/render/PdfView.kt index 80797842c..49996424a 100644 --- a/core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/pdf/render/PdfView.kt +++ b/core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/pdf/render/PdfView.kt @@ -30,7 +30,6 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.debugInspectorInfo import androidx.compose.ui.unit.Constraints import androidx.compose.ui.unit.dp -import de.tum.informatics.www1.artemis.native_app.core.ui.pdf.PdfFile import de.tum.informatics.www1.artemis.native_app.core.ui.pdf.PdfImage import de.tum.informatics.www1.artemis.native_app.core.ui.pdf.render.state.HorizontalPdfReaderState import de.tum.informatics.www1.artemis.native_app.core.ui.pdf.render.state.VerticalPdfReaderState @@ -48,14 +47,15 @@ fun VerticalPdfView( val ctx = LocalContext.current val coroutineScope = rememberCoroutineScope() val lazyState = state.lazyState + val pdfFile = state.pdfFile + DisposableEffect(key1 = Unit) { - PdfFile().load( + pdfFile.load( coroutineScope, ctx, state, constraints.maxWidth, constraints.maxHeight, - authToken = state.authToken, true ) onDispose { @@ -109,14 +109,15 @@ fun HorizontalPdfView( ) { val ctx = LocalContext.current val coroutineScope = rememberCoroutineScope() + val pdfFile = state.pdfFile + DisposableEffect(key1 = Unit) { - PdfFile().load( + pdfFile.load( coroutineScope, ctx, state, constraints.maxWidth, constraints.maxHeight, - authToken = state.authToken, constraints.maxHeight > constraints.maxWidth ) onDispose { diff --git a/core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/pdf/render/state/HorizontalPdfReaderState.kt b/core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/pdf/render/state/HorizontalPdfReaderState.kt index 3e6420525..4ef90981b 100644 --- a/core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/pdf/render/state/HorizontalPdfReaderState.kt +++ b/core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/pdf/render/state/HorizontalPdfReaderState.kt @@ -1,36 +1,33 @@ package de.tum.informatics.www1.artemis.native_app.core.ui.pdf.render.state -import android.net.Uri import androidx.compose.runtime.Composable import androidx.compose.runtime.saveable.Saver import androidx.compose.runtime.saveable.listSaver import androidx.compose.runtime.saveable.rememberSaveable -import androidx.core.net.toUri +import de.tum.informatics.www1.artemis.native_app.core.ui.pdf.PdfFile class HorizontalPdfReaderState( - uri: Uri, - authToken: String, + pdfFile: PdfFile, isZoomEnabled: Boolean = false -) : PdfReaderState(uri, authToken, isZoomEnabled) { +) : PdfReaderState(pdfFile, isZoomEnabled) { companion object { val Saver: Saver = listSaver( save = { state -> listOf( - state.file?.toUri() ?: state.uri, - state.authToken, + state.pdfFile.link, + state.pdfFile.authToken, + state.pdfFile.filename, state.isZoomEnabled, state.currentPage ) }, restore = { restoredList -> - val uri = restoredList[0] as Uri - val authToken = restoredList[1] as String + val pdfFile = PdfFile(restoredList[0] as String, restoredList[1] as String, restoredList[2] as String?) val isZoomEnabled = restoredList[2] as Boolean HorizontalPdfReaderState( - uri = uri, - authToken = authToken, + pdfFile = pdfFile, isZoomEnabled = isZoomEnabled ) } @@ -40,12 +37,11 @@ class HorizontalPdfReaderState( @Composable fun rememberHorizontalPdfReaderState( - uri: Uri, - authToken: String, + pdfFile: PdfFile, isZoomEnabled: Boolean = true, ): HorizontalPdfReaderState { return rememberSaveable(saver = HorizontalPdfReaderState.Saver) { - HorizontalPdfReaderState(uri, authToken, isZoomEnabled) + HorizontalPdfReaderState(pdfFile, isZoomEnabled) } } diff --git a/core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/pdf/render/state/PdfReaderState.kt b/core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/pdf/render/state/PdfReaderState.kt index 1d9daa831..ec8cd90b9 100644 --- a/core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/pdf/render/state/PdfReaderState.kt +++ b/core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/pdf/render/state/PdfReaderState.kt @@ -1,18 +1,17 @@ package de.tum.informatics.www1.artemis.native_app.core.ui.pdf.render.state -import android.net.Uri import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableFloatStateOf import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.compose.ui.geometry.Offset +import de.tum.informatics.www1.artemis.native_app.core.ui.pdf.PdfFile import de.tum.informatics.www1.artemis.native_app.core.ui.pdf.render.PdfRendering import java.io.File abstract class PdfReaderState( - val uri: Uri, - val authToken: String, + val pdfFile: PdfFile, isZoomEnabled: Boolean = false ) { internal var mError by mutableStateOf(null) diff --git a/core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/pdf/render/state/VerticalPdfReaderState.kt b/core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/pdf/render/state/VerticalPdfReaderState.kt index 5f2c41403..c63c38a3e 100644 --- a/core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/pdf/render/state/VerticalPdfReaderState.kt +++ b/core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/pdf/render/state/VerticalPdfReaderState.kt @@ -1,18 +1,16 @@ package de.tum.informatics.www1.artemis.native_app.core.ui.pdf.render.state -import android.net.Uri import androidx.compose.foundation.lazy.LazyListState import androidx.compose.runtime.Composable import androidx.compose.runtime.saveable.Saver import androidx.compose.runtime.saveable.listSaver import androidx.compose.runtime.saveable.rememberSaveable -import androidx.core.net.toUri +import de.tum.informatics.www1.artemis.native_app.core.ui.pdf.PdfFile class VerticalPdfReaderState( - uri: Uri, - authToken: String, + pdfFile: PdfFile, isZoomEnabled: Boolean = false, -) : PdfReaderState(uri, authToken, isZoomEnabled) { +) : PdfReaderState(pdfFile, isZoomEnabled) { internal var lazyState: LazyListState = LazyListState() private set @@ -44,26 +42,24 @@ class VerticalPdfReaderState( companion object { val Saver: Saver = listSaver( - save = { - val resourceUri = it.file?.toUri() ?: it.uri - val authToken = it.authToken + save = { state -> listOf( - resourceUri, - authToken, - it.isZoomEnabled, - it.lazyState.firstVisibleItemIndex, - it.lazyState.firstVisibleItemScrollOffset + state.pdfFile.link, + state.pdfFile.authToken, + state.pdfFile.filename, + state.isZoomEnabled, + state.lazyState.firstVisibleItemIndex, + state.lazyState.firstVisibleItemScrollOffset ) }, - restore = { + restore = { restoredList -> VerticalPdfReaderState( - it[0] as Uri, - it[1] as String, - it[2] as Boolean, + PdfFile(restoredList[0] as String, restoredList[1] as String, restoredList[2] as String?), + restoredList[1] as Boolean, ).apply { lazyState = LazyListState( - firstVisibleItemIndex = it[3] as Int, - firstVisibleItemScrollOffset = it[4] as Int + firstVisibleItemIndex = restoredList[2] as Int, + firstVisibleItemScrollOffset = restoredList[3] as Int ) } } @@ -73,11 +69,10 @@ class VerticalPdfReaderState( @Composable fun rememberVerticalPdfReaderState( - uri: Uri, - authToken: String, + pdfFile: PdfFile, isZoomEnabled: Boolean = true, ): VerticalPdfReaderState { return rememberSaveable(saver = VerticalPdfReaderState.Saver) { - VerticalPdfReaderState(uri, authToken, isZoomEnabled) + VerticalPdfReaderState(pdfFile, isZoomEnabled) } } diff --git a/core/ui/src/main/res/values/pdf_viewer_strings.xml b/core/ui/src/main/res/values/pdf_viewer_strings.xml new file mode 100644 index 000000000..9a72b1e1f --- /dev/null +++ b/core/ui/src/main/res/values/pdf_viewer_strings.xml @@ -0,0 +1,15 @@ + + + + Share PDF + Download PDF + Rotate PDF + + Page %1d/%2d + + Downloading to Downloads folder... + Share PDF using + + Error while loading PDF + + \ No newline at end of file diff --git a/core/ui/src/main/res/xml/file_paths.xml b/core/ui/src/main/res/xml/file_paths.xml new file mode 100644 index 000000000..d2e2c8dea --- /dev/null +++ b/core/ui/src/main/res/xml/file_paths.xml @@ -0,0 +1,4 @@ + + + + From ff9c4ad46d1ec6b1e05acfab85c1c7a06fe138a7 Mon Sep 17 00:00:00 2001 From: Julian Waluschyk Date: Thu, 19 Dec 2024 21:16:49 +0100 Subject: [PATCH 07/12] changed rotation icon in DropDownMenu --- .../native_app/core/ui/compose/ArtemisPdfView.kt | 6 +++--- core/ui/src/main/res/drawable/rotate.xml | 12 ++++++++++++ 2 files changed, 15 insertions(+), 3 deletions(-) create mode 100644 core/ui/src/main/res/drawable/rotate.xml diff --git a/core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/compose/ArtemisPdfView.kt b/core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/compose/ArtemisPdfView.kt index c1a726985..3555b996a 100644 --- a/core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/compose/ArtemisPdfView.kt +++ b/core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/compose/ArtemisPdfView.kt @@ -10,8 +10,6 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.filled.RotateLeft -import androidx.compose.material.icons.automirrored.filled.RotateRight import androidx.compose.material.icons.filled.Download import androidx.compose.material.icons.filled.MoreHoriz import androidx.compose.material.icons.filled.Share @@ -28,6 +26,7 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import de.tum.informatics.www1.artemis.native_app.core.ui.R @@ -162,7 +161,8 @@ fun ArtemisPdfView( DropdownMenuItem( leadingIcon = { Icon( - imageVector = if (isVertical.value) Icons.AutoMirrored.Filled.RotateLeft else Icons.AutoMirrored.Filled.RotateRight, + modifier = Modifier.height(19.dp), + painter = painterResource(id = R.drawable.rotate), contentDescription = null ) }, diff --git a/core/ui/src/main/res/drawable/rotate.xml b/core/ui/src/main/res/drawable/rotate.xml new file mode 100644 index 000000000..bca1172a0 --- /dev/null +++ b/core/ui/src/main/res/drawable/rotate.xml @@ -0,0 +1,12 @@ + + + + + + From 17746ca4c2edab456e1970233fc40d5b7f9734b4 Mon Sep 17 00:00:00 2001 From: Julian Waluschyk Date: Fri, 20 Dec 2024 11:55:05 +0100 Subject: [PATCH 08/12] added error handling --- .../core/ui/compose/ArtemisPdfView.kt | 21 +++---- .../core/ui/compose/LinkBottomSheet.kt | 55 +++++++++++-------- .../MarkdownLinkResolverImpl.kt | 9 +-- .../artemis/native_app/core/ui/pdf/PdfFile.kt | 11 +++- .../main/res/values/pdf_viewer_strings.xml | 2 + 5 files changed, 59 insertions(+), 39 deletions(-) diff --git a/core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/compose/ArtemisPdfView.kt b/core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/compose/ArtemisPdfView.kt index 3555b996a..ddbc29361 100644 --- a/core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/compose/ArtemisPdfView.kt +++ b/core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/compose/ArtemisPdfView.kt @@ -37,18 +37,15 @@ import de.tum.informatics.www1.artemis.native_app.core.ui.pdf.render.state.Horiz import de.tum.informatics.www1.artemis.native_app.core.ui.pdf.render.state.VerticalPdfReaderState import de.tum.informatics.www1.artemis.native_app.core.ui.pdf.render.state.rememberHorizontalPdfReaderState import de.tum.informatics.www1.artemis.native_app.core.ui.pdf.render.state.rememberVerticalPdfReaderState -import org.hildan.krossbow.stomp.ConnectionException +import java.net.UnknownHostException @Composable fun ArtemisPdfView( modifier: Modifier, - filename: String? = null, - url: String, - authToken: String, + pdfFile: PdfFile ) { val isVertical = remember { mutableStateOf(true) } val context = LocalContext.current - val pdfFile = PdfFile(url, authToken, filename) val verticalPdfState = rememberVerticalPdfReaderState( pdfFile = pdfFile, @@ -64,18 +61,18 @@ fun ArtemisPdfView( val showMenu = remember { mutableStateOf(false) } if (pdfState.mError != null) { - when (pdfState.mError) { - is ConnectionException -> { - //TODO + val errorMessage = when (pdfState.mError) { + is UnknownHostException -> { + R.string.pdf_view_error_no_internet } - else -> { - //TODO + R.string.pdf_view_error_loading } } + Toast.makeText( LocalContext.current, - stringResource(id = R.string.pdf_view_error_loading), + stringResource(id = errorMessage), Toast.LENGTH_LONG ).show() } @@ -86,7 +83,7 @@ fun ArtemisPdfView( Column { pdfState.file?.let { Text( - text = filename ?: it.name, + text = pdfFile.filename ?: it.name, modifier = Modifier.padding(8.dp), style = MaterialTheme.typography.titleMedium ) diff --git a/core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/compose/LinkBottomSheet.kt b/core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/compose/LinkBottomSheet.kt index 72cb79b97..65e28384b 100644 --- a/core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/compose/LinkBottomSheet.kt +++ b/core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/compose/LinkBottomSheet.kt @@ -6,6 +6,8 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material3.ModalBottomSheet import androidx.compose.runtime.Composable import androidx.compose.runtime.derivedStateOf @@ -17,6 +19,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import com.google.accompanist.web.WebContent import com.google.accompanist.web.WebViewState +import de.tum.informatics.www1.artemis.native_app.core.ui.pdf.PdfFile enum class LinkBottomSheetState { PDFVIEWSTATE, @@ -37,7 +40,7 @@ fun LinkBottomSheet( var webView: WebView? by remember { mutableStateOf(null) } val webViewState = getWebViewState(link) - ModalBottomSheet ( + ModalBottomSheet( modifier = modifier, onDismissRequest = onDismissRequest ) { @@ -46,30 +49,38 @@ fun LinkBottomSheet( .fillMaxHeight() .padding(8.dp) ) { - when (state) { - LinkBottomSheetState.PDFVIEWSTATE -> { - ArtemisPdfView( - modifier = Modifier.fillMaxSize(), - url = link, - authToken = authToken, - filename = fileName - ) - } - LinkBottomSheetState.WEBVIEWSTATE -> { - ArtemisWebView( - modifier = Modifier - .fillMaxHeight() - .padding(8.dp), - webViewState = webViewState, - webView = webView, - setWebView = { webView = it }, - serverUrl = serverUrl, - authToken = authToken - ) + when (state) { + LinkBottomSheetState.PDFVIEWSTATE -> { + val pdfFile = PdfFile(link, authToken, fileName) + ArtemisPdfView( + modifier = Modifier.fillMaxSize(), + pdfFile = pdfFile, + ) + } + + LinkBottomSheetState.WEBVIEWSTATE -> { + // The lazy column is needed to support scrolling + LazyColumn( + modifier = Modifier + .fillMaxHeight(), + state = rememberLazyListState() + ) { + item { + ArtemisWebView( + modifier = Modifier + .fillMaxHeight() + .padding(8.dp), + webViewState = webViewState, + webView = webView, + setWebView = { webView = it }, + serverUrl = serverUrl, + authToken = authToken + ) + } } } } - + } } } diff --git a/core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/markdown/link_resolving/MarkdownLinkResolverImpl.kt b/core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/markdown/link_resolving/MarkdownLinkResolverImpl.kt index 30f630258..f626070f0 100644 --- a/core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/markdown/link_resolving/MarkdownLinkResolverImpl.kt +++ b/core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/markdown/link_resolving/MarkdownLinkResolverImpl.kt @@ -66,10 +66,11 @@ class BaseMarkdownLinkResolver( setBottomSheetState(LinkBottomSheetState.PDFVIEWSTATE) showModalBottomSheet(link) } - link.startsWith(serverUrl) -> { - setBottomSheetState(LinkBottomSheetState.WEBVIEWSTATE) - showModalBottomSheet(link) - } + // TODO: open Artemis link in a Modal Bottom Sheet webview to attach session cookie (https://github.com/ls1intum/artemis-android/issues/245) +// link.startsWith(serverUrl) -> { +// setBottomSheetState(LinkBottomSheetState.WEBVIEWSTATE) +// showModalBottomSheet(link) +// } else -> { val customTabsIntent = CustomTabsIntent.Builder().build() customTabsIntent.launchUrl(context, Uri.parse(link)) diff --git a/core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/pdf/PdfFile.kt b/core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/pdf/PdfFile.kt index 253fee733..659b4640d 100644 --- a/core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/pdf/PdfFile.kt +++ b/core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/pdf/PdfFile.kt @@ -27,6 +27,7 @@ import kotlinx.coroutines.launch import okhttp3.OkHttpClient import okhttp3.Request import java.io.File +import java.net.UnknownHostException import java.util.Date @@ -146,7 +147,15 @@ class PdfFile( ).show() } catch (e: Exception) { - Toast.makeText(context, "Error downloading file: ${e.message}", Toast.LENGTH_LONG) + val errorMessage = when (e) { + is UnknownHostException -> { + R.string.pdf_view_error_no_internet + } + else -> { + R.string.pdf_view_error_downloading + } + } + Toast.makeText(context, getString(context, errorMessage), Toast.LENGTH_LONG) .show() e.printStackTrace() } diff --git a/core/ui/src/main/res/values/pdf_viewer_strings.xml b/core/ui/src/main/res/values/pdf_viewer_strings.xml index 9a72b1e1f..780befa10 100644 --- a/core/ui/src/main/res/values/pdf_viewer_strings.xml +++ b/core/ui/src/main/res/values/pdf_viewer_strings.xml @@ -11,5 +11,7 @@ Share PDF using Error while loading PDF + Error: No internet connection + Error while downloading PDF \ No newline at end of file From 3516ad55a4171663bfcbf789625a2647dd9b8a85 Mon Sep 17 00:00:00 2001 From: Julian Waluschyk Date: Sat, 21 Dec 2024 11:55:17 +0100 Subject: [PATCH 09/12] minor changes --- .../core/ui/pdf/render/state/HorizontalPdfReaderState.kt | 5 ++--- .../core/ui/pdf/render/state/VerticalPdfReaderState.kt | 6 +++--- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/pdf/render/state/HorizontalPdfReaderState.kt b/core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/pdf/render/state/HorizontalPdfReaderState.kt index 4ef90981b..cf4d03a29 100644 --- a/core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/pdf/render/state/HorizontalPdfReaderState.kt +++ b/core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/pdf/render/state/HorizontalPdfReaderState.kt @@ -18,13 +18,12 @@ class HorizontalPdfReaderState( state.pdfFile.link, state.pdfFile.authToken, state.pdfFile.filename, - state.isZoomEnabled, - state.currentPage + state.isZoomEnabled ) }, restore = { restoredList -> val pdfFile = PdfFile(restoredList[0] as String, restoredList[1] as String, restoredList[2] as String?) - val isZoomEnabled = restoredList[2] as Boolean + val isZoomEnabled = restoredList[3] as Boolean HorizontalPdfReaderState( pdfFile = pdfFile, diff --git a/core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/pdf/render/state/VerticalPdfReaderState.kt b/core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/pdf/render/state/VerticalPdfReaderState.kt index c63c38a3e..f0c226915 100644 --- a/core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/pdf/render/state/VerticalPdfReaderState.kt +++ b/core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/pdf/render/state/VerticalPdfReaderState.kt @@ -55,11 +55,11 @@ class VerticalPdfReaderState( restore = { restoredList -> VerticalPdfReaderState( PdfFile(restoredList[0] as String, restoredList[1] as String, restoredList[2] as String?), - restoredList[1] as Boolean, + restoredList[3] as Boolean, ).apply { lazyState = LazyListState( - firstVisibleItemIndex = restoredList[2] as Int, - firstVisibleItemScrollOffset = restoredList[3] as Int + firstVisibleItemIndex = restoredList[4] as Int, + firstVisibleItemScrollOffset = restoredList[5] as Int ) } } From ceeb0b2f9c2d207dc0f39be13eca14f4b82d4255 Mon Sep 17 00:00:00 2001 From: Julian Waluschyk Date: Sat, 21 Dec 2024 16:49:24 +0100 Subject: [PATCH 10/12] extracted composables in ArtemisPdfView and made adjustments --- core/ui/build.gradle.kts | 4 +- .../core/ui/compose/ArtemisPdfView.kt | 158 +++++++++++------- .../link_resolving/MarkdownLinkResolver.kt | 3 + .../MarkdownLinkResolverImpl.kt | 17 +- 4 files changed, 108 insertions(+), 74 deletions(-) diff --git a/core/ui/build.gradle.kts b/core/ui/build.gradle.kts index 2196ddec5..209a76bfc 100644 --- a/core/ui/build.gradle.kts +++ b/core/ui/build.gradle.kts @@ -21,7 +21,6 @@ dependencies { api(libs.androidx.compose.material.iconsExtended) api(libs.androidx.compose.material3) api(libs.androidx.compose.material3.windowsizeclass) - implementation(libs.androidx.browser) debugApi(libs.androidx.compose.ui.tooling) api(libs.androidx.compose.ui.tooling.preview) api(libs.androidx.compose.ui.util) @@ -33,11 +32,12 @@ dependencies { debugApi(libs.coil.compose.core) debugApi(libs.coil.test) api(libs.accompanist.webview) + api(libs.noties.markwon.core) implementation(libs.kotlinx.datetime) implementation(libs.kotlinx.coroutines.android) + implementation(libs.androidx.browser) - api(libs.noties.markwon.core) implementation(libs.noties.markwon.ext.strikethrough) implementation(libs.noties.markwon.ext.tables) implementation(libs.noties.markwon.html) diff --git a/core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/compose/ArtemisPdfView.kt b/core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/compose/ArtemisPdfView.kt index ddbc29361..4c94c86b3 100644 --- a/core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/compose/ArtemisPdfView.kt +++ b/core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/compose/ArtemisPdfView.kt @@ -34,6 +34,7 @@ import de.tum.informatics.www1.artemis.native_app.core.ui.pdf.PdfFile import de.tum.informatics.www1.artemis.native_app.core.ui.pdf.render.HorizontalPdfView import de.tum.informatics.www1.artemis.native_app.core.ui.pdf.render.VerticalPdfView import de.tum.informatics.www1.artemis.native_app.core.ui.pdf.render.state.HorizontalPdfReaderState +import de.tum.informatics.www1.artemis.native_app.core.ui.pdf.render.state.PdfReaderState import de.tum.informatics.www1.artemis.native_app.core.ui.pdf.render.state.VerticalPdfReaderState import de.tum.informatics.www1.artemis.native_app.core.ui.pdf.render.state.rememberHorizontalPdfReaderState import de.tum.informatics.www1.artemis.native_app.core.ui.pdf.render.state.rememberVerticalPdfReaderState @@ -56,7 +57,7 @@ fun ArtemisPdfView( isZoomEnabled = true, ) - val pdfState = if (isVertical.value) verticalPdfState else horizontalPdfState + var pdfState = if (isVertical.value) verticalPdfState else horizontalPdfState val showMenu = remember { mutableStateOf(false) } @@ -124,52 +125,24 @@ fun ArtemisPdfView( Icon(imageVector = Icons.Default.MoreHoriz, contentDescription = null) } - DropdownMenu( + PdfActionDropDownMenu( modifier = Modifier, - expanded = showMenu.value, - onDismissRequest = { showMenu.value = false } - ) { - DropdownMenuItem( - leadingIcon = { - Icon( - imageVector = Icons.Default.Download, - contentDescription = null - ) - }, - text = { Text(stringResource(R.string.pdf_view_download_menu_item)) }, - onClick = { - showMenu.value = false - verticalPdfState.file?.let { pdfFile.downloadPdf(context) } - } - ) - DropdownMenuItem( - leadingIcon = { - Icon( - imageVector = Icons.Default.Share, - contentDescription = null - ) - }, - text = { Text(stringResource(R.string.pdf_view_share_menu_item)) }, - onClick = { - showMenu.value = false - verticalPdfState.file?.let { pdfFile.sharePdf(context, it) } - } - ) - DropdownMenuItem( - leadingIcon = { - Icon( - modifier = Modifier.height(19.dp), - painter = painterResource(id = R.drawable.rotate), - contentDescription = null - ) - }, - text = { Text(stringResource(R.string.pdf_view_rotate_menu_item)) }, - onClick = { - showMenu.value = false - isVertical.value = !isVertical.value - } - ) - } + isExpanded = showMenu.value, + onDismissRequest = { showMenu.value = false }, + downloadPdf = { + showMenu.value = false + pdfState.file?.let { pdfFile.downloadPdf(context) } + }, + sharePdf = { + showMenu.value = false + pdfState.file?.let { pdfFile.sharePdf(context, it) } + }, + toggleViewMode = { + showMenu.value = false + pdfState = if (isVertical.value) horizontalPdfState else verticalPdfState + isVertical.value = !isVertical.value + } + ) } if (!pdfState.isLoaded) { @@ -194,23 +167,82 @@ fun ArtemisPdfView( .padding(16.dp), contentAlignment = Alignment.TopEnd ) { - Column( - modifier = Modifier - .background( - color = MaterialTheme.colorScheme.surface.copy(alpha = 0.5f), - shape = MaterialTheme.shapes.medium - ) - .padding(8.dp) - ) { - Text( - text = stringResource( - R.string.pdf_view_page_count, - pdfState.currentPage, - pdfState.pdfPageCount - ), - style = MaterialTheme.typography.bodyMedium - ) - } + PageCountText( + modifier = Modifier, + pdfState = pdfState + ) } } +} + +@Composable +private fun PageCountText( + modifier: Modifier, + pdfState: PdfReaderState +) { + Box( + modifier = modifier + .background( + color = MaterialTheme.colorScheme.surface.copy(alpha = 0.5f), + shape = MaterialTheme.shapes.medium + ) + .padding(8.dp) + ) { + Text( + text = stringResource( + R.string.pdf_view_page_count, + pdfState.currentPage, + pdfState.pdfPageCount + ), + style = MaterialTheme.typography.bodyMedium + ) + } +} + +@Composable +private fun PdfActionDropDownMenu( + modifier: Modifier, + isExpanded: Boolean, + onDismissRequest: () -> Unit, + downloadPdf: () -> Unit, + sharePdf: () -> Unit, + toggleViewMode: () -> Unit, +) { + DropdownMenu( + modifier = Modifier, + expanded = isExpanded, + onDismissRequest = onDismissRequest + ) { + DropdownMenuItem( + leadingIcon = { + Icon( + imageVector = Icons.Default.Download, + contentDescription = null + ) + }, + text = { Text(stringResource(R.string.pdf_view_download_menu_item)) }, + onClick = downloadPdf + ) + DropdownMenuItem( + leadingIcon = { + Icon( + imageVector = Icons.Default.Share, + contentDescription = null + ) + }, + text = { Text(stringResource(R.string.pdf_view_share_menu_item)) }, + onClick = sharePdf + ) + DropdownMenuItem( + leadingIcon = { + Icon( + modifier = Modifier.height(19.dp), + painter = painterResource(id = R.drawable.rotate), + contentDescription = null + ) + }, + text = { Text(stringResource(R.string.pdf_view_rotate_menu_item)) }, + onClick = toggleViewMode + ) + } } \ No newline at end of file diff --git a/core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/markdown/link_resolving/MarkdownLinkResolver.kt b/core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/markdown/link_resolving/MarkdownLinkResolver.kt index 084e75ddc..8d2dbcdcb 100644 --- a/core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/markdown/link_resolving/MarkdownLinkResolver.kt +++ b/core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/markdown/link_resolving/MarkdownLinkResolver.kt @@ -1,8 +1,11 @@ package de.tum.informatics.www1.artemis.native_app.core.ui.markdown.link_resolving import androidx.compose.runtime.Composable +import androidx.compose.runtime.compositionLocalOf import io.noties.markwon.LinkResolver +val LocalMarkdownLinkResolver = compositionLocalOf { error("No MarkdownLinkResolver provided") } + interface MarkdownLinkResolver { @Composable fun rememberMarkdownLinkResolver(): LinkResolver diff --git a/core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/markdown/link_resolving/MarkdownLinkResolverImpl.kt b/core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/markdown/link_resolving/MarkdownLinkResolverImpl.kt index f626070f0..1be2c74fc 100644 --- a/core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/markdown/link_resolving/MarkdownLinkResolverImpl.kt +++ b/core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/markdown/link_resolving/MarkdownLinkResolverImpl.kt @@ -1,13 +1,10 @@ package de.tum.informatics.www1.artemis.native_app.core.ui.markdown.link_resolving import android.content.Context -import android.net.Uri import android.view.View -import androidx.browser.customtabs.CustomTabsIntent import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.compositionLocalOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -16,12 +13,12 @@ import androidx.compose.ui.platform.LocalContext import de.tum.informatics.www1.artemis.native_app.core.datastore.AccountService import de.tum.informatics.www1.artemis.native_app.core.datastore.ServerConfigurationService import de.tum.informatics.www1.artemis.native_app.core.datastore.authToken +import de.tum.informatics.www1.artemis.native_app.core.ui.LinkOpener +import de.tum.informatics.www1.artemis.native_app.core.ui.LocalLinkOpener import de.tum.informatics.www1.artemis.native_app.core.ui.compose.LinkBottomSheet import de.tum.informatics.www1.artemis.native_app.core.ui.compose.LinkBottomSheetState import io.noties.markwon.LinkResolver -val LocalMarkdownLinkResolver = compositionLocalOf { error("No MarkdownLinkResolver provided") } - class MarkdownLinkResolverImpl( private val accountService: AccountService, private val serverConfigurationService: ServerConfigurationService, @@ -31,6 +28,7 @@ class MarkdownLinkResolverImpl( val serverUrl by serverConfigurationService.serverUrl.collectAsState(initial = "") val authToken by accountService.authToken.collectAsState(initial = "") val context = LocalContext.current + val localLinkOpener = LocalLinkOpener.current val (bottomSheetLink, setLinkToShow) = remember { mutableStateOf(null) } val (bottomSheetState, setBottomSheetState) = remember { mutableStateOf(LinkBottomSheetState.WEBVIEWSTATE) } @@ -48,14 +46,15 @@ class MarkdownLinkResolverImpl( ) } - return remember(context, authToken, serverUrl) { - BaseMarkdownLinkResolver(context, serverUrl, setLinkToShow, setBottomSheetState) + return remember(context, localLinkOpener, authToken, serverUrl) { + BaseMarkdownLinkResolver(context, localLinkOpener, serverUrl, setLinkToShow, setBottomSheetState) } } } class BaseMarkdownLinkResolver( private val context: Context, + private val linkOpener: LinkOpener, private val serverUrl: String = "", private val showModalBottomSheet: (String) -> Unit, private val setBottomSheetState: (LinkBottomSheetState) -> Unit @@ -72,9 +71,9 @@ class BaseMarkdownLinkResolver( // showModalBottomSheet(link) // } else -> { - val customTabsIntent = CustomTabsIntent.Builder().build() - customTabsIntent.launchUrl(context, Uri.parse(link)) + linkOpener.openLink(link) } } } } + From ad2861f42101b6a4be207515065e29f1f187ee75 Mon Sep 17 00:00:00 2001 From: Julian Waluschyk Date: Fri, 3 Jan 2025 09:55:49 +0100 Subject: [PATCH 11/12] changed view-mode text --- core/ui/src/main/res/values/pdf_viewer_strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/ui/src/main/res/values/pdf_viewer_strings.xml b/core/ui/src/main/res/values/pdf_viewer_strings.xml index 780befa10..d1acd8363 100644 --- a/core/ui/src/main/res/values/pdf_viewer_strings.xml +++ b/core/ui/src/main/res/values/pdf_viewer_strings.xml @@ -3,7 +3,7 @@ Share PDF Download PDF - Rotate PDF + Toggle View Mode Page %1d/%2d From 931a926c963e50b148a95fe4d39e8de31fea0160 Mon Sep 17 00:00:00 2001 From: Julian Waluschyk Date: Sat, 4 Jan 2025 13:00:34 +0100 Subject: [PATCH 12/12] add close function call on opening error and changed padding for LinkBottomSheet --- .../artemis/native_app/core/ui/compose/ArtemisPdfView.kt | 5 ++++- .../artemis/native_app/core/ui/compose/LinkBottomSheet.kt | 8 +++++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/compose/ArtemisPdfView.kt b/core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/compose/ArtemisPdfView.kt index 4c94c86b3..75f63f356 100644 --- a/core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/compose/ArtemisPdfView.kt +++ b/core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/compose/ArtemisPdfView.kt @@ -43,7 +43,8 @@ import java.net.UnknownHostException @Composable fun ArtemisPdfView( modifier: Modifier, - pdfFile: PdfFile + pdfFile: PdfFile, + dismiss: () -> Unit ) { val isVertical = remember { mutableStateOf(true) } val context = LocalContext.current @@ -76,6 +77,8 @@ fun ArtemisPdfView( stringResource(id = errorMessage), Toast.LENGTH_LONG ).show() + + dismiss() } Box( diff --git a/core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/compose/LinkBottomSheet.kt b/core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/compose/LinkBottomSheet.kt index 65e28384b..4e00a3494 100644 --- a/core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/compose/LinkBottomSheet.kt +++ b/core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/compose/LinkBottomSheet.kt @@ -3,9 +3,12 @@ package de.tum.informatics.www1.artemis.native_app.core.ui.compose import android.annotation.SuppressLint import android.webkit.WebView import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.asPaddingValues import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.systemBars import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material3.ModalBottomSheet @@ -41,7 +44,9 @@ fun LinkBottomSheet( val webViewState = getWebViewState(link) ModalBottomSheet( - modifier = modifier, + modifier = modifier.padding( + top = WindowInsets.systemBars.asPaddingValues().calculateTopPadding(), + ), onDismissRequest = onDismissRequest ) { Box( @@ -55,6 +60,7 @@ fun LinkBottomSheet( ArtemisPdfView( modifier = Modifier.fillMaxSize(), pdfFile = pdfFile, + dismiss = onDismissRequest ) }