diff --git a/buildSrc/src/main/kotlin/ProjectConfig.kt b/buildSrc/src/main/kotlin/ProjectConfig.kt index 6f6f42f73..3462b2837 100644 --- a/buildSrc/src/main/kotlin/ProjectConfig.kt +++ b/buildSrc/src/main/kotlin/ProjectConfig.kt @@ -15,6 +15,6 @@ const val NEWPIPE_APPLICATION_ID_OLD = "org.schabi.newpipe" const val NEWPIPE_APPLICATION_ID_NEW = "net.newpipe.app" // Sulkta fork — Straw -const val STRAW_VERSION_CODE = 12 -const val STRAW_VERSION_NAME = "0.1.0-X" +const val STRAW_VERSION_CODE = 13 +const val STRAW_VERSION_NAME = "0.1.0-Y" const val STRAW_APPLICATION_ID = "com.sulkta.straw" diff --git a/strawApp/src/main/kotlin/com/sulkta/straw/StrawHome.kt b/strawApp/src/main/kotlin/com/sulkta/straw/StrawHome.kt index c9d940c32..31e1f2452 100644 --- a/strawApp/src/main/kotlin/com/sulkta/straw/StrawHome.kt +++ b/strawApp/src/main/kotlin/com/sulkta/straw/StrawHome.kt @@ -36,7 +36,11 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.ModalDrawerSheet import androidx.compose.material3.ModalNavigationDrawer import androidx.compose.material3.NavigationDrawerItem +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Box import androidx.compose.material3.Scaffold +import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.material3.TopAppBar @@ -122,16 +126,6 @@ fun StrawHome( modifier = Modifier.padding(horizontal = 12.dp), ) HorizontalDivider(modifier = Modifier.padding(vertical = 12.dp)) - NavigationDrawerItem( - label = { Text("Search") }, - icon = { Text("🔍") }, - selected = false, - onClick = { - scope.launch { drawerState.close() } - onOpenSearch() - }, - modifier = Modifier.padding(horizontal = 12.dp), - ) NavigationDrawerItem( label = { Text("Settings") }, icon = { Text("⚙") }, @@ -149,13 +143,34 @@ fun StrawHome( topBar = { TopAppBar( title = { - Text( - text = when (view) { - HomeView.Subs -> "Subscriptions" - HomeView.History -> "History" - }, - style = MaterialTheme.typography.titleLarge, - ) + // Search-pill in the title slot — tap takes you to the + // full search screen with the field auto-focused. Same + // idea as YT's mobile top bar. + Surface( + modifier = Modifier + .fillMaxWidth() + .padding(end = 8.dp) + .height(40.dp) + .clip(RoundedCornerShape(20.dp)) + .clickable(onClick = onOpenSearch), + color = MaterialTheme.colorScheme.surfaceVariant, + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.padding(horizontal = 14.dp), + ) { + Text( + "🔍", + style = MaterialTheme.typography.bodyMedium, + ) + Spacer(modifier = Modifier.width(10.dp)) + Text( + "Search YouTube", + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + } }, navigationIcon = { IconButton(onClick = { scope.launch { drawerState.open() } }) { @@ -171,6 +186,16 @@ fun StrawHome( .padding(padding) .padding(horizontal = 20.dp, vertical = 8.dp), ) { + // Section header — clarifies which view is current. + Text( + text = when (view) { + HomeView.Subs -> "Latest from your subs" + HomeView.History -> "History" + }, + style = MaterialTheme.typography.titleLarge, + fontWeight = FontWeight.SemiBold, + ) + Spacer(modifier = Modifier.height(12.dp)) when (view) { HomeView.Subs -> SubsPane(onOpenChannel, onOpenVideo, feedVm) HomeView.History -> HistoryPane(onOpenVideo) @@ -222,10 +247,16 @@ private fun SubsPane( Column { if (subs.isEmpty()) { Text( - "No subscriptions yet. Tap Search in the menu, find a channel, and Subscribe.", + "No subscriptions yet.", style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant, ) + Spacer(modifier = Modifier.height(4.dp)) + Text( + "Tap the search bar above, find a channel, and Subscribe.", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) return@Column } diff --git a/strawApp/src/main/kotlin/com/sulkta/straw/feature/channel/ChannelScreen.kt b/strawApp/src/main/kotlin/com/sulkta/straw/feature/channel/ChannelScreen.kt index 6b4eafcf8..9d3e57143 100644 --- a/strawApp/src/main/kotlin/com/sulkta/straw/feature/channel/ChannelScreen.kt +++ b/strawApp/src/main/kotlin/com/sulkta/straw/feature/channel/ChannelScreen.kt @@ -13,6 +13,7 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.statusBarsPadding import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding @@ -63,18 +64,18 @@ fun ChannelScreen( when { state.loading -> Box( - modifier = Modifier.fillMaxSize(), + modifier = Modifier.fillMaxSize().statusBarsPadding(), contentAlignment = Alignment.Center, ) { CircularProgressIndicator() } state.error != null -> Box( - modifier = Modifier.fillMaxSize().padding(16.dp), + modifier = Modifier.fillMaxSize().statusBarsPadding().padding(16.dp), contentAlignment = Alignment.Center, ) { Text("error: ${state.error}", color = MaterialTheme.colorScheme.error) } - else -> LazyColumn(modifier = Modifier.fillMaxSize()) { + else -> LazyColumn(modifier = Modifier.fillMaxSize().statusBarsPadding()) { item { state.banner?.let { b -> AsyncImage( diff --git a/strawApp/src/main/kotlin/com/sulkta/straw/feature/detail/VideoDetailScreen.kt b/strawApp/src/main/kotlin/com/sulkta/straw/feature/detail/VideoDetailScreen.kt index 33d1c165a..be0fdf8c1 100644 --- a/strawApp/src/main/kotlin/com/sulkta/straw/feature/detail/VideoDetailScreen.kt +++ b/strawApp/src/main/kotlin/com/sulkta/straw/feature/detail/VideoDetailScreen.kt @@ -18,6 +18,7 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.statusBarsPadding import androidx.compose.foundation.layout.width import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape @@ -74,6 +75,7 @@ fun VideoDetailScreen( Column( modifier = Modifier .fillMaxSize() + .statusBarsPadding() .verticalScroll(rememberScrollState()) .padding(16.dp), ) { @@ -121,7 +123,40 @@ fun VideoDetailScreen( ) } } - Spacer(modifier = Modifier.height(12.dp)) + Spacer(modifier = Modifier.height(20.dp)) + + // ── Recommended ────────────────────────────────────────── + if (d.related.isNotEmpty()) { + Text( + "Recommended", + style = MaterialTheme.typography.titleSmall, + fontWeight = FontWeight.SemiBold, + ) + Spacer(modifier = Modifier.height(8.dp)) + d.related.take(20).forEach { rel -> + RelatedRow(rel) { onOpenVideo(rel.url, rel.title) } + androidx.compose.material3.HorizontalDivider() + } + Spacer(modifier = Modifier.height(20.dp)) + } + + // ── More from ───────────────────────────────── + if (d.moreFromChannel.isNotEmpty()) { + Text( + if (d.uploader.isBlank()) "More from this channel" + else "More from ${d.uploader}", + style = MaterialTheme.typography.titleSmall, + fontWeight = FontWeight.SemiBold, + ) + Spacer(modifier = Modifier.height(8.dp)) + d.moreFromChannel.take(20).forEach { item -> + RelatedRow(item) { onOpenVideo(item.url, item.title) } + androidx.compose.material3.HorizontalDivider() + } + Spacer(modifier = Modifier.height(20.dp)) + } + + // ── Video details ──────────────────────────────────────── Text( text = d.title, style = MaterialTheme.typography.titleLarge, @@ -189,6 +224,15 @@ fun VideoDetailScreen( } Spacer(modifier = Modifier.height(16.dp)) + Text("Description", style = MaterialTheme.typography.titleSmall, fontWeight = FontWeight.SemiBold) + Spacer(modifier = Modifier.height(8.dp)) + // AUD-MED: cap input length before regex passes — defends + // against ANR on multi-MB descriptions. + Text( + text = stripHtml(d.description.take(20_000)).take(2000), + style = MaterialTheme.typography.bodySmall, + ) + if (showDownloadDialog) { val info = state.streamInfo AlertDialog( @@ -249,28 +293,6 @@ fun VideoDetailScreen( ) } - Text("Description", style = MaterialTheme.typography.titleSmall, fontWeight = FontWeight.SemiBold) - Spacer(modifier = Modifier.height(8.dp)) - // AUD-MED: cap input length before regex passes — defends - // against ANR on multi-MB descriptions. - Text( - text = stripHtml(d.description.take(20_000)).take(2000), - style = MaterialTheme.typography.bodySmall, - ) - - if (d.related.isNotEmpty()) { - Spacer(modifier = Modifier.height(24.dp)) - Text( - "Related", - style = MaterialTheme.typography.titleSmall, - fontWeight = FontWeight.SemiBold, - ) - Spacer(modifier = Modifier.height(8.dp)) - d.related.take(20).forEach { rel -> - RelatedRow(rel) { onOpenVideo(rel.url, rel.title) } - androidx.compose.material3.HorizontalDivider() - } - } } } } diff --git a/strawApp/src/main/kotlin/com/sulkta/straw/feature/detail/VideoDetailViewModel.kt b/strawApp/src/main/kotlin/com/sulkta/straw/feature/detail/VideoDetailViewModel.kt index 0557c4051..06ee83b6c 100644 --- a/strawApp/src/main/kotlin/com/sulkta/straw/feature/detail/VideoDetailViewModel.kt +++ b/strawApp/src/main/kotlin/com/sulkta/straw/feature/detail/VideoDetailViewModel.kt @@ -20,6 +20,11 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import org.schabi.newpipe.extractor.NewPipe +import org.schabi.newpipe.extractor.ServiceList +import org.schabi.newpipe.extractor.channel.ChannelInfo +import org.schabi.newpipe.extractor.channel.tabs.ChannelTabInfo +import org.schabi.newpipe.extractor.channel.tabs.ChannelTabs import org.schabi.newpipe.extractor.stream.StreamInfo import org.schabi.newpipe.extractor.stream.StreamInfoItem @@ -34,6 +39,9 @@ data class VideoDetail( val ryd: RydVotes? = null, val sbSegmentCount: Int = 0, val related: List = emptyList(), + /** Other videos from the same channel — separate from related (which is YT's + * algo). Anchored to the uploader the user chose; matches the sub-feed ethos. */ + val moreFromChannel: List = emptyList(), ) data class VideoDetailUiState( @@ -98,6 +106,38 @@ class VideoDetailViewModel : ViewModel() { ) } ?: emptyList() + // More from this channel — anchored to the uploader the user + // already chose. Best-effort: empty if the fetch fails so the + // detail screen still renders. Filters out the current video. + val moreFromChannel: List = + if (info.uploaderUrl.isNullOrBlank()) emptyList() + else withContext(Dispatchers.IO) { + runCatching { + val service = NewPipe.getService(ServiceList.YouTube.serviceId) + val ch = ChannelInfo.getInfo(service, info.uploaderUrl) + val videosTab = ch.tabs.firstOrNull { + it.contentFilters.contains(ChannelTabs.VIDEOS) + } ?: ch.tabs.firstOrNull() + if (videosTab == null) emptyList() + else ChannelTabInfo.getInfo(service, videosTab) + .relatedItems + .filterIsInstance() + .filter { it.url != streamUrl } + .take(20) + .map { si -> + com.sulkta.straw.feature.search.StreamItem( + url = si.url, + title = si.name ?: "(no title)", + uploader = si.uploaderName ?: uploader, + uploaderUrl = si.uploaderUrl ?: info.uploaderUrl, + thumbnail = bestThumbnail(si.thumbnails), + durationSeconds = si.duration, + viewCount = si.viewCount, + ) + } + }.getOrDefault(emptyList()) + } + _ui.value = VideoDetailUiState( loading = false, detail = VideoDetail( @@ -111,6 +151,7 @@ class VideoDetailViewModel : ViewModel() { ryd = ryd, sbSegmentCount = sbCount, related = related, + moreFromChannel = moreFromChannel, ), streamInfo = info, ) diff --git a/strawApp/src/main/kotlin/com/sulkta/straw/feature/search/SearchScreen.kt b/strawApp/src/main/kotlin/com/sulkta/straw/feature/search/SearchScreen.kt index 9801814f3..a6809ebc4 100644 --- a/strawApp/src/main/kotlin/com/sulkta/straw/feature/search/SearchScreen.kt +++ b/strawApp/src/main/kotlin/com/sulkta/straw/feature/search/SearchScreen.kt @@ -16,6 +16,7 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.statusBarsPadding import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items @@ -54,7 +55,7 @@ fun SearchScreen( val state by vm.ui.collectAsStateWithLifecycle() val recentSearches by History.get().searches.collectAsState() - Column(modifier = Modifier.fillMaxSize().padding(16.dp)) { + Column(modifier = Modifier.fillMaxSize().statusBarsPadding().padding(16.dp)) { OutlinedTextField( value = state.query, onValueChange = vm::onQueryChange, diff --git a/strawApp/src/main/kotlin/com/sulkta/straw/feature/settings/SettingsScreen.kt b/strawApp/src/main/kotlin/com/sulkta/straw/feature/settings/SettingsScreen.kt index b426a9d9e..f090fc99c 100644 --- a/strawApp/src/main/kotlin/com/sulkta/straw/feature/settings/SettingsScreen.kt +++ b/strawApp/src/main/kotlin/com/sulkta/straw/feature/settings/SettingsScreen.kt @@ -10,6 +10,7 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.statusBarsPadding import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding @@ -43,6 +44,7 @@ fun SettingsScreen() { Column( modifier = Modifier .fillMaxSize() + .statusBarsPadding() .verticalScroll(rememberScrollState()) .padding(horizontal = 20.dp, vertical = 16.dp), ) {