diff --git a/composeApp/build.gradle.kts b/composeApp/build.gradle.kts index 10c2e2612..d0b2f008e 100644 --- a/composeApp/build.gradle.kts +++ b/composeApp/build.gradle.kts @@ -95,6 +95,8 @@ kotlin { implementation(libs.russhwolf.settings) implementation(libs.about.libraries.compose.m3) + + implementation(libs.touchlab.kermit) } commonTest.dependencies { implementation(libs.kotlin.test.core) @@ -104,6 +106,7 @@ kotlin { implementation(libs.jetbrains.compose.preview) implementation(libs.androidx.activity) implementation(libs.androidx.preference) + implementation(libs.androidx.browser) } val androidDeviceTest by getting { dependencies { diff --git a/composeApp/src/androidMain/kotlin/net/newpipe/app/platform/AndroidShareHandler.kt b/composeApp/src/androidMain/kotlin/net/newpipe/app/platform/AndroidShareHandler.kt new file mode 100644 index 000000000..8d55611a2 --- /dev/null +++ b/composeApp/src/androidMain/kotlin/net/newpipe/app/platform/AndroidShareHandler.kt @@ -0,0 +1,36 @@ +/* + * SPDX-FileCopyrightText: 2026 NewPipe e.V. + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package net.newpipe.app.platform + +import android.content.Context +import android.content.Intent +import androidx.browser.customtabs.CustomTabsIntent +import androidx.core.net.toUri +import co.touchlab.kermit.Logger +import org.koin.core.annotation.Singleton + +/** + * Handles sharing of data and information on Android + * @property context Context on Android, injected automatically by Koin + */ +@Singleton(binds = [ShareHandler::class]) +class AndroidShareHandler(private val context: Context) : ShareHandler { + + override fun openUrlInBrowser(url: String) { + try { + CustomTabsIntent.Builder() + .build() + .also { customIntent -> + customIntent.intent.addFlags( + Intent.FLAG_ACTIVITY_NEW_TASK + ) + } + .launchUrl(context, url.toUri()) + } catch (exception: Exception) { + Logger.e(messageString = "Failed to share URL", throwable = exception) + } + } +} diff --git a/composeApp/src/commonMain/kotlin/net/newpipe/app/platform/PlatformModule.kt b/composeApp/src/commonMain/kotlin/net/newpipe/app/platform/PlatformModule.kt new file mode 100644 index 000000000..07026defc --- /dev/null +++ b/composeApp/src/commonMain/kotlin/net/newpipe/app/platform/PlatformModule.kt @@ -0,0 +1,20 @@ +/* + * SPDX-FileCopyrightText: 2026 NewPipe e.V. + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package net.newpipe.app.platform + +import org.koin.core.annotation.ComponentScan +import org.koin.core.annotation.Configuration +import org.koin.core.annotation.Module + +/** + * Module to access various common implementations that are dependent upon platform. + * See individual interfaces in this package and their implementations on platform packages + * for the declarations included in this module. + */ +@Module +@ComponentScan +@Configuration +object PlatformModule diff --git a/composeApp/src/commonMain/kotlin/net/newpipe/app/platform/ShareHandler.kt b/composeApp/src/commonMain/kotlin/net/newpipe/app/platform/ShareHandler.kt new file mode 100644 index 000000000..5f7ad7826 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/net/newpipe/app/platform/ShareHandler.kt @@ -0,0 +1,14 @@ +/* + * SPDX-FileCopyrightText: 2026 NewPipe e.V. + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package net.newpipe.app.platform + +/** + * Helper methods related to sharing of data and information + * See individual platform classes for real implementation. + */ +interface ShareHandler { + fun openUrlInBrowser(url: String) +} diff --git a/composeApp/src/commonMain/kotlin/net/newpipe/app/screen/about/AboutPage.kt b/composeApp/src/commonMain/kotlin/net/newpipe/app/screen/about/AboutPage.kt index 327171a2c..c4a67351f 100644 --- a/composeApp/src/commonMain/kotlin/net/newpipe/app/screen/about/AboutPage.kt +++ b/composeApp/src/commonMain/kotlin/net/newpipe/app/screen/about/AboutPage.kt @@ -93,7 +93,10 @@ private val DEFAULT_LINKS = listOf( ) @Composable -fun AboutPage(links: List = DEFAULT_LINKS) { +fun AboutPage( + links: List = DEFAULT_LINKS, + onOpenUrl: (url: String) -> Unit = {} +) { LazyColumn( modifier = Modifier.fillMaxWidth(), contentPadding = PaddingValues(spaceNormal), @@ -137,7 +140,8 @@ fun AboutPage(links: List = DEFAULT_LINKS) { // Links about NewPipe items(items = links, key = { link -> link.url }) { link -> LinkListItem( - link = link + link = link, + onAction = { onOpenUrl(link.url) } ) } } diff --git a/composeApp/src/commonMain/kotlin/net/newpipe/app/screen/about/AboutScreen.kt b/composeApp/src/commonMain/kotlin/net/newpipe/app/screen/about/AboutScreen.kt index b2c6d0530..c1d80ff08 100644 --- a/composeApp/src/commonMain/kotlin/net/newpipe/app/screen/about/AboutScreen.kt +++ b/composeApp/src/commonMain/kotlin/net/newpipe/app/screen/about/AboutScreen.kt @@ -24,23 +24,30 @@ import androidx.compose.ui.tooling.preview.PreviewWrapper import androidx.compose.ui.util.fastForEachIndexed import kotlinx.coroutines.launch import net.newpipe.app.composable.TopAppBar +import net.newpipe.app.platform.ShareHandler import net.newpipe.app.preview.ThemePreviewProvider import org.jetbrains.compose.resources.stringResource import net.newpipe.app.screen.about.navigation.Page import newpipe.composeapp.generated.resources.Res import newpipe.composeapp.generated.resources.title_activity_about +import org.koin.compose.koinInject @Composable -fun AboutScreen(onNavigateUp: () -> Unit) { +fun AboutScreen( + onNavigateUp: () -> Unit, + shareHandler: ShareHandler = koinInject() +) { ScreenContent( - onNavigateUp = onNavigateUp + onNavigateUp = onNavigateUp, + onOpenUrl = { url -> shareHandler.openUrlInBrowser(url) } ) } @Composable private fun ScreenContent( pages: List = listOf(Page.ABOUT, Page.LICENSE), - onNavigateUp: () -> Unit = {} + onNavigateUp: () -> Unit = {}, + onOpenUrl: (url: String) -> Unit = {} ) { Scaffold( topBar = { @@ -78,7 +85,7 @@ private fun ScreenContent( HorizontalPager(state = pagerState, modifier = Modifier.fillMaxSize()) { page -> when (pages[page]) { - Page.ABOUT -> AboutPage() + Page.ABOUT -> AboutPage(onOpenUrl = onOpenUrl) Page.LICENSE -> LicensePage() } } diff --git a/composeApp/src/iosMain/kotlin/net/newpipe/app/platform/IosShareHandler.kt b/composeApp/src/iosMain/kotlin/net/newpipe/app/platform/IosShareHandler.kt new file mode 100644 index 000000000..9297cdb32 --- /dev/null +++ b/composeApp/src/iosMain/kotlin/net/newpipe/app/platform/IosShareHandler.kt @@ -0,0 +1,30 @@ +/* + * SPDX-FileCopyrightText: 2026 NewPipe e.V. + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package net.newpipe.app.platform + +import co.touchlab.kermit.Logger +import org.koin.core.annotation.Singleton +import platform.Foundation.NSURL +import platform.SafariServices.SFSafariViewController +import platform.UIKit.UIApplication + +/** + * Handles sharing of data and information on iOS + */ +@Singleton(binds = [ShareHandler::class]) +class IosShareHandler : ShareHandler { + + override fun openUrlInBrowser(url: String) { + val nsUrl = NSURL.URLWithString(url) + if (nsUrl == null) { + Logger.e(messageString = "Unable to open malformatted URL, bailing out!") + } else { + val safariVC = SFSafariViewController(uRL = nsUrl) + val rootVC = UIApplication.sharedApplication.keyWindow?.rootViewController + rootVC?.presentViewController(safariVC, animated = true, completion = null) + } + } +} diff --git a/composeApp/src/jvmMain/kotlin/net/newpipe/app/platform/JvmShareHandler.kt b/composeApp/src/jvmMain/kotlin/net/newpipe/app/platform/JvmShareHandler.kt new file mode 100644 index 000000000..4d7923e8a --- /dev/null +++ b/composeApp/src/jvmMain/kotlin/net/newpipe/app/platform/JvmShareHandler.kt @@ -0,0 +1,28 @@ +/* + * SPDX-FileCopyrightText: 2026 NewPipe e.V. + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package net.newpipe.app.platform + +import co.touchlab.kermit.Logger +import org.koin.core.annotation.ComponentScan +import org.koin.core.annotation.Configuration +import org.koin.core.annotation.Module +import org.koin.core.annotation.Singleton +import java.awt.Desktop +import java.net.URI + +/** + * Handles sharing of data and information on JVM + */ +@Singleton(binds = [ShareHandler::class]) +class JvmShareHandler : ShareHandler { + + override fun openUrlInBrowser(url: String) { + when { + Desktop.isDesktopSupported() -> Desktop.getDesktop().browse(URI(url)) + else -> Logger.e(messageString = "Unsupported platform! Cannot open URL in browser") + } + } +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index a7e959b50..7e0a2cc43 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -13,6 +13,7 @@ assertj = "3.27.7" autoservice-google = "1.1.1" autoservice-zacsweers = "1.2.0" bridge = "v2.0.2" +browser = "1.10.0" cardview = "1.0.0" checkstyle = "13.4.0" coil = "3.4.0" @@ -29,6 +30,7 @@ groupie = "2.10.1" jsoup = "1.22.2" junit = "4.13.2" junit-ext = "1.3.0" +kermit = "2.1.0" koin = "4.2.1" koin-plugin = "1.0.0-RC2" kotlin = "2.3.21" @@ -81,6 +83,7 @@ acra-core = { module = "ch.acra:acra-core", version.ref = "acra" } android-desugar = { module = "com.android.tools:desugar_jdk_libs_nio", version.ref = "desugar" } androidx-activity = { module = "androidx.activity:activity-compose", version.ref = "activity" } androidx-appcompat = { module = "androidx.appcompat:appcompat", version.ref = "appcompat" } +androidx-browser = { module = "androidx.browser:browser", version.ref = "browser" } androidx-cardview = { module = "androidx.cardview:cardview", version.ref = "cardview" } androidx-compose-test-ui-junit = { module = "androidx.compose.ui:ui-test-junit4-android", version.ref = "compose" } androidx-compose-test-ui-manifest = { module = "androidx.compose.ui:ui-test-manifest", version.ref = "compose" } @@ -129,9 +132,9 @@ jetbrains-compose-material3 = { module = "org.jetbrains.compose.material3:materi jetbrains-compose-preview = { module = "org.jetbrains.compose.ui:ui-tooling-preview", version.ref = "multiplatform" } jetbrains-compose-resources = { module = "org.jetbrains.compose.components:components-resources", version.ref = "multiplatform" } jetbrains-compose-runtime = { module = "org.jetbrains.compose.runtime:runtime", version.ref = "multiplatform" } +jetbrains-compose-test-ui = { module = "org.jetbrains.compose.ui:ui-test", version.ref = "multiplatform" } jetbrains-compose-tooling = { module = "org.jetbrains.compose.ui:ui-tooling", version.ref = "multiplatform" } jetbrains-compose-ui = { module = "org.jetbrains.compose.ui:ui", version.ref = "multiplatform" } -jetbrains-compose-test-ui = { module = "org.jetbrains.compose.ui:ui-test", version.ref = "multiplatform" } jetbrains-coroutines-swing = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-swing", version.ref = "coroutines" } jetbrains-lifecycle-navigation3 = { module = "org.jetbrains.androidx.lifecycle:lifecycle-viewmodel-navigation3", version.ref = "lifecycle" } jetbrains-lifecycle-viewmodel = { module = "org.jetbrains.androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "lifecycle" } @@ -162,6 +165,7 @@ squareup-leakcanary-core = { module = "com.squareup.leakcanary:leakcanary-androi squareup-leakcanary-plumber = { module = "com.squareup.leakcanary:plumber-android", version.ref = "leakcanary" } squareup-leakcanary-watcher = { module = "com.squareup.leakcanary:leakcanary-object-watcher-android", version.ref = "leakcanary" } squareup-okhttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" } +touchlab-kermit = { module = "co.touchlab:kermit", version.ref = "kermit" } zacsweers-autoservice-compiler = { module = "dev.zacsweers.autoservice:auto-service-ksp", version.ref = "autoservice-zacsweers" } [plugins]