Commit graph

12297 commits

Author SHA1 Message Date
85efaf13e9 Update dependency androidx.lifecycle:lifecycle-viewmodel-compose to v2.11.0
Some checks failed
gitleaks / scan (push) Failing after 1s
gitleaks / scan (pull_request) Failing after 1s
2026-07-06 06:51:49 +00:00
3d86b5be52 Merge pull request 'Update compose to v1.11.4' (#24) from renovate/compose into main
Some checks failed
build-apk / build-and-publish (push) Successful in 7m8s
gitleaks / scan (push) Failing after 2s
2026-07-04 23:56:19 -07:00
d755bd32fb Update compose to v1.11.4
Some checks failed
renovate/stability-days Updates have met minimum release age requirement
gitleaks / scan (push) Failing after 2s
gitleaks / scan (pull_request) Failing after 2s
2026-07-05 06:56:10 +00:00
7ff1546d31 ci: remove dead inherited .github/workflows (pwn-request + cruft)
Some checks failed
gitleaks / scan (push) Failing after 3s
CI security audit finding. These are all inherited NewPipe GitHub workflows,
none functional on Forgejo — the real pipeline is .forgejo/workflows/.

Highest risk: pr-labeler.yml ran on pull_request_target with pull-requests:write
and called a mutable @main third-party action with GITHUB_TOKEN — a textbook
pwn-request on a public repo (base-repo write token, triggerable by any fork PR).
image-minimizer.yml ran arbitrary JS with issues/PR write perms. The rest
(no-response, backport-pr, ci, build-release-apk) are dead + were spamming a red
X on every push. Removing the lot drops third-party supply-chain surface to zero.

Also dropped changed-lines-count-labeler.yml (dead config for the deleted labeler).
2026-07-04 20:25:56 -07:00
e34fbd457b ci: restore Forgejo build+publish + gitleaks workflows (infra-clean)
Some checks failed
build-apk / build-and-publish (push) Successful in 8m20s
gitleaks / scan (push) Failing after 1s
The OSS public-scrub removed .forgejo/workflows/ wholesale — it only needed
to parameterize the two infra literals in the publish step. Restored both
workflows; the publish target (root@host) and its SSH host-key now come from
Forgejo repo secrets (STRAW_PUBLISH_TARGET / STRAW_LUCY_HOSTKEY) instead of
literals, so the public workflow carries zero infra topology. Everything else
was already clean: signing keystore + deploy key are secrets, the actual
publish is a forced-command on the host (not in this YAML), and the runner
label / build image / signer fingerprint are non-sensitive.

Restores push-triggered CI: build assembleDebug in the straw-build image,
verify signer bb9ca96b, publish to fdroid. This push also builds vc=91.
2026-07-04 12:29:06 -07:00
e4a8751942 reliability + security fix sprint (vc=91)
Straw full-audit fix batch (2026-07-04). Confirmed HIGH/MED findings:

- Updater self-brick: drop the fdroid.sulkta.com leaf+E7 SPKI pins (LE
  rotates the leaf every ~90d + intermediates from a pool → a routine
  renewal was guaranteed to miss both pins and silently kill the only
  in-app update path). System-CA validation + the install-time APK
  signature gate remain. Also distinguish "index unreachable" from "up to
  date", add a 30s callTimeout + bounded index read, guard the API-26
  NotificationChannel, and setOnlyAlertOnce.
- Request POST_NOTIFICATIONS at runtime so A13+ update/media notifications
  actually post (was declared but never requested → silent no-op).
- isDebuggable=false on the shipped debug variant (closes ADB run-as dump
  of watch/search history + subs; no package-id cutover).
- Crash fixes: distinctBy(url) on moreFromChannel + related (duplicate
  LazyColumn key); back-handling driven by live nav depth so it survives
  moveTaskToBack on A12+; PlaylistsStore per-store caps + off-main
  hydration (hostile import ANR); SponsorBlock staleness fence via
  NowPlaying.refreshIfCurrent.
- CacheCap.nearest() snaps up to the nearest finite cap (defaults no longer
  resolve to Unlimited/100k on fresh installs).
- RYD/SB FFI shims wrap the uniffi call (swallow a core panic per contract).
- Rust wrapper: release panic="unwind" (was "abort", which defeated UniFFI
  catch_unwind → any core panic = whole-app SIGABRT) + a 60s wall-clock
  timeout on every blocking extractor call (unblocks the caller, bounds
  thread pileup). Pairs with the strawcore-core reliability fix.
2026-07-04 07:09:54 -07:00
410e44305e Merge pull request 'Update sonarqube to v7.3.1.8318' (#18) from renovate/sonarqube into main 2026-07-01 23:53:36 -07:00
a89329f2d5 Update sonarqube to v7.3.1.8318
Some checks failed
renovate/stability-days Updates have met minimum release age requirement
Image Minimizer / try-minimize (pull_request) Failing after 1s
PR size labeler / Automatically labelling pull requests based on the changed lines count (pull_request_target) Failing after 1s
2026-07-02 06:53:28 +00:00
b74eca8fd1 Merge pull request 'Update prettytime to v5.0.9.Final' (#17) from renovate/prettytime into main 2026-06-30 23:32:30 -07:00
cb10b558dd Merge pull request 'Update multiplatform to v1.11.1' (#16) from renovate/multiplatform into main 2026-06-30 23:32:18 -07:00
6284adedf8 Update prettytime to v5.0.9.Final
Some checks failed
renovate/stability-days Updates have met minimum release age requirement
Image Minimizer / try-minimize (pull_request) Failing after 1s
PR size labeler / Automatically labelling pull requests based on the changed lines count (pull_request_target) Failing after 1s
2026-07-01 06:32:16 +00:00
3e04bd7a66 Update multiplatform to v1.11.1
Some checks failed
renovate/stability-days Updates have met minimum release age requirement
Image Minimizer / try-minimize (pull_request) Failing after 1s
PR size labeler / Automatically labelling pull requests based on the changed lines count (pull_request_target) Failing after 1s
2026-07-01 06:31:54 +00:00
0be806bbf4 Merge pull request 'Update koin.plugin to v1.0.1' (#14) from renovate/koin.plugin into main 2026-06-29 23:29:47 -07:00
cb741cb56e Update koin.plugin to v1.0.1
Some checks failed
renovate/stability-days Updates have met minimum release age requirement
Image Minimizer / try-minimize (pull_request) Failing after 2s
PR size labeler / Automatically labelling pull requests based on the changed lines count (pull_request_target) Failing after 1s
2026-06-30 06:29:39 +00:00
6cfcaf3e83 Merge pull request 'Update koin to v4.2.2' (#13) from renovate/koin into main 2026-06-28 23:30:31 -07:00
436d2a4276 Merge pull request 'Update dependency androidx.compose.material:material-icons-extended to v1.7.8' (#12) from renovate/androidx.compose.material-material-icons-extended-1.x into main 2026-06-28 23:30:19 -07:00
890452a8ef Update koin to v4.2.2
Some checks failed
renovate/stability-days Updates have met minimum release age requirement
Image Minimizer / try-minimize (pull_request) Failing after 1s
PR size labeler / Automatically labelling pull requests based on the changed lines count (pull_request_target) Failing after 1s
2026-06-29 06:30:18 +00:00
bfeda2b43a Update dependency androidx.compose.material:material-icons-extended to v1.7.8
Some checks failed
renovate/stability-days Updates have met minimum release age requirement
Image Minimizer / try-minimize (pull_request) Failing after 1s
PR size labeler / Automatically labelling pull requests based on the changed lines count (pull_request_target) Failing after 1s
2026-06-29 06:30:12 +00:00
Sulkta
a033402a44 docs: public README + reproducible build image 2026-06-28 12:25:49 -07:00
120caa9050 Merge pull request 'Update teamnewpipe.newpipe.extractor to v0.26.3' (#10) from renovate/teamnewpipe.newpipe.extractor into main 2026-06-27 23:50:50 -07:00
cc02a9166d Update teamnewpipe.newpipe.extractor to v0.26.3 2026-06-28 06:50:35 +00:00
ddcd2d80f4 Merge pull request 'Update compose to v1.11.3' (#1) from renovate/compose into main 2026-06-27 23:48:49 -07:00
7bee7ca26c Update compose to v1.11.3 2026-06-27 20:49:58 +00:00
Sulkta
7c90f70a0a vc=90: cold-start store hydration off-Main + video-page status-bar fix + RYD FFI slim
Resume + Enrichment stores no longer JSON-decode on the main thread in Application.onCreate (Resume is the heaviest cold-start cost: ~50-100ms decode at its 100k-entry cap). Both seed an empty StateFlow and hydrate off-thread on their PrefsWriter single-thread dispatcher. Mutations that arrive before the hydrate finishes defer onto that FIFO dispatcher (a hydrated gate) so they re-run on fully-loaded state -- correct for adds AND clears; a naive loaded+live merge would resurrect a clear that landed on the empty seed. Subscriptions/Playlists/History/Settings stay eager (tiny + rendered directly -> would flash empty). (audit 2.1)

Video page: details/related rows no longer leak into the status-bar strip above the player when scrolled. topPadding is now a layout inset on the detail LazyColumn, so the scroll viewport begins+clips at the player's bottom edge instead of letting rows scroll up over the clock/signal.

RYD: dropped the dead rating + view_count fields from the strawcore RydVotes FFI Record + Kotlin shim -- only likes/dislikes are rendered; RYD's rating restates the like/dislike ratio and its viewCount is a stale aggregate conflicting with the real YouTube count already shown. (audit #2 L-9)

Adversarially reviewed (Opus): clear-resurrection closed, no defer-loop, FFI slim has no readers. Headless compileDebugKotlin green.
2026-06-22 05:10:48 -07:00
Sulkta
41b3999ad3 vc=89: bound hide-shorts pagination burst + finish SP-write serialization
D-3 — hide-shorts no longer drains a whole channel/search in one burst. The
infinite-scroll trigger is derived from the FILTERED list while loadMore()
appends to the UNFILTERED one, so a shorts-heavy feed with hide-shorts ON kept
the trigger hot (the filter stripped each fetched page back to ~nothing) and
auto-fetched every page to the end of the continuation. Now it keeps loading
while pages are productive (the filtered list grows) and stops after 3
consecutive pages that add nothing visible; toggling hide-shorts off resumes.
Applies to both SearchScreen and ChannelScreen; normal (non-filtered) infinite
scroll is unaffected (productive pages keep the counter at 0).

PrefsWriter completeness — the four SP stores the vc=88 sweep didn't cover
(Enrichment, SearchCache, Playlists, FeedCache) now serialize their writes too,
so no store is left on a bare sp.edit().apply(). EnrichmentStore is the one that
mattered: put() runs 8-wide concurrently from the feed-enrichment fan-out
(Semaphore(8) on Dispatchers.IO), so its apply() ordering genuinely raced — a
real M-2 instance the audit's four-store scope missed. FeedCacheStore has no
StateFlow (caller-owned state) so it uses the captured-arg form; ordering is
safe because its sole writer (the feed VM) serializes save/clear from one
refresh coroutine.

Verified: headless compileDebugKotlin green on the straw-build image. The vc=88
PrefsWriter concurrency was cleared by an adversarial review before this builds
on it.
2026-06-22 02:41:24 -07:00
Sulkta
ce7174cb5e vc=88: deferred-hygiene sweep (audit #2 leftovers, no behavior change)
M-2: route every SharedPreferences write (Settings/History/Subs/Resume) through
one PrefsWriter — a per-store single-thread dispatcher — so the on-disk apply()
order matches the in-memory CAS order. Previously a Main-thread toggle and an
IO-thread import could land apply() out of order, and ResumePositions detached
ordering entirely via a fresh globalScope.launch per write; a stale value could
then win the next cold-start load. Each write reads the live StateFlow so disk
converges to the latest in-memory state regardless of enqueue order.

L-14: Settings storage-usage sampling (File.length() x4 + Coil diskCache.size)
moved off the composition/Main thread into a LaunchedEffect on Dispatchers.IO.

L-2 / L-4..L-8 / L-15 / L-16: dead code + stale comments from the vc=85 SB/RYD
to Rust migration. Http.kt trimmed to STRAW_USER_AGENT; reconciled the
network_security_config / feed.rs / SubscriptionFeedViewModel / net.rs / CI
comments with reality; recencyScore overflow-guarded; ci/Dockerfile now
pre-installs build-tools 36 (AGP 9.2.1's actual floor, was auto-fetched).

Verified: headless compileDebugKotlin green on the straw-build image.
2026-06-21 20:03:45 -07:00
Sulkta
378634078e vc=87: audit #2 fix sprint — close two vc=86 regressions + MED/LOW
Fixes from the second code audit (which adversarially refute-verified every
HIGH/CRIT — 0 survived; these are the confirmed MED/LOW):

- Duplicate-key crash (M-1 + M-4): the subs feed, Search, and Channel lists
  key their LazyColumn by video url, but the sources weren't fully deduped —
  a video appearing twice (collab/cross-post in two subscribed feeds, or the
  same videoId across two search/channel shelves) made a duplicate Compose key
  → IllegalArgumentException → screen crash. Subs merge + Search/Channel FIRST
  page now distinctBy(url); the continuation paths already did. (M-4 was a
  regression of the vc=86 key additions, which only deduped the append path.)
- IosSafe end-of-chunk roll (M-5): the vc=86 change set chunkRemaining=0 on
  inner EOF expecting the NEXT read() to roll, but Media3 stops calling read()
  after a -1, so a LENGTH_UNSET inner still truncated to the first chunk. The
  roll now happens WITHIN read() (loop) with a progress guard so a no-progress
  chunk can't spin; folds in the zero-length-chunk EOF case too.
- SearchCacheStore.clear() is now atomic (updateAndGet) so a concurrent
  record() can't resurrect a just-cleared entry on disk (M-3).
- Gate the YtRelated autoplay branch through isAllowedYtUrl to match the
  universal-allowlist invariant (L-11); no-op-guard setLatestKnownVersion
  (L-13); drop two unused TrackSelectionParameters imports (L-3).

Deferred (low-risk hygiene, none a crash): SharedPreferences write-ordering
serialization (M-2), the migration's dead Http.kt sweep (L-2), a stale-comment
pass. Verified: full Android compileDebugKotlin green.
2026-06-21 14:30:58 -07:00
Sulkta
d6399b367d vc=86: audit-fix sprint (HIGH H2 + 5 MED/LOW from the 2026-06-21 audit)
- Audio-only toggle no longer drops the max-resolution cap: both the
  fullscreen button (PlayerScreen) and the detail Audio pill (VideoDetailBody)
  rebuilt TrackSelectionParameters from a fresh Builder, wiping the data-saver
  ceiling. Now buildUpon() the existing params so the cap survives. (H2)
- Subscriptions Refresh button no longer sticks at "..." forever on a warm
  restart within the cache TTL: refreshIfStale clears the initial loading
  seed when it decides nothing needs refreshing. (M2)
- Search + Channel result lists get a stable item key (video url) so paging /
  shorts-filtering stops re-binding rows to new data (re-triggered thumbnail
  loads, scroll shift). (M3, M4)
- IosSafeHttpDataSource: the unknown-length (LENGTH_UNSET) chunk path rolls
  forward to the next Range chunk at inner-EOF instead of re-reading the
  exhausted source forever (was truncating playback to the first chunk). (M5)
- strawcore channel_feed_rss propagates the real failure (network/HTTP/parse)
  instead of collapsing every error to an empty list, so a broken fetch is
  distinguishable from "no new videos" (subscription_feed keeps its per-channel
  tolerance for fan-out). (M6)
- Feed recency: a clock-skewed future upload emits "0 seconds ago" (parses to
  top) instead of "just now" (which Kotlin's recency parser couldn't read, so
  the item sank to the bottom). (L4)

Deferred to a follow-up: M1 (bg-refresh cache-key mismatch — needs a worker
redesign) + M7 (build config-cache wiring). Verified: cargo check/clippy +
full Android compileDebugKotlin green.
2026-06-21 13:37:51 -07:00
Sulkta
a36e673627 vc=85: image caching + SB/RYD clients -> Rust + crash/autoplay fixes
- Thumbnails + channel icons stay cached: pin an explicit 256MB Coil disk
  cache + sized memory cache via SingletonImageLoader.Factory. Coil's
  default disk cap is 2% of the device's free space, so on a storage-tight
  phone the subs feed (most image-heavy screen) thrashed it and
  re-downloaded thumbnails on every visit.
- SponsorBlock + Return-YouTube-Dislike clients moved Kotlin -> Rust
  (strawcore net.rs: fetchSponsorSegments / fetchRydVotes). SponsorBlock
  keeps its privacy-preserving SHA-256 hash-prefix lookup. Kotlin is now a
  thin shim mapping the FFI records onto the SbSegment/RydVotes domain
  types; behavior identical. Migration #2 of "all backend -> Rust".
- Fix crash: extract_channel_id sliced the channel URL by a length derived
  from a lowercased copy of itself; to_lowercase() can change byte length
  on non-ASCII, so a non-ASCII URL tail could panic across the FFI and
  abort the app on a feed refresh. Now matches the prefix case-insensitively
  against the original with length + char-boundary guards.
- Fix autoplay hijack: advancing to the next video resolves over ~500ms; if
  you manually start a different video meanwhile, autoplay would replace
  your choice with the stale next-up. Added a staleness fence.

Verified: cargo check/test/clippy on the wrapper, full Android
compileDebugKotlin green, adversarial FFI pre-push audit passed.
2026-06-21 12:59:04 -07:00
Sulkta
4affa1e739 vc=84 — rename launcher to 'Straw' + move stream picker into Rust
Two changes:

1. Launcher name is now just 'Straw', not 'Straw debug' — past the
   debug-branding phase. Kept the .debug applicationId suffix (package
   stays com.sulkta.straw.debug) on purpose so fdroid updates install in
   place and the in-app auto-updater keeps working; dropping the suffix
   would change the package id and force a reinstall that wipes everyone's
   subs/history. That's a separate, deliberate release-track cutover.

2. Stream-selection logic moved out of Kotlin (resolveStreamPlayback) into
   the Rust strawcore wrapper as resolve_playback(StreamInfo, max_height)
   -> ResolvedStreams. The app keeps a thin shim that supplies the
   resolution cap (Settings.maxResolution) and attaches SponsorBlock
   segments. Byte-for-byte behavior parity with the old Kotlin picker:
   highest-bitrate stream at/under the cap, lowest-height fallback when
   nothing fits, first-element-wins on ties (matching Kotlin
   maxByOrNull/minByOrNull, not Rust's last-on-ties max_by_key), and
   isNotBlank() handling for the DASH/HLS URLs. First step of moving all
   backend logic to Rust; the picking lives at the FFI boundary because it
   depends on an app setting, keeping strawcore-core a pure extractor.

Wrapper cargo check + clippy clean (no new warnings); FFI surface adds
ResolvedStreams + resolvePlayback, bindings regen at build.
2026-06-21 11:41:50 -07:00
Sulkta
1531d41742 vc=83 — fix: swapping videos from the minibar kept playing the old one
The inline player's auto-resolve→play LaunchedEffect read the shared
activity-scoped VideoDetailViewModel's `resolved` stream without checking
it belonged to the newly-opened video. Right after a swap (e.g. pick
another video from the browse screen while one sits in the minibar), the
VM still holds the previous video's resolved URLs for one composition
frame until vm.load() nulls them — so setPlayingFrom fired with
streamUrl=NEW but resolved=OLD: NowPlaying.claim won under the new url
(title/details/minibar flipped to NEW) while the controller kept
streaming OLD, and the correct re-fire with NEW's resolved was then
swallowed by the 'already playing this url' short-circuit.

Restore the loadedUrl fence (the same guard VideoDetailBody and every
ViewModel already use) that the vc=75 expandable-player rearchitect
dropped when the inline resolve→play wiring moved out of VideoDetailScreen.
2026-06-21 10:26:21 -07:00
Sulkta
a636977512 vc=82 — subscription-feed enrichment via lightweight stream_metadata
enrich_feed_item now calls the new strawcore stream_metadata() path (Android
/player + videoDetails read only) instead of the full stream_info. The full
path ran the JS sig/nsig deobf, an extra WEB /player metadata round-trip, the
iOS client, and stream/manifest/caption extraction — then kept only view_count
+ duration_seconds. Those two come from the same videoDetails the lightweight
path reads (populate_microformat never touches them), so the values are
identical; the feed just stops paying for the discarded work — ~one heavy
round-trip dropped per enriched item per refresh.

FFI surface (enrichFeedItem -> EnrichedFeedMetadata) unchanged. Needs
strawcore 30f24d2 (pushed first; CI clones strawcore main).
2026-06-21 06:56:06 -07:00
Sulkta
f51b4fbb77 vc=81 — perf-audit app-side batch (search debounce + feed-merge memoize)
Search: the reactive cache-preview filter no longer runs on the main
thread on every keystroke. It walked the whole cached-results pool
(thousands of items on a heavy user) inline; now each keystroke
debounces ~150ms and the scan runs on Dispatchers.Default. A submit
cancels the pending preview so a late scan can't clobber live results.

Feed: mergeFromCache memoizes the relative-upload-date parse by string,
so the recency regex runs once per distinct "N days ago" value instead
of once per item (~3000 per merge on a 200-sub feed) — across
hydration, every refresh, and each background-enrichment emit.

No behavior change.
2026-06-21 06:26:40 -07:00
Sulkta
d9e5addb0b vc=80 — strawcore extraction perf batch (borrow-not-clone + parallel channel browse)
Picks up strawcore 91d4824: streamingData + format objects borrowed
instead of deep-cloned per video open; channel Home/Videos tabs fetched
concurrently (one round-trip, not two); response bodies decoded in place
on the valid-UTF-8 path. No behavior change — internal allocation +
latency wins only.
2026-06-21 05:42:08 -07:00
Sulkta
005aef395c Fix ExoPlayer wrong-thread crash + recordSearch off Main (perf-audit pass-2) — vc=79
- PlaybackService: the pauseOnHeadphoneDisconnect settings-watcher collector
  ran on globalScope (Dispatchers.IO) and called setHandleAudioBecomingNoisy
  on the ExoPlayer, which is thread-affine to the Main thread it was built on
  → latent IllegalStateException('Player accessed on the wrong thread') that
  fires every service start (the StateFlow emits on first subscription). Run
  the collector on Dispatchers.Main (mirrors resumePollJob).
- SearchViewModel: recordSearch (json-encode + SharedPrefs write) was on Main;
  wrap in withContext(Dispatchers.IO).

Both adversarially verified in the multi-agent perf audit (pass 2).
2026-06-21 05:04:39 -07:00
Sulkta
4ab46cf556 Perf audit batch 1 (app-side): preserve res-cap on autoplay + detail LazyColumn — vc=78
From the multi-agent perf audit (adversarially verified), the two app-side wins:

- 1.1 Preserve the max-resolution cap on autoplay-next. The enter-video
  trackSelectionParameters reset built from a blank default, silently
  dropping the data-saver ceiling every URL change so autoplay streamed
  uncapped. Now: re-enable the video track via buildUpon + reassert
  applyMaxResolutionCap().
- 2.1 VideoDetailBody verticalScroll Column -> LazyColumn. Related +
  more-from-channel rows recycle and defer each AsyncImage decode + the
  two ThumbnailProgress flow collectors until scrolled into view (was ~40
  decodes + ~80 collectors eager on every open). Namespaced item keys
  (rel:/mfc:) so a url in both lists doesn't crash the list; kept take(20);
  dialogs hoisted out of the lazy content.
2026-06-21 04:44:58 -07:00
Sulkta
05381e8d8b Expandable player: static poster during collapse/morph, live player only when expanded — vc=77
The remaining sluggishness was scaling a live-playing TextureView through
the morph's graphicsLayer every frame. Now the minibar + the whole
collapse/expand morph render the video's static poster (AsyncImage); the
live PlayerView only mounts once settled fully expanded. Audio is
unaffected — it's owned by the foreground service, never stops.
2026-06-20 17:02:03 -07:00
Sulkta
d7f86ebf14 Expandable player: kill sluggish morph (cheaper compositing + snappy spring) — vc=76
- The detail body's alpha fade was rendering the whole scroll subtree to
  an offscreen buffer every frame (CompositingStrategy.Auto goes offscreen
  when alpha<1). Switch to ModulateAlpha — the body's rows don't overlap,
  so the per-draw-op fade is correct and skips the buffer. Main fix.
- Replace the 300ms FastOutSlowIn tween (slow ramp at both ends) with a
  no-bounce StiffnessMedium spring — distance-adaptive, reads as snappy.
2026-06-20 13:41:46 -07:00
Sulkta
c6e7f46633 Expandable player morph: one container, video page ⇄ minibar (vc=75)
Replace the separate Screen.VideoDetail page + MinibarOverlay with one
ExpandablePlayer container that morphs continuously between the full
video page and the bottom minibar, in both directions. The old flow just
made the minibar appear/vanish; this is a true shared-element transition.

- One fraction (0=minibar, 1=full page) drives a graphicsLayer
  scale+translate on a single mounted TextureView PlayerView. The
  transform runs in the render phase (reads the Animatable inside the
  layer block) so the morph is smooth without recomposing the detail
  body, and the same video surface stays live across the whole range.
- 100dp collapsed player is 16:9, same as expanded, so the morph is a
  pure uniform scale (no aspect distortion).
- Opening a video sets OpenVideo + expands instead of pushing a screen;
  the browse screen stays underneath so collapsing returns you there.
- New OpenVideo singleton (open video) distinct from NowPlaying (playing
  video); the two are kept in sync while collapsed so autoplay-next
  doesn't leave the open page stale.
- VideoDetailBody extracted from VideoDetailScreen; the inline player
  surface + resolve/play wiring became InlinePlayerSurface inside
  ExpandablePlayer. VideoDetailScreen + MinibarOverlay deleted.
- Back: fullscreen pops, then expanded collapses, then browse stack.
- Unchanged: shared controller, NowPlaying, setPlayingFrom, SponsorBlock,
  autoplay-next, PiP, background audio, and true-fullscreen Player (⛶).
2026-06-20 12:17:41 -07:00
Sulkta
024906d66c Minibar swipe-up-to-restore + vc=74 (0.1.0-CH)
Item 1 (partial): the minibar was tap-to-expand only — added an upward-drag gesture that expands back to the full player when released past a small threshold (a tester: couldn't swipe the minibar back up). The continuous collapse-into-the-bar shared-element animation is deferred — needs on-device iteration. vc=74 ships items 2-5 fully + this.
2026-06-20 09:57:59 -07:00
Sulkta
426ae8fe4e Fix autoplay-next: play the next video instead of silently queuing it (vc=74 item 5)
At STATE_ENDED the finished video is still loaded (mediaItemCount==1), so enqueueLast appended the autoplay candidate at index 1 and the ENDED player never advanced to it — autoplay did nothing. Switch tryAutoplay to setPlayingFrom, which replaces + prepares + plays the next video. The old comment's 'queue is empty' assumption was the root error.
2026-06-20 09:56:08 -07:00
Sulkta
f919a914ff Watched-status: persistent red bar + our play count (vc=74 items 2,3)
Item 2: thumbnail progress bar now stays full on watched videos — falls back to watch-history when the live resume point is gone (was vanishing on finished videos). A live resume entry still wins for mid-watch progress. Item 3: HistoryStore tracks a per-video playCount (increments each watch, carried forward atomically in recordWatch; defaults 1 for pre-vc74 entries). VideoDetail shows 'Watched N times' under the view count.
2026-06-20 09:53:51 -07:00
Sulkta
c4c871d906 Persist hide-watched toggle across restart (vc=74 item 4)
hide-watched was session-only remember state in StrawHome — reset to OFF on every cold start. Back it with SettingsStore (SharedPreferences), mirroring hideShorts: new hideWatched StateFlow + setHideWatched(). StrawHome reads/writes Settings.
2026-06-20 07:57:51 -07:00
Sulkta
de0796076a Strip NewPipe: remove legacy :app + unused :desktopApp/:shared modules
Straw runs on the strawcore Rust pipeline and ships only :strawApp — it is not NewPipe and uses none of their app code. Removes the 12M org.schabi.newpipe :app module (the fork base) and the unused NewPipe-origin KMP :desktopApp/:shared scaffold. settings.gradle now includes only :strawApp; also drops the NewPipe SPDX header + the NewPipeExtractor includeBuild stub. This also kills the recurring config-time git failure from app/build.gradle.kts.
2026-06-20 07:19:33 -07:00
Sulkta
6767801fd4 VideoDetail vc=73: smooth swipe-dismiss + collapsible Details + clean action bar
Inline player → TextureView (XML surface_type) so the swipe-down-to-minimize drag follows the Compose graphicsLayer transform instead of the SurfaceView lagging behind (the stutter). Description folded into a collapsible Details section, collapsed by default, above recommendations. Action buttons restyled into one horizontally-scrollable row of uniform tonal icon pills; dropped the redundant Play button (inline player + fullscreen pill cover it).
2026-06-20 07:07:43 -07:00
Sulkta
f79e0cc999 release: explicit signing config + vc 72 (0.1.0-CF)
Wire an env-driven signingConfig so CI/release builds reuse ONE keystore
instead of Gradle's per-machine auto-generated ~/.android/debug.keystore
— a fresh CI container would otherwise mint a different key and break
in-place fdroid updates for everyone. STRAW_KEYSTORE_FILE (+ _PASS /
_ALIAS / KEY_PASS) point at the canonical key; unset → default debug
signing so local dev needs no setup. The key is the same androiddebugkey
(SHA1 BB:9C:A9:6B…) that signed vc 15→71, now also vaulted.

vc 71 → 72 for the channel/search infinite-scroll pagination fix.
2026-06-19 19:38:48 -07:00
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
Sulkta
7cccc96d20 ci: broaden gitleaks allowlist — catch all variable-name patterns. Refs #300 2026-05-28 12:19:24 -07:00
Sulkta
fef3b59a5c ci: gitleaks allowlist — InnerTube public API key + SharedPreferences key constants. Refs #300 2026-05-28 12:16:20 -07:00
Sulkta
1ba4450b99 Public-flip audit: scrub audit-ticket prefixes + internal references + tighten README
URLs → git.sulkta.com. Audit-ticket prefixes (SPEC §N, audit Track X, vc=N
audit-fix, FIX (audit ...), PORT DEVIATION) stripped from comments — technical
reasoning retained. Build-host references softened.
README sheds marketing scaffolding + stale status tables.
2026-05-27 13:29:53 -07:00