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" }