straw/rust/strawcore/src/search.rs
Sulkta c80be35d7b vc=32: subs feed — dates, watched filter, infinite scroll, avatar fallback
Subscription feed is now actually a feed instead of a teaser.

Rust (strawcore wrapper)
  Added upload_date_relative and uploader_avatar to SearchItem so
  Kotlin can see both. strawcore-core already extracts upload_date
  relative ("2 days ago") on every StreamInfoItem and uploader_avatars
  on most — we were just throwing them away in from_core. Fixed.

StreamItem
  uploadDateRelative + uploaderAvatar fields added. Every construction
  site (search/channel/detail/feed) plumbs them through.

SubscriptionFeedViewModel
  Per-channel cap 5 → 30. With 30 subs that's up to 900 items in
  memory; ConcurrentHashMap entries are small enough.
  Sort by parsed relative recency (RECENCY_RE on the "N <unit> ago"
  string, signed seconds-ago, tied items break by viewCount).
  Opportunistic avatar backfill: every successful channelInfo fetch
  updates the stored ChannelRef.avatar via Subscriptions.updateAvatar
  when strawcore returns a non-null avatar — fixes the "I just subbed
  to a channel and the chip has no icon" case where the channel header
  parser missed the avatar at subscribe time but the feed-fetch
  layout returns one.

SubsPane (StrawHome)
  Hide-watched FilterChip (session-sticky). Cross-references
  History.watches by 11-char YT video ID; filters out anything you've
  already watched. "All caught up — nothing unwatched" empty state.
  Infinite scroll: PAGE_SIZE = 20. derivedStateOf-gated snapshotFlow
  watches the LazyListState's lastVisibleItem index; when within 5
  items of the bottom, bumps visibleCount by 20. "Loading more..."
  spinner at the bottom while there's more to show.
  Visible-count resets to PAGE_SIZE when the underlying list shrinks
  (refresh dropped items, filter just engaged).
  FeedRow now shows: uploader · views · "3 days ago".

SubChip
  Lettered fallback when ch.avatar is null. PrimaryContainer-tinted
  circle with the first letter — no more broken-image placeholder
  while the feed-fetch backfills the real avatar.

SubscriptionsStore
  updateAvatar(url, avatar) for the backfill path. Atomic via
  updateAndGet, persists to SharedPreferences.
2026-05-25 12:34:02 -07:00

72 lines
2.3 KiB
Rust

// Phase 7 — search via Sulkta-OSS/strawcore-core. Exposed to Kotlin
// as a suspend fun. SearchItem field shape is unchanged from Phase U-2
// so Kotlin callers (SearchViewModel) keep working with no code
// changes.
use strawcore_core::stream::StreamInfoItem;
use strawcore_core::youtube::linkhandler::search::SearchFilter;
use strawcore_core::youtube::search_extractor;
use crate::error::StrawcoreError;
#[derive(Debug, Clone, uniffi::Record)]
pub struct SearchItem {
pub url: String,
pub title: String,
pub uploader: String,
pub uploader_url: Option<String>,
pub uploader_avatar: Option<String>,
pub thumbnail: Option<String>,
/// Duration in seconds. 0 = live/unknown.
pub duration_seconds: i64,
/// Reported view count. 0 = unknown.
pub view_count: i64,
/// Relative upload date as YT renders it ("2 days ago", "3 weeks
/// ago"). Empty if not extracted. Strawcore-core already populates
/// this on StreamInfoItem; we just pass it through.
pub upload_date_relative: String,
}
pub(crate) fn from_core(item: StreamInfoItem) -> SearchItem {
let uploader_url = if item.uploader_url.is_empty() {
None
} else {
Some(item.uploader_url)
};
let thumbnail = item
.thumbnails
.last()
.map(|i| i.url().to_string());
let uploader_avatar = item
.uploader_avatars
.last()
.map(|i| i.url().to_string());
SearchItem {
url: item.url,
title: item.name,
uploader: item.uploader_name,
uploader_url,
uploader_avatar,
thumbnail,
duration_seconds: item.duration_seconds,
view_count: if item.view_count < 0 {
0
} else {
item.view_count
},
upload_date_relative: item.upload_date_relative,
}
}
#[uniffi::export(async_runtime = "tokio")]
pub async fn search(query: String) -> Result<Vec<SearchItem>, StrawcoreError> {
log::info!("strawcore::search query={}", query);
let result = tokio::task::spawn_blocking(move || {
search_extractor::search(&query, SearchFilter::Videos)
})
.await
.map_err(|e| StrawcoreError::Extractor {
msg: format!("join: {e}"),
})??;
Ok(result.videos.into_iter().map(from_core).collect())
}