diff --git a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/App.kt b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/App.kt index cc578a25..66d14649 100644 --- a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/App.kt +++ b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/App.kt @@ -19,6 +19,7 @@ package dev.krtirtho.spotube import androidx.compose.animation.ExperimentalSharedTransitionApi import androidx.compose.animation.SharedTransitionLayout +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.Composable @@ -41,6 +42,7 @@ import dev.krtirtho.spotube.core.ui.theming.SpotubeTheme import dev.krtirtho.spotube.modules.settings.SettingsRepository import dev.krtirtho.spotube.modules.settings.UserSettings import dev.krtirtho.spotube.modules.shell.AppShell +import dev.krtirtho.spotube.modules.webview.WebViewScreen import dev.krtirtho.spotube.resources.iconsax.Iconsax import dev.krtirtho.spotube.resources.iconsax.IconsaxHome import dev.krtirtho.spotube.resources.iconsax.IconsaxHomeBroken @@ -89,7 +91,11 @@ val tabs = listOf( ) ) -@OptIn(KoinExperimentalAPI::class, ExperimentalCoroutinesApi::class, ExperimentalSharedTransitionApi::class) +@OptIn( + KoinExperimentalAPI::class, + ExperimentalCoroutinesApi::class, + ExperimentalSharedTransitionApi::class +) @Composable fun App( content: @Composable () -> Unit = {} @@ -108,19 +114,29 @@ fun App( SpotubeTheme(settings = userSettings) { val baseUITheme = rememberBaseUITheme() CompositionLocalProvider(LocalBaseUITheme provides baseUITheme) { - AppShell(navigator, navigationState) { - SharedTransitionLayout { - CompositionLocalProvider( - LocalSharedTransitionScope provides this@SharedTransitionLayout, - ) { - Column { - NavDisplay( - modifier = Modifier.fillMaxSize(), - onBack = navigator::pop, - entries = navigationState.toEntries(koinEntryProvider()) - ) + Box(modifier = Modifier.fillMaxSize()) { + val currentRoute = navigationState.backStacks[navigationState.topLevelRoute]?.last() + + Box(Modifier.fillMaxSize()) { + AppShell(navigator, navigationState) { + SharedTransitionLayout { + CompositionLocalProvider( + LocalSharedTransitionScope provides this@SharedTransitionLayout, + ) { + Column { + NavDisplay( + modifier = Modifier.fillMaxSize(), + onBack = navigator::pop, + entries = navigationState.toEntries(koinEntryProvider()) + ) + } + } } } + when (currentRoute) { + Routes.WebView -> WebViewScreen(koinInject()) + else -> {} + } } } content() diff --git a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/navigation/NavigationModule.kt b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/navigation/NavigationModule.kt index 8d059071..039e17c6 100644 --- a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/navigation/NavigationModule.kt +++ b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/navigation/NavigationModule.kt @@ -18,9 +18,8 @@ package dev.krtirtho.spotube.core.navigation import androidx.navigation3.runtime.NavKey -import dev.krtirtho.spotube.modules.artist.ArtistScreen import dev.krtirtho.spotube.modules.album.AlbumScreen -import dev.krtirtho.spotube.modules.artist.ArtistViewModel +import dev.krtirtho.spotube.modules.artist.ArtistScreen import dev.krtirtho.spotube.modules.blacklist.BlacklistScreen import dev.krtirtho.spotube.modules.home.HomeScreen import dev.krtirtho.spotube.modules.library.LibraryScreen @@ -31,7 +30,6 @@ import dev.krtirtho.spotube.modules.saved_tracks.SAVED_TRACKS_COLLECTION_ID import dev.krtirtho.spotube.modules.saved_tracks.SavedTracksScreen import dev.krtirtho.spotube.modules.search.SearchScreen import dev.krtirtho.spotube.modules.settings.SettingsScreen -import dev.krtirtho.spotube.modules.webview.WebViewScreen import kotlinx.serialization.Serializable import org.koin.compose.viewmodel.koinViewModel import org.koin.core.annotation.KoinExperimentalAPI @@ -97,9 +95,7 @@ val navigationModule = module { navigation { PluginScreen(pluginManager = get()) } - navigation { - WebViewScreen(get()) - } + navigation {} navigation { PlaylistScreen( playlistId = it.playlistId, diff --git a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/webview/PlatformWebviewScreen.kt b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/webview/PlatformWebviewScreen.kt index 5d30fbc3..93ecbd47 100644 --- a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/webview/PlatformWebviewScreen.kt +++ b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/webview/PlatformWebviewScreen.kt @@ -19,6 +19,7 @@ package dev.krtirtho.spotube.core.webview import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.fillMaxSize @@ -51,6 +52,7 @@ import compose.icons.feathericons.ChevronLeft import compose.icons.feathericons.ChevronRight import compose.icons.feathericons.X import dev.krtirtho.spotube.core.tools.user_agents.UserAgents +import dev.krtirtho.spotube.core.ui.component.ApplicationMainBar import dev.nucleusframework.webview.jsbridge.IJsMessageHandler import dev.nucleusframework.webview.jsbridge.JsMessage import dev.nucleusframework.webview.jsbridge.rememberWebViewJsBridge @@ -149,64 +151,63 @@ fun PlatformWebViewScreen(webViewController: WebViewController) { Scaffold( contentWindowInsets = WindowInsets.statusBars, topBar = { - Row( - modifier = Modifier.fillMaxWidth().statusBarsPadding().height(56.dp), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically - ) { - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.Start, - modifier = Modifier.height(56.dp) - ) { - IconButton( - onClick = { - navigator.navigateBack() - }, enabled = navigator.canGoBack + ApplicationMainBar( + title = { + Row( + modifier = Modifier.fillMaxWidth().statusBarsPadding().height(56.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically ) { - Icon( - FeatherIcons.ChevronLeft, - contentDescription = "Go back to browser history" - ) - } - IconButton( - onClick = { - navigator.navigateForward() - }, enabled = navigator.canGoForward - ) { - Icon( - FeatherIcons.ChevronRight, - contentDescription = "Go forward to browser history" - ) + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Start, + modifier = Modifier.height(56.dp) + ) { + IconButton( + onClick = { + navigator.navigateBack() + }, enabled = navigator.canGoBack + ) { + Icon( + FeatherIcons.ChevronLeft, + contentDescription = "Go back to browser history" + ) + } + IconButton( + onClick = { + navigator.navigateForward() + }, enabled = navigator.canGoForward + ) { + Icon( + FeatherIcons.ChevronRight, + contentDescription = "Go forward to browser history" + ) + } + } + Surface( + modifier = Modifier.weight(1f).height(36.dp).padding(horizontal = 4.dp), + shape = RoundedCornerShape(18.dp), + color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f), + border = BorderStroke(1.dp, Color.Gray.copy(alpha = 0.5f)) + ) { + BasicTextField( + value = state.lastLoadedUrl ?: "", + onValueChange = {}, // Read-only + readOnly = true, + singleLine = true, + textStyle = MaterialTheme.typography.bodyMedium.copy( + color = MaterialTheme.colorScheme.onSurface, + textAlign = TextAlign.Start + ), + modifier = Modifier.fillMaxWidth().padding(horizontal = 12.dp) + .wrapContentHeight(Alignment.CenterVertically) + ) + } } } - Surface( - modifier = Modifier.weight(1f).height(36.dp).padding(horizontal = 4.dp), - shape = RoundedCornerShape(18.dp), - color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f), - border = BorderStroke(1.dp, Color.Gray.copy(alpha = 0.5f)) - ) { - BasicTextField( - value = state.lastLoadedUrl ?: "", - onValueChange = {}, // Read-only - readOnly = true, - singleLine = true, - textStyle = MaterialTheme.typography.bodyMedium.copy( - color = MaterialTheme.colorScheme.onSurface, - textAlign = TextAlign.Start - ), - modifier = Modifier.fillMaxWidth().padding(horizontal = 12.dp) - .wrapContentHeight(Alignment.CenterVertically) - ) - } - IconButton( - onClick = { - webViewController.closeWebview() - }) { - Icon(FeatherIcons.X, contentDescription = "Close WebView") - } - } - }) { innerPadding -> + ) + }) + { innerPadding -> WebView( state = state, modifier = Modifier.padding(innerPadding).fillMaxSize(), diff --git a/composeApp/src/jvmMain/kotlin/dev/krtirtho/spotube/main.kt b/composeApp/src/jvmMain/kotlin/dev/krtirtho/spotube/main.kt index e99718ed..3e597a48 100644 --- a/composeApp/src/jvmMain/kotlin/dev/krtirtho/spotube/main.kt +++ b/composeApp/src/jvmMain/kotlin/dev/krtirtho/spotube/main.kt @@ -26,6 +26,8 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.unit.dp import androidx.compose.ui.window.rememberWindowState +import com.sun.jna.Library +import com.sun.jna.Native import dev.krtirtho.spotube.core.di.initKoin import dev.krtirtho.spotube.core.newpipe.NewPipeDownloader import dev.krtirtho.spotube.core.paths.Paths @@ -55,8 +57,33 @@ private object KoinServicesProvider : KoinComponent { val settingsProvider: SettingsProvider get() = get() } +private interface LibC : Library { + fun setenv(name: String, value: String, overwrite: Int): Int + + companion object { + val INSTANCE: LibC = Native.load("c", LibC::class.java) + } +} + +/** + * WebKitGTK is only used on Linux (Windows = WebView2, macOS = WKWebView). + * The webview is created *after* the window is already mapped and the Tao + * render/swap loop is running. WebKitGTK's accelerated-compositing path then + * initialises its own GL context in-process, racing Tao's swap thread on the + * same Mesa display, which deterministically segfaults `libgallium` on the + * next Compose flush. Disabling WebKit's hardware-accelerated compositing + * (and its DMABUF renderer) removes that GL context entirely — login pages + * render fine in software. Must run before libwebkit2gtk is loaded. + */ +private fun disableWebKitGpuCompositing() { + if (!System.getProperty("os.name").lowercase().contains("linux")) return + LibC.INSTANCE.setenv("WEBKIT_DISABLE_COMPOSITING_MODE", "1", 1) + LibC.INSTANCE.setenv("WEBKIT_DISABLE_DMABUF_RENDERER", "1", 1) +} + @OptIn(ExperimentalComposeUiApi::class) fun main() { + disableWebKitGpuCompositing() FileKit.init(appId = "dev.krtirtho.spotube") initKoin() NewPipeDownloader.init(KoinPathsProvider.paths)