straw: infinite-scroll pagination for channel + search
All checks were successful
gitleaks / scan (push) Successful in 41s
All checks were successful
gitleaks / scan (push) Successful in 41s
Wire the new strawcore continuation fetchers through UniFFI and add
load-more-on-scroll to the Channel and Search screens — previously both
loaded only page 1 and stopped.
FFI (rust/strawcore): search() now returns Page{items, continuation};
channelInfo carries videos_continuation; new searchContinuation() and
channelVideosContinuation() suspend funs map the core ContinuationPage.
Channel + Search ViewModels: loadMore() fetches the next page, dedups by
url, advances the token, and stops when the token runs out or a page
yields zero net-new items (guards a looping continuation). Result-set
swaps (channel switch / new submit / cache preview) cancel the in-flight
page, and a token fence inside the state update prevents a stale page
being spliced into a replaced list. Screens add a near-end LazyColumn
trigger (rememberLazyListState + derivedStateOf) and a footer spinner.
This commit is contained in:
parent
6f2ae831cc
commit
4dfb2e1450
8 changed files with 2670 additions and 9 deletions
|
|
@ -2,11 +2,14 @@
|
|||
// Used by ChannelScreen (single-channel view) AND
|
||||
// SubscriptionFeedViewModel (which fans out across all subscriptions).
|
||||
|
||||
use strawcore_core::youtube::channel::{channel_info as core_channel_info, ChannelInfo as CoreInfo};
|
||||
use strawcore_core::youtube::channel::{
|
||||
channel_info as core_channel_info, channel_videos_continuation as core_channel_continuation,
|
||||
ChannelInfo as CoreInfo,
|
||||
};
|
||||
use strawcore_core::youtube::linkhandler::channel as core_link;
|
||||
|
||||
use crate::error::StrawcoreError;
|
||||
use crate::search::{from_core as search_from_core, SearchItem};
|
||||
use crate::search::{from_core as search_from_core, page_from_core, Page, SearchItem};
|
||||
|
||||
#[derive(Debug, Clone, uniffi::Record)]
|
||||
pub struct ChannelInfo {
|
||||
|
|
@ -19,6 +22,10 @@ pub struct ChannelInfo {
|
|||
pub description: String,
|
||||
/// Latest videos from the channel (Videos tab, newest first).
|
||||
pub videos: Vec<SearchItem>,
|
||||
/// Token to fetch the next page of the Videos tab via
|
||||
/// `channel_videos_continuation`. null = the channel has no more
|
||||
/// videos (or YT didn't hand out a continuation).
|
||||
pub videos_continuation: Option<String>,
|
||||
}
|
||||
|
||||
#[uniffi::export(async_runtime = "tokio")]
|
||||
|
|
@ -47,6 +54,23 @@ fn resolve_channel_identifier(
|
|||
})
|
||||
}
|
||||
|
||||
/// Fetch the next page of a channel's Videos tab from a continuation
|
||||
/// token returned on `ChannelInfo.videos_continuation` or a prior Page.
|
||||
#[uniffi::export(async_runtime = "tokio")]
|
||||
pub async fn channel_videos_continuation(token: String) -> Result<Page, StrawcoreError> {
|
||||
log::info!(
|
||||
"strawcore::channel_videos_continuation token_len={}",
|
||||
token.len()
|
||||
);
|
||||
crate::runtime::ensure_initialized();
|
||||
let page = tokio::task::spawn_blocking(move || core_channel_continuation(&token))
|
||||
.await
|
||||
.map_err(|e| StrawcoreError::Extractor {
|
||||
msg: format!("join: {e}"),
|
||||
})??;
|
||||
Ok(page_from_core(page))
|
||||
}
|
||||
|
||||
fn map_channel(c: CoreInfo) -> ChannelInfo {
|
||||
let avatar = c.avatars.last().map(|i| i.url().to_string());
|
||||
let banner = c.banners.last().map(|i| i.url().to_string());
|
||||
|
|
@ -59,5 +83,6 @@ fn map_channel(c: CoreInfo) -> ChannelInfo {
|
|||
subscriber_count: c.subscriber_count,
|
||||
description: c.description,
|
||||
videos,
|
||||
videos_continuation: c.videos_continuation,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ mod stream;
|
|||
// Re-exports so UniFFI sees the types at the crate root for macro discovery.
|
||||
pub use channel::ChannelInfo;
|
||||
pub use error::StrawcoreError;
|
||||
pub use search::SearchItem;
|
||||
pub use search::{Page, SearchItem};
|
||||
pub use stream::{AudioStreamItem, StreamInfo, VideoStreamItem};
|
||||
|
||||
/// Initialize Android logging + the strawcore-core HTTP downloader.
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
// changes.
|
||||
|
||||
use strawcore_core::stream::StreamInfoItem;
|
||||
use strawcore_core::youtube::continuation::ContinuationPage;
|
||||
use strawcore_core::youtube::linkhandler::search::SearchFilter;
|
||||
use strawcore_core::youtube::search_extractor;
|
||||
|
||||
|
|
@ -26,6 +27,24 @@ pub struct SearchItem {
|
|||
pub upload_date_relative: String,
|
||||
}
|
||||
|
||||
/// A page of results plus the opaque token that fetches the NEXT page.
|
||||
/// `continuation == null` means there are no more results — the Kotlin
|
||||
/// side stops requesting once it sees a null token. Shared by `search`,
|
||||
/// `search_continuation`, and the channel-videos continuation.
|
||||
#[derive(Debug, Clone, uniffi::Record)]
|
||||
pub struct Page {
|
||||
pub items: Vec<SearchItem>,
|
||||
pub continuation: Option<String>,
|
||||
}
|
||||
|
||||
/// Map a core ContinuationPage (channel or search) onto the FFI Page.
|
||||
pub(crate) fn page_from_core(page: ContinuationPage) -> Page {
|
||||
Page {
|
||||
items: page.items.into_iter().map(from_core).collect(),
|
||||
continuation: page.continuation,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn from_core(item: StreamInfoItem) -> SearchItem {
|
||||
let uploader_url = if item.uploader_url.is_empty() {
|
||||
None
|
||||
|
|
@ -53,7 +72,7 @@ pub(crate) fn from_core(item: StreamInfoItem) -> SearchItem {
|
|||
}
|
||||
|
||||
#[uniffi::export(async_runtime = "tokio")]
|
||||
pub async fn search(query: String) -> Result<Vec<SearchItem>, StrawcoreError> {
|
||||
pub async fn search(query: String) -> Result<Page, StrawcoreError> {
|
||||
// Don't log the query itself — searches are PII (sometimes
|
||||
// names, sometimes embarrassing) and android_logger emits at
|
||||
// info-level in release builds, which means they'd ride the
|
||||
|
|
@ -72,5 +91,25 @@ pub async fn search(query: String) -> Result<Vec<SearchItem>, StrawcoreError> {
|
|||
.map_err(|e| StrawcoreError::Extractor {
|
||||
msg: format!("join: {e}"),
|
||||
})??;
|
||||
Ok(result.videos.into_iter().map(from_core).collect())
|
||||
// Page 1 carries the first continuation token (None once YT stops
|
||||
// handing them out). The Kotlin SearchViewModel passes it back to
|
||||
// `search_continuation` on scroll.
|
||||
Ok(Page {
|
||||
items: result.videos.into_iter().map(from_core).collect(),
|
||||
continuation: result.continuation_token,
|
||||
})
|
||||
}
|
||||
|
||||
/// Fetch the next page of search results from a continuation token
|
||||
/// returned by a prior `search` / `search_continuation` Page.
|
||||
#[uniffi::export(async_runtime = "tokio")]
|
||||
pub async fn search_continuation(token: String) -> Result<Page, StrawcoreError> {
|
||||
log::info!("strawcore::search_continuation token_len={}", token.len());
|
||||
crate::runtime::ensure_initialized();
|
||||
let page = tokio::task::spawn_blocking(move || search_extractor::search_continuation(&token))
|
||||
.await
|
||||
.map_err(|e| StrawcoreError::Extractor {
|
||||
msg: format!("join: {e}"),
|
||||
})??;
|
||||
Ok(page_from_core(page))
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue