diff --git a/build.gradle.kts b/build.gradle.kts index ca5003b2c..1cfc429ca 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -14,5 +14,6 @@ plugins { alias(libs.plugins.jetbrains.compose.hotreload) apply false alias(libs.plugins.google.ksp) apply false alias(libs.plugins.jetbrains.kotlin.parcelize) apply false + alias(libs.plugins.jetbrains.kotlin.serialization) apply false alias(libs.plugins.sonarqube) apply false } diff --git a/composeApp/build.gradle.kts b/composeApp/build.gradle.kts index 92e8dc7f1..308e8afed 100644 --- a/composeApp/build.gradle.kts +++ b/composeApp/build.gradle.kts @@ -12,7 +12,7 @@ plugins { alias(libs.plugins.jetbrains.compose.multiplatform) alias(libs.plugins.jetbrains.compose.hotreload) alias(libs.plugins.google.ksp) - alias(libs.plugins.jetbrains.kotlin.parcelize) + alias(libs.plugins.jetbrains.kotlin.serialization) } kotlin { @@ -56,6 +56,10 @@ kotlin { // Settings implementation(libs.russhwolf.settings) + + // Navigation + implementation(libs.jetbrains.navigation3.ui) + implementation(libs.jetbrains.serialization.json) } commonTest.dependencies { implementation(libs.kotlin.test) @@ -68,7 +72,7 @@ kotlin { } jvmMain.dependencies { implementation(compose.desktop.currentOs) - implementation(libs.jetbrains.kotlinx.coroutinesSwing) + implementation(libs.jetbrains.coroutines.swing) } } diff --git a/composeApp/src/commonMain/composeResources/values/strings.xml b/composeApp/src/commonMain/composeResources/values/strings.xml index db4b09e00..a64bf61a5 100644 --- a/composeApp/src/commonMain/composeResources/values/strings.xml +++ b/composeApp/src/commonMain/composeResources/values/strings.xml @@ -5,4 +5,8 @@ --> NewPipe + + + About \u0026 FAQ + Licenses diff --git a/composeApp/src/commonMain/kotlin/net/newpipe/app/navigation/MainNavDisplay.kt b/composeApp/src/commonMain/kotlin/net/newpipe/app/navigation/MainNavDisplay.kt new file mode 100644 index 000000000..f6c47e38e --- /dev/null +++ b/composeApp/src/commonMain/kotlin/net/newpipe/app/navigation/MainNavDisplay.kt @@ -0,0 +1,28 @@ +/* + * SPDX-FileCopyrightText: 2026 NewPipe e.V. + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package net.newpipe.app.navigation + +import androidx.compose.runtime.Composable +import androidx.navigation3.runtime.NavKey +import androidx.navigation3.runtime.entryProvider +import androidx.navigation3.runtime.rememberNavBackStack +import androidx.navigation3.ui.NavDisplay + +/** + * Navigation display for compose screens + * @param startDestination Starting destination for the activity/app + */ +@Composable +fun MainNavDisplay(startDestination: NavKey) { + val backstack = rememberNavBackStack(Screen.config, startDestination) + + NavDisplay( + backStack = backstack, + entryProvider = entryProvider { + + } + ) +} diff --git a/composeApp/src/commonMain/kotlin/net/newpipe/app/navigation/Screen.kt b/composeApp/src/commonMain/kotlin/net/newpipe/app/navigation/Screen.kt new file mode 100644 index 000000000..5a8499adb --- /dev/null +++ b/composeApp/src/commonMain/kotlin/net/newpipe/app/navigation/Screen.kt @@ -0,0 +1,34 @@ +/* + * SPDX-FileCopyrightText: 2026 NewPipe e.V. + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package net.newpipe.app.navigation + +import androidx.navigation3.runtime.NavKey +import androidx.savedstate.serialization.SavedStateConfiguration +import kotlinx.serialization.Serializable +import kotlinx.serialization.modules.SerializersModule +import kotlinx.serialization.modules.polymorphic + +/** + * Destinations for navigation in compose + */ +@Serializable +sealed class Screen : NavKey { + + @Serializable + data object About: Screen() + + companion object { + val config = SavedStateConfiguration { + serializersModule = SerializersModule { + polymorphic(NavKey::class) { + // TODO: Add all subclasses using a for-each loop + subclass(About::class, About.serializer()) + } + } + } + } +} + diff --git a/composeApp/src/commonMain/kotlin/net/newpipe/app/preview/PreviewTemplate.kt b/composeApp/src/commonMain/kotlin/net/newpipe/app/preview/PreviewTemplate.kt new file mode 100644 index 000000000..2850c9814 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/net/newpipe/app/preview/PreviewTemplate.kt @@ -0,0 +1,17 @@ +/* + * SPDX-FileCopyrightText: 2026 NewPipe e.V. + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package net.newpipe.app.preview + +import androidx.compose.runtime.Composable +import net.newpipe.app.theme.AppTheme + +/** + * Template for previewing composable with defaults + */ +@Composable +fun PreviewTemplate(content: @Composable () -> Unit) { + AppTheme(content = content) +} diff --git a/composeApp/src/commonMain/kotlin/net/newpipe/app/screens/AboutScreen.kt b/composeApp/src/commonMain/kotlin/net/newpipe/app/screens/AboutScreen.kt new file mode 100644 index 000000000..f7bdd33fd --- /dev/null +++ b/composeApp/src/commonMain/kotlin/net/newpipe/app/screens/AboutScreen.kt @@ -0,0 +1,87 @@ +/* + * SPDX-FileCopyrightText: 2024 NewPipe contributors + * SPDX-FileCopyrightText: 2026 NewPipe e.V. + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package net.newpipe.app.screens + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.pager.HorizontalPager +import androidx.compose.foundation.pager.rememberPagerState +import androidx.compose.material3.Scaffold +import androidx.compose.material3.SecondaryTabRow +import androidx.compose.material3.Tab +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Modifier +import androidx.compose.ui.util.fastForEachIndexed +import kotlinx.coroutines.launch +import net.newpipe.app.preview.PreviewTemplate +import newpipe.composeapp.generated.resources.Res +import newpipe.composeapp.generated.resources.tab_about +import newpipe.composeapp.generated.resources.tab_licenses +import org.jetbrains.compose.resources.stringResource +import org.jetbrains.compose.ui.tooling.preview.Preview + +@Composable +fun AboutScreen() { + ScreenContent() +} + +@Composable +private fun ScreenContent(onNavigateUp: () -> Unit = {}) { + Scaffold { paddingValues -> + Column( + modifier = Modifier + .fillMaxSize() + .padding(paddingValues) + ) { + val pages = listOf(Res.string.tab_about, Res.string.tab_licenses) + val pagerState = rememberPagerState { pages.size } + val coroutineScope = rememberCoroutineScope() + + SecondaryTabRow( + modifier = Modifier.fillMaxWidth(), + selectedTabIndex = pagerState.currentPage + ) { + pages.fastForEachIndexed { index, pageId -> + Tab( + selected = pagerState.currentPage == index, + text = { + Text(text = stringResource(pageId)) + }, + onClick = { + coroutineScope.launch { + pagerState.animateScrollToPage(index) + } + } + ) + } + } + + HorizontalPager( + state = pagerState, + modifier = Modifier.fillMaxSize() + ) { page -> + if (page == 0) { + AboutTab() + } else { + LicenseTab() + } + } + } + } +} + +@Preview +@Composable +private fun AboutScreenPreview() { + PreviewTemplate { + ScreenContent() + } +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 9e0efbd77..d91578a7e 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -40,6 +40,7 @@ material = "1.11.0" # TODO: update to newer version after bug is fixed. See http media = "1.7.1" mockitoCore = "5.21.0" multiplatform = "1.9.3" +navigation3 = "1.0.0-alpha06" okhttp = "5.3.2" phoenix = "3.0.0" #noinspection NewerVersionAvailable,GradleDependency --> 2.8 is the last version, not 2.71828! @@ -52,6 +53,7 @@ runner = "1.7.0" rxandroid = "3.0.2" rxbinding = "4.0.0" rxjava = "3.1.12" +serialization = "1.9.0" settings = "1.3.0" sonarqube = "7.2.1.6560" statesaver = "1.4.1" # TODO: Drop because it is deprecated and incompatible with KSP2 @@ -116,8 +118,10 @@ google-exoplayer-smoothstreaming = { module = "com.google.android.exoplayer:exop google-exoplayer-ui = { module = "com.google.android.exoplayer:exoplayer-ui", version.ref = "exoplayer" } jakewharton-phoenix = { module = "com.jakewharton:process-phoenix", version.ref = "phoenix" } jakewharton-rxbinding = { module = "com.jakewharton.rxbinding4:rxbinding", version.ref = "rxbinding" } -jetbrains-kotlinx-coroutinesSwing = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-swing", version.ref = "coroutines" } +jetbrains-coroutines-swing = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-swing", version.ref = "coroutines" } jetbrains-lifecycle-viewmodel = { module = "org.jetbrains.androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "lifecycle-jetbrains" } +jetbrains-navigation3-ui = { module = "org.jetbrains.androidx.navigation3:navigation3-ui", version.ref = "navigation3" } +jetbrains-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "serialization" } jsoup = { module = "org.jsoup:jsoup", version.ref = "jsoup" } junit = { module = "junit:junit", version.ref = "junit" } koin-annotations = { module = "io.insert-koin:koin-annotations", version.ref = "koin-annotations" } @@ -160,4 +164,5 @@ jetbrains-kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version jetbrains-kotlin-kapt = { id = "org.jetbrains.kotlin.kapt", version.ref = "kotlin" } # Needed for statesaver jetbrains-kotlin-multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" } jetbrains-kotlin-parcelize = { id = "org.jetbrains.kotlin.plugin.parcelize", version.ref = "kotlin" } +jetbrains-kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } sonarqube = { id = "org.sonarqube", version.ref = "sonarqube" }