diff options
| author | Jonatan Rhodin <jonatan.rhodin@mullvad.net> | 2026-03-27 12:23:59 +0100 |
|---|---|---|
| committer | Jonatan Rhodin <jonatan.rhodin@mullvad.net> | 2026-03-29 23:10:34 +0200 |
| commit | 952354cb33a69df1a58b1a32858e5143882f72e2 (patch) | |
| tree | fbb215f393548c3ae69954b5a9be8a055ce92ebc | |
| parent | 726ce1a3fddf0eb613ef8f8d15fe2a30dbd76c10 (diff) | |
| download | mullvadvpn-hackday-remote-compose.tar.xz mullvadvpn-hackday-remote-compose.zip | |
An attempt was madehackday-remote-compose
16 files changed, 451 insertions, 4 deletions
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/di/UiModule.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/di/UiModule.kt index 9ce95697d5..c4e288296d 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/di/UiModule.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/di/UiModule.kt @@ -52,7 +52,9 @@ import net.mullvad.mullvadvpn.feature.problemreport.impl.viewlogs.ViewLogsViewMo import net.mullvad.mullvadvpn.feature.redeemvoucher.impl.VoucherDialogViewModel import net.mullvad.mullvadvpn.feature.serveripoverride.impl.ServerIpOverridesViewModel import net.mullvad.mullvadvpn.feature.serveripoverride.impl.reset.ResetServerIpOverridesConfirmationViewModel +import net.mullvad.mullvadvpn.feature.settings.impl.FaqRemoteViewModel import net.mullvad.mullvadvpn.feature.settings.impl.SettingsViewModel +import net.mullvad.mullvadvpn.feature.settings.impl.server.Server import net.mullvad.mullvadvpn.feature.splittunneling.impl.SplitTunnelingViewModel import net.mullvad.mullvadvpn.feature.splittunneling.impl.applist.ApplicationsProvider import net.mullvad.mullvadvpn.feature.vpnsettings.impl.VpnSettingsViewModel @@ -255,6 +257,8 @@ val uiModule = module { single { RelayListScrollConnection() } + single { Server(androidContext()) } + // View models viewModel { AccountViewModel(get(), get(), get()) } viewModel { DeleteAccountConfirmationViewModel(get(), get()) } @@ -417,6 +421,7 @@ val uiModule = module { } viewModel { AppearanceViewModel(get()) } viewModel { AutoConnectAndLockdownModeViewModel(isPlayBuild = IS_PLAY_BUILD) } + viewModel { FaqRemoteViewModel(get()) } // This view model must be single so we correctly attach lifecycle and share it with activity single { MullvadAppViewModel(get(), get()) } diff --git a/android/gradle/libs.versions.toml b/android/gradle/libs.versions.toml index 8d1f6af9fa..fa458b4252 100644 --- a/android/gradle/libs.versions.toml +++ b/android/gradle/libs.versions.toml @@ -4,7 +4,7 @@ # See Bump SDK or container image template in Linear for more details. compile-sdk = "36" build-tools = "36.1.0" -min-sdk = "28" +min-sdk = "30" target-sdk = "36" jvm-toolchain = "21" ndk = "27.3.13750724" @@ -75,6 +75,10 @@ turbine = "1.2.1" annotation-jvm = "1.9.1" junit-version = "4.13.2" material = "1.13.0" +remote-compose = "1.0.0-alpha07" +jsoup = "1.22.1" +remote-creation-compose = "1.0.0-alpha06" +remote-player-compose = "1.0.0-alpha06" [libraries] accompanist-drawablepainter = { module = "com.google.accompanist:accompanist-drawablepainter", version.ref = "accompanist" } @@ -135,6 +139,7 @@ grpc-kotlin-stub = { module = "io.grpc:grpc-kotlin-stub", version.ref = "grpc-ko grpc-okhttp = { module = "io.grpc:grpc-okhttp", version.ref = "grpc" } grpc-protobuf-lite = { module = "io.grpc:grpc-protobuf-lite", version.ref = "grpc" } grpc-stub = { module = "io.grpc:grpc-stub", version.ref = "grpc" } +jsoup = { module = "org.jsoup:jsoup", version.ref = "jsoup" } junit-jupiter-api = { module = "org.junit.jupiter:junit-jupiter-api", version.ref = "junit" } junit-jupiter-engine = { module = "org.junit.jupiter:junit-jupiter-engine", version.ref = "junit" } junit-jupiter-params = { module = "org.junit.jupiter:junit-jupiter-params", version.ref = "junit" } @@ -173,6 +178,14 @@ mockk = { module = "io.mockk:mockk", version.ref = "mockk" } mockk-android = { module = "io.mockk:mockk-android", version.ref = "mockk" } play-services-location = { module = "com.google.android.gms:play-services-location", version.ref = "play-services-location" } protobuf-kotlin-lite = { module = "com.google.protobuf:protobuf-kotlin-lite", version.ref = "protobuf" } +remote-compose-core = { module = "androidx.compose.remote:remote-core", version.ref = "remote-compose" } +remote-compose-creation = { module = "androidx.compose.remote:remote-creation", version.ref = "remote-compose" } +remote-compose-creation-core = { module = "androidx.compose.remote:remote-creation-core", version.ref = "remote-compose" } +remote-compose-creation-android = { module = "androidx.compose.remote:remote-creation-android", version.ref = "remote-compose" } +remote-compose-creation-compose = { module = "androidx.compose.remote:remote-creation-compose", version.ref = "remote-compose" } +remote-compose-player-core = { module = "androidx.compose.remote:remote-player-core", version.ref = "remote-compose" } +remote-compose-player-compose = { module = "androidx.compose.remote:remote-player-compose", version.ref = "remote-compose" } +remote-compose-tooling-preview = { module = "androidx.compose.remote:remote-tooling-preview", version.ref = "remote-compose" } turbine = { module = "app.cash.turbine:turbine", version.ref = "turbine" } junit = { group = "junit", name = "junit", version.ref = "junit-version" } material = { group = "com.google.android.material", name = "material", version.ref = "material" } diff --git a/android/gradle/verification-metadata.xml b/android/gradle/verification-metadata.old.xml index c775d4a7c0..c775d4a7c0 100644 --- a/android/gradle/verification-metadata.xml +++ b/android/gradle/verification-metadata.old.xml diff --git a/android/lib/feature/settings/api/src/main/kotlin/net/mullvad/mullvadvpn/feature/settings/api/FaqRemoteNavKey.kt b/android/lib/feature/settings/api/src/main/kotlin/net/mullvad/mullvadvpn/feature/settings/api/FaqRemoteNavKey.kt new file mode 100644 index 0000000000..161add6623 --- /dev/null +++ b/android/lib/feature/settings/api/src/main/kotlin/net/mullvad/mullvadvpn/feature/settings/api/FaqRemoteNavKey.kt @@ -0,0 +1,6 @@ +package net.mullvad.mullvadvpn.feature.settings.api + +import kotlinx.parcelize.Parcelize +import net.mullvad.mullvadvpn.core.NavKey2 + +@Parcelize data object FaqRemoteNavKey : NavKey2 diff --git a/android/lib/feature/settings/impl/build.gradle.kts b/android/lib/feature/settings/impl/build.gradle.kts index a7d325b986..8c9d171e6f 100644 --- a/android/lib/feature/settings/impl/build.gradle.kts +++ b/android/lib/feature/settings/impl/build.gradle.kts @@ -25,4 +25,11 @@ dependencies { implementation(libs.koin.compose) implementation(libs.arrow) implementation(libs.protobuf.kotlin.lite) + implementation(libs.jsoup) + implementation(libs.remote.compose.creation.compose) + implementation(libs.remote.compose.player.core) + implementation(libs.remote.compose.player.compose) + implementation(libs.remote.compose.core) + implementation(libs.remote.compose.creation.core) + implementation(libs.remote.compose.creation.android) } diff --git a/android/lib/feature/settings/impl/src/main/kotlin/net/mullvad/mullvadvpn/feature/settings/impl/FaqRemoteScreen.kt b/android/lib/feature/settings/impl/src/main/kotlin/net/mullvad/mullvadvpn/feature/settings/impl/FaqRemoteScreen.kt new file mode 100644 index 0000000000..2c8bb8cbad --- /dev/null +++ b/android/lib/feature/settings/impl/src/main/kotlin/net/mullvad/mullvadvpn/feature/settings/impl/FaqRemoteScreen.kt @@ -0,0 +1,48 @@ +package net.mullvad.mullvadvpn.feature.settings.impl + +import androidx.compose.foundation.layout.BoxWithConstraints +import androidx.compose.remote.player.compose.RemoteDocumentPlayer +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.res.stringResource +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.lifecycle.compose.dropUnlessResumed +import net.mullvad.mullvadvpn.core.Navigator +import net.mullvad.mullvadvpn.lib.common.Lc +import net.mullvad.mullvadvpn.lib.ui.component.NavigateBackIconButton +import net.mullvad.mullvadvpn.lib.ui.component.ScaffoldWithSmallTopBar +import net.mullvad.mullvadvpn.lib.ui.designsystem.MullvadCircularProgressIndicatorLarge +import org.koin.androidx.compose.koinViewModel + +@Composable +fun FaqRemote(navigator: Navigator) { + val viewmodel = koinViewModel<FaqRemoteViewModel>() + val state by viewmodel.uiState.collectAsStateWithLifecycle() + FaqRemoteScreen(state = state, onBackClick = dropUnlessResumed { navigator.goBack() }) +} + +@Composable +private fun FaqRemoteScreen(state: Lc<Unit, FaqRemoteState>, onBackClick: () -> Unit) { + ScaffoldWithSmallTopBar( + appBarTitle = stringResource(R.string.faqs_and_guides), + navigationIcon = { NavigateBackIconButton(onNavigateBack = onBackClick) }, + ) { modifier -> + BoxWithConstraints(modifier = modifier) { + val screenWidth = constraints.maxWidth + val screenHeight = constraints.maxHeight + + when (state) { + is Lc.Loading -> { + MullvadCircularProgressIndicatorLarge() + } + is Lc.Content -> { + RemoteDocumentPlayer( + document = state.value.document, + documentWidth = screenWidth, + documentHeight = screenHeight, + ) + } + } + } + } +} diff --git a/android/lib/feature/settings/impl/src/main/kotlin/net/mullvad/mullvadvpn/feature/settings/impl/FaqRemoteViewModel.kt b/android/lib/feature/settings/impl/src/main/kotlin/net/mullvad/mullvadvpn/feature/settings/impl/FaqRemoteViewModel.kt new file mode 100644 index 0000000000..ba25cc4bfc --- /dev/null +++ b/android/lib/feature/settings/impl/src/main/kotlin/net/mullvad/mullvadvpn/feature/settings/impl/FaqRemoteViewModel.kt @@ -0,0 +1,53 @@ +package net.mullvad.mullvadvpn.feature.settings.impl + +import androidx.compose.remote.core.CoreDocument +import androidx.compose.remote.creation.RemoteComposeWriter +import androidx.compose.remote.creation.compose.capture.RemoteComposeCapture +import androidx.compose.remote.creation.compose.widgets.RemoteComposeWidget +import androidx.compose.remote.player.core.RemoteDocument +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import co.touchlab.kermit.Logger +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.WhileSubscribed +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch +import net.mullvad.mullvadvpn.feature.settings.impl.server.Server +import net.mullvad.mullvadvpn.lib.common.Lc +import net.mullvad.mullvadvpn.lib.common.constant.VIEW_MODEL_STOP_TIMEOUT + +data class FaqRemoteState(val document: CoreDocument) + +class FaqRemoteViewModel(private val server: Server) : ViewModel() { + + private val document = MutableStateFlow<CoreDocument?>(null) + + val uiState = + document + .filterNotNull() + .map { Lc.Content(FaqRemoteState(document = it)) } + .stateIn( + viewModelScope, + SharingStarted.WhileSubscribed(VIEW_MODEL_STOP_TIMEOUT), + Lc.Loading(Unit), + ) + + init { + viewModelScope.launch(Dispatchers.IO) { + val remoteDocument = server.main() + //val bytes = remoteDocument.bytes + + //val playerDocument = CoreDocument() + Logger.d { "remoteDocument:$remoteDocument" } + + //playerDocument. + //document.value = playerDocument + //RemoteComposeCapture + document.value = RemoteDocument(remoteDocument.bytes).document + } + } +} diff --git a/android/lib/feature/settings/impl/src/main/kotlin/net/mullvad/mullvadvpn/feature/settings/impl/SettingsScreen.kt b/android/lib/feature/settings/impl/src/main/kotlin/net/mullvad/mullvadvpn/feature/settings/impl/SettingsScreen.kt index 7d72d706fe..e8cab9953b 100644 --- a/android/lib/feature/settings/impl/src/main/kotlin/net/mullvad/mullvadvpn/feature/settings/impl/SettingsScreen.kt +++ b/android/lib/feature/settings/impl/src/main/kotlin/net/mullvad/mullvadvpn/feature/settings/impl/SettingsScreen.kt @@ -30,6 +30,7 @@ import net.mullvad.mullvadvpn.feature.daita.api.DaitaNavKey import net.mullvad.mullvadvpn.feature.multihop.api.MultihopNavKey import net.mullvad.mullvadvpn.feature.notification.api.NotificationSettingsNavKey import net.mullvad.mullvadvpn.feature.problemreport.api.ProblemReportNavKey +import net.mullvad.mullvadvpn.feature.settings.api.FaqRemoteNavKey import net.mullvad.mullvadvpn.feature.splittunneling.api.SplitTunnelingNavKey import net.mullvad.mullvadvpn.feature.vpnsettings.api.VpnSettingsNavKey import net.mullvad.mullvadvpn.lib.common.Lc @@ -68,6 +69,8 @@ private fun PreviewSettingsScreen( onDaitaClick = {}, onBackClick = {}, onNotificationSettingsCellClick = {}, + onAppObfuscationClick = {}, + onFaqClick = {} ) } } @@ -91,6 +94,7 @@ fun Settings(navigator: Navigator) { onNotificationSettingsCellClick = dropUnlessResumed { navigator.navigate(NotificationSettingsNavKey) }, onAppObfuscationClick = dropUnlessResumed { navigator.navigate(AppearanceNavKey) }, + onFaqClick = dropUnlessResumed { navigator.navigate(FaqRemoteNavKey) } ) } @@ -106,7 +110,8 @@ fun SettingsScreen( onDaitaClick: () -> Unit, onBackClick: () -> Unit, onNotificationSettingsCellClick: () -> Unit, - onAppObfuscationClick: () -> Unit = {}, + onAppObfuscationClick: () -> Unit, + onFaqClick: () -> Unit, ) { ScaffoldWithMediumTopBar( appBarTitle = stringResource(id = R.string.settings), @@ -135,6 +140,7 @@ fun SettingsScreen( onDaitaClick = onDaitaClick, onNotificationSettingsCellClick = onNotificationSettingsCellClick, onAppObfuscationClick = onAppObfuscationClick, + onFaqClick = onFaqClick, ) } } @@ -152,7 +158,8 @@ private fun LazyListScope.content( onMultihopClick: () -> Unit, onDaitaClick: () -> Unit, onNotificationSettingsCellClick: () -> Unit, - onAppObfuscationClick: () -> Unit = {}, + onAppObfuscationClick: () -> Unit, + onFaqClick: () -> Unit, ) { if (state.isLoggedIn) { itemWithDivider { @@ -208,7 +215,9 @@ private fun LazyListScope.content( itemWithDivider { ReportProblem(onReportProblemCellClick) } - if (!state.isPlayBuild) { + if (state.isPlayBuild) { + itemWithDivider { RemoteFaq(onClick = onFaqClick) } + } else { itemWithDivider { FaqAndGuides() } } @@ -260,6 +269,16 @@ private fun FaqAndGuides() { } @Composable +private fun RemoteFaq(onClick: () -> Unit) { + val faqGuideLabel = stringResource(id = R.string.faqs_and_guides) + NavigationListItem( + title = faqGuideLabel, + onClick = onClick, + position = Position.Middle, + ) +} + +@Composable private fun PrivacyPolicy(state: SettingsUiState) { val privacyPolicyLabel = stringResource(id = R.string.privacy_policy_label) diff --git a/android/lib/feature/settings/impl/src/main/kotlin/net/mullvad/mullvadvpn/feature/settings/impl/navigation/FaqRemoteEntryProvider.kt b/android/lib/feature/settings/impl/src/main/kotlin/net/mullvad/mullvadvpn/feature/settings/impl/navigation/FaqRemoteEntryProvider.kt new file mode 100644 index 0000000000..7107573a45 --- /dev/null +++ b/android/lib/feature/settings/impl/src/main/kotlin/net/mullvad/mullvadvpn/feature/settings/impl/navigation/FaqRemoteEntryProvider.kt @@ -0,0 +1,13 @@ +package net.mullvad.mullvadvpn.feature.settings.impl.navigation + +import androidx.navigation3.runtime.EntryProviderScope +import net.mullvad.mullvadvpn.core.NavKey2 +import net.mullvad.mullvadvpn.core.Navigator +import net.mullvad.mullvadvpn.feature.settings.api.FaqRemoteNavKey +import net.mullvad.mullvadvpn.feature.settings.impl.FaqRemote + +internal fun EntryProviderScope<NavKey2>.faqRemoteEntry(navigator: Navigator) { + entry<FaqRemoteNavKey>() { + FaqRemote(navigator = navigator) + } +} diff --git a/android/lib/feature/settings/impl/src/main/kotlin/net/mullvad/mullvadvpn/feature/settings/impl/navigation/SettingsEntryProvider.kt b/android/lib/feature/settings/impl/src/main/kotlin/net/mullvad/mullvadvpn/feature/settings/impl/navigation/SettingsEntryProvider.kt index fe9f5660d5..2fae30f986 100644 --- a/android/lib/feature/settings/impl/src/main/kotlin/net/mullvad/mullvadvpn/feature/settings/impl/navigation/SettingsEntryProvider.kt +++ b/android/lib/feature/settings/impl/src/main/kotlin/net/mullvad/mullvadvpn/feature/settings/impl/navigation/SettingsEntryProvider.kt @@ -9,4 +9,6 @@ import net.mullvad.mullvadvpn.feature.settings.impl.Settings fun EntryProviderScope<NavKey2>.settingsEntry(navigator: Navigator) { entry<SettingsNavKey>(metadata = topLevelTransition()) { Settings(navigator = navigator) } + + faqRemoteEntry(navigator) } diff --git a/android/lib/feature/settings/impl/src/main/kotlin/net/mullvad/mullvadvpn/feature/settings/impl/server/Downloader.kt b/android/lib/feature/settings/impl/src/main/kotlin/net/mullvad/mullvadvpn/feature/settings/impl/server/Downloader.kt new file mode 100644 index 0000000000..e474b51397 --- /dev/null +++ b/android/lib/feature/settings/impl/src/main/kotlin/net/mullvad/mullvadvpn/feature/settings/impl/server/Downloader.kt @@ -0,0 +1,5 @@ +package net.mullvad.mullvadvpn.feature.settings.impl.server + +import org.jsoup.Jsoup + +suspend fun download() = Jsoup.connect("https://mullvad.net/en/help/faq").get() diff --git a/android/lib/feature/settings/impl/src/main/kotlin/net/mullvad/mullvadvpn/feature/settings/impl/server/Parser.kt b/android/lib/feature/settings/impl/src/main/kotlin/net/mullvad/mullvadvpn/feature/settings/impl/server/Parser.kt new file mode 100644 index 0000000000..50e74c274c --- /dev/null +++ b/android/lib/feature/settings/impl/src/main/kotlin/net/mullvad/mullvadvpn/feature/settings/impl/server/Parser.kt @@ -0,0 +1,186 @@ +package net.mullvad.mullvadvpn.feature.settings.impl.server + +import kotlin.text.indexOf +import net.mullvad.mullvadvpn.feature.settings.impl.server.model.FaqBlock +import net.mullvad.mullvadvpn.feature.settings.impl.server.model.FaqItem +import net.mullvad.mullvadvpn.feature.settings.impl.server.model.RichText +import org.jsoup.nodes.Document +import org.jsoup.nodes.Element +import org.jsoup.nodes.TextNode + +fun parseFaq(doc: Document): List<FaqBlock.Question> { + val article = doc.selectFirst("main article") ?: return emptyList() + + val result = mutableListOf<FaqBlock.Question>() + + var currentTitle: String? = null + val currentContent = mutableListOf<FaqBlock.Content>() + + for (el in article.children()) { + when (el.tagName()) { + + "h2" -> { + // flush previous question + if (currentTitle != null) { + result += FaqBlock.Question(currentTitle, currentContent.toList()) + currentContent.clear() + } + currentTitle = el.text() + } + + "p" -> { + currentContent += FaqBlock.Content.Paragraph(el.text()) + } + + "ul" -> { + el.select("li").forEach { li -> + currentContent += FaqBlock.Content.ListItem(li.text()) + } + } + } + } + + // flush last item + if (currentTitle != null) { + result += FaqBlock.Question(currentTitle, currentContent.toList()) + } + + return result +} + +fun parseFaqFromText(doc: org.jsoup.nodes.Document): List<FaqItem> { + val rawText = doc.body().text() + + // Split by question marks + whitespace + val parts = rawText.split(Regex("(?<=\\?)(\\s+|$)")) + + val items = mutableListOf<FaqItem>() + + var i = 0 + while (i < parts.size) { + val question = parts[i].trim() + val answer = if (i + 1 < parts.size) parts[i + 1].trim() else "" + items += FaqItem(question, answer) + i += 2 + } + + return items +} + +fun parseFaqClean(doc: org.jsoup.nodes.Document): List<FaqItem> { + val text = doc.body().text() + + // Find where the FAQ really starts + val faqStartIndex = Regex("\\?").find(text)?.range?.first ?: 0 + + val trimmedText = text.substring(faqStartIndex) + + // Split by question/answer boundaries + val parts = trimmedText.split(Regex("(?<=\\?).*?(?=\\S)")) + + val items = mutableListOf<FaqItem>() + var i = 0 + while (i < parts.size - 1) { + val question = parts[i].trim() + val answer = parts[i + 1].trim() + items += FaqItem(question, answer) + i += 2 + } + return items +} + +fun parseFaqByHash(doc: Document): List<FaqItem> { + val body = doc.body() + + // Collect all text lines + val lines = body.text().lines() + + val items = mutableListOf<FaqItem>() + var currentQuestion: String? = null + val currentAnswer = StringBuilder() + + for (line in lines) { + val trimmed = line.trim() + if (trimmed.startsWith("#")) { + // Save previous question/answer + if (currentQuestion != null) { + items += FaqItem(currentQuestion, currentAnswer.toString().trim()) + currentAnswer.clear() + } + // New question (remove leading #) + currentQuestion = trimmed.removePrefix("#").trim() + } else { + // Append to current answer + if (currentQuestion != null) { + if (currentAnswer.isNotEmpty()) currentAnswer.append("\n") + currentAnswer.append(trimmed) + } + } + } + + // Add the last question + if (currentQuestion != null) { + items += FaqItem(currentQuestion, currentAnswer.toString().trim()) + } + + return items +} + +fun parseMullvadFaq(doc: Document): List<FaqItem> { + val items = mutableListOf<FaqItem>() + + // 1. Mullvad FAQ questions are contained within <h3> tags + val questionHeaders = doc.select("h3") + + for (header in questionHeaders) { + val question = header.text().trim() + val answerBuilder = StringBuilder() + + // 2. The answer consists of all siblings (p, ul, div) + // until we hit the next <h3> or the end of the section. + var sibling = header.nextElementSibling() + + while (sibling != null && sibling.tagName() != "h3" && sibling.tagName() != "h2") { + // Append the text of the sibling (paragraph, list item, etc.) + val text = sibling.text().trim() + if (text.isNotEmpty()) { + if (answerBuilder.isNotEmpty()) answerBuilder.append("\n\n") + answerBuilder.append(text) + } + sibling = sibling.nextElementSibling() + } + + val answer = answerBuilder.toString().trim() + + if (question.isNotEmpty() && answer.isNotEmpty()) { + items.add(FaqItem(question, answer)) + } + } + + return items +} + +fun parseParagraph(el: Element): FaqBlock.Content.Paragraph { + val parts = mutableListOf<RichText.Part>() + + el.childNodes().forEach { node -> + when (node) { + is TextNode -> parts += RichText.Part.Text(node.text()) + is Element -> { + if (node.tagName() == "a") { + parts += RichText.Part.Link( + node.text(), + node.absUrl("href") + ) + } + } + } + } + + return FaqBlock.Content.Paragraph(parts.joinToString("") { + when (it) { + is RichText.Part.Text -> it.text + is RichText.Part.Link -> it.text // simple fallback + } + }) +} diff --git a/android/lib/feature/settings/impl/src/main/kotlin/net/mullvad/mullvadvpn/feature/settings/impl/server/Server.kt b/android/lib/feature/settings/impl/src/main/kotlin/net/mullvad/mullvadvpn/feature/settings/impl/server/Server.kt new file mode 100644 index 0000000000..472928af71 --- /dev/null +++ b/android/lib/feature/settings/impl/src/main/kotlin/net/mullvad/mullvadvpn/feature/settings/impl/server/Server.kt @@ -0,0 +1,67 @@ +package net.mullvad.mullvadvpn.feature.settings.impl.server + +import android.content.Context +import androidx.compose.material3.MaterialTheme +import androidx.compose.remote.creation.compose.capture.CapturedDocument +import androidx.compose.remote.creation.compose.capture.captureSingleRemoteDocument +import androidx.compose.remote.creation.compose.layout.RemoteArrangement +import androidx.compose.remote.creation.compose.layout.RemoteColumn +import androidx.compose.remote.creation.compose.layout.RemoteText +import androidx.compose.remote.creation.compose.state.RemoteDp +import androidx.compose.remote.creation.compose.state.asRemoteTextUnit +import androidx.compose.remote.creation.compose.state.rc +import androidx.compose.remote.creation.compose.text.RemoteTextStyle +import androidx.compose.runtime.Composable +import androidx.compose.ui.unit.dp +import co.touchlab.kermit.Logger +import net.mullvad.mullvadvpn.feature.settings.impl.server.model.FaqItem + +class Server(private val context: Context) { + suspend fun main(): CapturedDocument { + val document = download() + val data = parseFaqClean(document) + val captured = captureSingleRemoteDocument(context = context) { RemoteFaq(faqItems = data) } + return captured + } +} + +@Composable +fun RemoteFaq(faqItems: List<FaqItem>) { + RemoteColumn(verticalArrangement = RemoteArrangement.spacedBy(RemoteDp(10.dp))) { + RemoteText( + "These are FAQs", + color = MaterialTheme.colorScheme.onPrimary.rc, + fontSize = MaterialTheme.typography.titleLarge.fontSize.asRemoteTextUnit(), + ) + Logger.d { "questions=$faqItems" } + faqItems.forEach { q -> + RemoteText( + text = "Section", + style = RemoteTextStyle.Default, + color = MaterialTheme.colorScheme.onPrimary.rc, + ) + + RemoteText( + text = q.question, + style = RemoteTextStyle.Default, + color = MaterialTheme.colorScheme.onPrimary.rc, + ) + + RemoteText( + text = q.answer, + style = RemoteTextStyle.Default, + color = MaterialTheme.colorScheme.onPrimary.rc, + ) + + /*q.content.forEach { c -> + when (c) { + is FaqBlock.Content.Paragraph -> + RemoteText(c.text, color = MaterialTheme.colorScheme.onPrimary.rc) + + is FaqBlock.Content.ListItem -> + RemoteText("• ${c.text}", color = MaterialTheme.colorScheme.onPrimary.rc) + } + }*/ + } + } +} diff --git a/android/lib/feature/settings/impl/src/main/kotlin/net/mullvad/mullvadvpn/feature/settings/impl/server/model/FaqBlock.kt b/android/lib/feature/settings/impl/src/main/kotlin/net/mullvad/mullvadvpn/feature/settings/impl/server/model/FaqBlock.kt new file mode 100644 index 0000000000..f71fa328bf --- /dev/null +++ b/android/lib/feature/settings/impl/src/main/kotlin/net/mullvad/mullvadvpn/feature/settings/impl/server/model/FaqBlock.kt @@ -0,0 +1,10 @@ +package net.mullvad.mullvadvpn.feature.settings.impl.server.model + +sealed class FaqBlock { + data class Question(val title: String, val content: List<Content>) : FaqBlock() + + sealed class Content { + data class Paragraph(val text: String) : Content() + data class ListItem(val text: String) : Content() + } +} diff --git a/android/lib/feature/settings/impl/src/main/kotlin/net/mullvad/mullvadvpn/feature/settings/impl/server/model/FaqItem.kt b/android/lib/feature/settings/impl/src/main/kotlin/net/mullvad/mullvadvpn/feature/settings/impl/server/model/FaqItem.kt new file mode 100644 index 0000000000..65aeac5529 --- /dev/null +++ b/android/lib/feature/settings/impl/src/main/kotlin/net/mullvad/mullvadvpn/feature/settings/impl/server/model/FaqItem.kt @@ -0,0 +1,3 @@ +package net.mullvad.mullvadvpn.feature.settings.impl.server.model + +data class FaqItem(val question: String, val answer: String) diff --git a/android/lib/feature/settings/impl/src/main/kotlin/net/mullvad/mullvadvpn/feature/settings/impl/server/model/RichText.kt b/android/lib/feature/settings/impl/src/main/kotlin/net/mullvad/mullvadvpn/feature/settings/impl/server/model/RichText.kt new file mode 100644 index 0000000000..aa73ea99f2 --- /dev/null +++ b/android/lib/feature/settings/impl/src/main/kotlin/net/mullvad/mullvadvpn/feature/settings/impl/server/model/RichText.kt @@ -0,0 +1,10 @@ +package net.mullvad.mullvadvpn.feature.settings.impl.server.model + +data class RichText( + val parts: List<Part> +) { + sealed class Part { + data class Text(val text: String) : Part() + data class Link(val text: String, val url: String) : Part() + } +} |
