straw/rust/strawcore/src/channel.rs
Sulkta 21ca8da8d2 straw: infinite-scroll pagination for channel + search
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.
2026-06-19 18:21:42 -07:00

88 lines
3.2 KiB
Rust

// Phase 7 — `channel_info(channel_url)` via the new strawcore.
// 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, 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, page_from_core, Page, SearchItem};
#[derive(Debug, Clone, uniffi::Record)]
pub struct ChannelInfo {
pub id: String,
pub name: String,
pub avatar: Option<String>,
pub banner: Option<String>,
/// -1 = unknown / hidden by the channel.
pub subscriber_count: i64,
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")]
pub async fn channel_info(input: String) -> Result<ChannelInfo, StrawcoreError> {
log::info!("strawcore::channel_info input_len={}", input.len());
crate::runtime::ensure_initialized();
let identifier = resolve_channel_identifier(&input)?;
let core = tokio::task::spawn_blocking(move || core_channel_info(identifier))
.await
.map_err(|e| StrawcoreError::Extractor {
msg: format!("join: {e}"),
})??;
Ok(map_channel(core))
}
fn resolve_channel_identifier(
input: &str,
) -> Result<core_link::ChannelIdentifier, StrawcoreError> {
let trimmed = input.trim();
// Bare channel ID — UC..., 24 chars.
if trimmed.starts_with("UC") && trimmed.len() == 24 {
return Ok(core_link::ChannelIdentifier::DirectId(trimmed.into()));
}
core_link::parse(trimmed).map_err(|e| StrawcoreError::Unsupported {
detail: e.to_string(),
})
}
/// 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());
let videos = c.recent_videos.into_iter().map(search_from_core).collect();
ChannelInfo {
id: c.channel_id,
name: c.name,
avatar,
banner,
subscriber_count: c.subscriber_count,
description: c.description,
videos,
videos_continuation: c.videos_continuation,
}
}