Commit graph

8 commits

Author SHA1 Message Date
Sulkta
9d0f8cc741 vc=28: edge-to-edge player, nav-bar inset, video-track reset, app icon
Three layout/playback fixes on vc=26-27 feedback plus the long-deferred
app icon.

Layout — VideoDetailScreen
  Player surface now fills the screen width (no 16dp side gutters).
  Outer Column dropped its 16dp padding; the player Box hangs off the
  full width with no rounded corners — NewPipe/YouTube look. Everything
  below (title, chips, button row, description, related list) goes back
  into an inner Column with 16dp horizontal + 12dp vertical padding so
  the body still reads correctly.
  Bottom inset: Spacer(windowInsetsBottomHeight(WindowInsets.navigationBars))
  appended at the end of the scrollable column. Last related video can
  scroll up past the gesture pill / 3-button nav instead of being
  obscured by it. (Plain navigationBarsPadding would have pushed the
  whole surface up and left a dead band.)

Black-video fix
  vc=27's Background button disabled the video track on the controller
  via setTrackTypeDisabled(C.TRACK_TYPE_VIDEO, true) and that override
  is sticky. Returning to a video left it audio-only with a black
  surface. Added a LaunchedEffect(controller, streamUrl) that resets
  TrackSelectionParameters to defaults on every entry into detail — if
  the user opened a video page, they want video. The audio-only
  fullscreen toggle and the Background button still set the override
  for the duration of that session; they just no longer leak.

App icon
  Replaced the Android default placeholder (sym_def_app_icon, which
  fdroid was failing to render anyway) with a proper adaptive icon:
    Background: #166534 deep green (sulkta.com brand)
    Foreground: tilted lime parallelogram + white play triangle
      (literal "straw" nod + video-app affordance)
  Adaptive XML in mipmap-anydpi-v26/. PNG fallbacks rendered via
  rsvg-convert at all five mipmap densities (mdpi/hdpi/xhdpi/xxhdpi/
  xxxhdpi) for pre-API-26 devices. Manifest now points at
  @mipmap/ic_launcher and @mipmap/ic_launcher_round.
2026-05-25 11:43:38 -07:00
Sulkta
9c4b3f8318 v0.1.0-T (vc=7): bug fixes + Opus audit pass #2 + home redesign
User-visible:
- BUG: 'clicking a second item went back to the first video' — VideoDetailViewModel
  guard was short-circuiting on Activity-scoped ViewModel reuse. Now tracks
  loadedUrl and only skips when the requested URL matches.
- BUG: PiP / window mode now auto-enters on Home gesture (Android 12+ via
  setAutoEnterEnabled). Manual PiP button reports failure cause via Toast.
- HOME REDESIGN: replaced 3-tab bottom nav with hamburger ModalNavigationDrawer.
  Default view = sub feed. Drawer items = Subscriptions / History / Search /
  Settings. Top-left hamburger like normal Android apps.

Audit pass #2 (Opus max-effort) — CRIT + HIGH fixes shipped:
- CRIT-1: PlaybackService now calls startForeground() inside onStartCommand
  with a media-playback notification + channel. Pre-fix could throw
  ForegroundServiceDidNotStartInTimeException on Android 12+ and crash-kill.
- CRIT-2: AndroidManifest service exported=false. Previously any installed
  app could craft an Intent and drive playback from attacker URLs.
- HIGH-1: 🎧 background handoff stops the activity player before starting
  the service so we don't dual-host two ExoPlayers + MediaSessions.
- HIGH-2: onStartCommand returns START_NOT_STICKY and tears down on null
  intent; no more crash-restart-crash loop after OS kills.
- HIGH-3: stop service on STATE_ENDED / STATE_IDLE via Player.Listener.
  onTaskRemoved checks playbackState properly so we don't hold WAKE_LOCK
  forever after a video ends in background.
- HIGH-4: Downloader validates scheme=https + googlevideo/youtube host
  before handing the URL to DownloadManager.
- HIGH-5: filename sanitization extended to ASCII control chars, DEL,
  Unicode bidi-override block, leading-dot, trailing whitespace.
- HIGH-6: SubscriptionFeedViewModel cancels prior in-flight refresh,
  caps parallelism at 8 via Semaphore, applies 15s per-channel timeout.
- HIGH-7: sub feed error banner now shows above cached items when refresh
  fails (previously hidden, looked indistinguishable from success).
- HIGH-8: PlayerViewModel falls back to lowest-available stream when no
  stream is under the max-resolution ceiling (was: silent black screen).
- HIGH-9: network_security_config explicit cleartextTrafficPermitted='false'
  on the RYD domain-config block (doesn't inherit from base-config).
- MED-1: PlaybackService.onDestroy nulls field before releasing session to
  close a race with onGetSession during teardown.
- MED-6: Downloader catches enqueue exceptions, returns -1L, caller toasts
  'download refused (bad URL)' instead of crashing.

Deferred (audit said 'can wait'): MED-2..5, MED-7..11, HIGH-10 UX consistency.
2026-05-24 07:49:35 -07:00
Sulkta
3a7c596244 Straw phase M-2: MediaSession + audio focus + service skeleton
Wraps the existing Activity-owned ExoPlayer in a Media3 MediaSession. Side
effects:
- Lock-screen media controls (play/pause)
- System media notification with play/pause buttons
- Audio focus + ducking (other audio dims when straw plays)
- Bluetooth + headset hardware-button routing
- Plays nicely alongside other media apps' notifications

The ExoPlayer is also configured with AudioAttributes (USAGE_MEDIA +
CONTENT_TYPE_MOVIE) and handleAudioFocus=true so other apps can request
focus correctly (e.g., a phone call ducks/pauses straw).

Also scaffolded but NOT yet wired:
- PlaybackService extending MediaSessionService — the foundation for
  true background-after-Activity-kill audio. Manifest entry + deps
  added (media3-session 1.4.1, concurrent-futures-ktx 1.2.0). The
  Activity-to-Service migration via MediaController is M-3 work.
- POST_NOTIFICATIONS, FOREGROUND_SERVICE,
  FOREGROUND_SERVICE_MEDIA_PLAYBACK, WAKE_LOCK permissions in manifest.

For the user right now: lock the phone while a video plays — media
controls appear on the lock screen. Open another app — notification with
play/pause sits in the shade. Press a headset's play/pause — straw
responds. Pull the home screen — PiP keeps the video floating + audio
continues. Full screen-off-background-audio after killing the activity
arrives in M-3.
2026-05-23 20:46:47 -07:00
Sulkta
d0a5122e4f Straw phase M-1: tap-thumbnail-to-play + hi-res thumbs + Picture-in-Picture
a tester's real-use feedback: "playing a video is a button under a very
pixelated thumbnail, no playing in window mode, ... where are all the
features." This is the visible-UX pass.

Thumbnails:
- New util/Thumbnails.kt#bestThumbnail picks the highest-res image from
  NewPipeExtractor's List<Image> by w*h pixel area. Was firstOrNull()
  which is the smallest in the sorted list. Applied across Search,
  Channel video rows, Channel avatar + banner, VideoDetail.

Tap-to-play:
- VideoDetail thumbnail is now wrapped in a clickable Box that fires
  onPlay(). Centered semi-transparent black circle with a white play
  triangle overlay so the affordance is obvious. The standalone "Play"
  button below stays for accessibility (tap target consistency).

Picture-in-Picture:
- Manifest: android:supportsPictureInPicture="true" on StrawActivity +
  added screenLayout|smallestScreenSize|screenSize to configChanges so
  rotation into PiP doesn't recreate.
- PlayerScreen: top-right floating button enters PiP with 16:9 aspect.
- Lifecycle ON_STOP observer now checks isInPictureInPictureMode and
  skips pausing when we're in PiP (was killing playback on PiP entry).

Phase M-2 next: MediaSession + background audio + foreground service
(the "play with screen off / lock screen controls" feature).
2026-05-23 20:40:53 -07:00
Sulkta
ae8bef117f Straw audit-fix sprint: CRIT-1 + HIGH-9 + targeted MED
Phase L. Triages findings from the Opus max-effort audit
(memory/2026-05-23-night3-straw later; agent report inline).

CRIT-1: ACTION_SEND URL extracted from arbitrary text/plain share now
  re-validated via URI host check before reaching NewPipeExtractor.
  Manifest scheme also validated for VIEW. Regex broadened to accept
  music.youtube.com + youtube-nocookie. Host set expanded.

HIGH: usesCleartextTraffic="true" removed from manifest (was redundant
  with network_security_config + misleading to reviewers).

HIGH: NewPipeDownloader hardens header copy — runCatching around addHeader
  to survive poisoned response headers with \r/\n. Explicit UA added AFTER
  upstream header copy so it wins. Switched to OkHttp 5 extension form
  for toRequestBody.

HIGH: Bounded response bodies via new util `cappedString(maxBytes)`. Caps
  at 8MiB (NewPipeExtractor), 1MiB (SponsorBlock), 256KiB (RYD). Defends
  against OOM via gigabyte response. Partial defense — chunked transfers
  without Content-Length still streamed up to cap.

HIGH: HistoryStore / SettingsStore / SubscriptionsStore moved from
  non-atomic `_flow.value = next` to `_flow.updateAndGet { ... }`. Closes
  the read-modify-write race that could drop concurrent writes.

HIGH: VideoDetailViewModel.load had a dead-code `if (...)` block with no
  body — falls through every call. Replaced with a real early-return on
  detail-already-loaded.

HIGH: ChannelViewModel picked `info.tabs.firstOrNull()` which is YouTube's
  curated "Home" tab. Switched to `firstOrNull { ChannelTabs.VIDEOS in
  contentFilters }` with fallback. Channel screen now shows actual videos.

HIGH: PlayerScreen SponsorBlock skip loop hardened:
  - dedup skipped UUIDs so re-listen doesn't fight the user
  - poll 250ms → 150ms reduces sponsor leak through buffering
  - `isPlaying` → `playbackState != IDLE/ENDED` so buffering doesn't miss
  - clamp seek away from duration boundary (prevents past-end jank)
  - filter POI-style point segments (start ≈ end)

MED: ExoPlayer pause on app background via Lifecycle.Event.ON_STOP
  observer. Was silently playing audio with no MediaSession when app
  was backgrounded.

MED: Log.d/Log.w gated behind BuildConfig.DEBUG via new strawLogD/strawLogW
  inline helpers in util/Log.kt. Lambda body skipped in release too.
  Log.i remains unguarded for user-quotable events. RydClient +
  SponsorBlockClient + PlayerScreen updated.

MED: Hardcoded version "v0.1.0" in StrawHome replaced with
  BuildConfig.VERSION_NAME. Now reads "v0.1.0-day1" matching ProjectConfig.

MED: Triplicated formatCount/formatViews/formatDuration extracted to
  util/Formatting.kt. Search/Channel/VideoDetail import the shared
  functions.

MED: SponsorBlockClient.buildJsonArray uses kotlinx-serialization Json
  encoder instead of hand-rolled string concat — defends against future
  user-typed category names breaking the URL.

MED: Description regex passes capped to 20k input chars before stripHtml
  on detail screen — defends against ANR on multi-MB descriptions.

Deferred to phase M (not in this sprint):
  - R8 + ProGuard rules for release builds (isMinifyEnabled=true)
  - ExoPlayer hoisting into PlayerViewModel (AndroidViewModel)
  - DI / Koin to replace `error("not initialized")` singleton accessors
  - String resources / i18n
  - rememberSaveable nav stack for process-death restore
  - onNewIntent override for in-running YouTube URL shares
  - certificate pinning on RYD endpoint
2026-05-23 20:23:34 -07:00
Sulkta
0252f6c63f Straw phase F: visible polish — RYD, HTML, intent filter, network sec
Four polish items, all visible:
1. Fixed RYD URL: was returnyoutubedislike.com (the website, 404s on /votes)
   → returnyoutubedislikeapi.com (the actual API).
2. Bundled Sectigo "Public Server Authentication CA DV R36" intermediate
   as an additional trust anchor (network_security_config.xml) for the
   .com web host — the API uses Google Trust Services already so this
   is defensive, in case we ever fetch the landing page.
3. Added intent-filter for YouTube URLs (VIEW + SEND) on StrawActivity.
   Single-task launchMode; pickYouTubeUrl() routes the initial Intent
   to Screen.VideoDetail(url) instead of Home.
4. stripHtml() utility removes <br>, </p>, and other tags from
   NewPipeExtractor's description. Chapter timestamps now render
   readably; raw HTML gone.

Also added Log.d to SponsorBlockClient and RydClient for visible
verification (StrawSb / StrawRyd tags). PlayerScreen now shows a
"SB: N segments" overlay chip in the top-left so the user can see
that SponsorBlock is armed.

Verified on Android 14 emulator with "Me at the zoo" intent:
- RYD: 👍 18.9M / 👎 424.2K renders
- Intent: direct URL → VideoDetail (no Home pass-through)
- HTML: chapter timestamps render clean
2026-05-23 19:40:08 -07:00
Sulkta
c30c171c91 Sulkta day-2: search → detail → player → SponsorBlock + RYD
Phase A: NewPipeExtractor + OkHttp Downloader wired in. Search bar +
LazyColumn results. Tap = navigate to detail.

Phase B: VideoDetail screen — StreamInfo metadata + Return YouTube
Dislike chips + description.

Phase C: Media3 ExoPlayer in Compose. Resolves StreamInfo to best
playable: DASH MPD → HLS → combined progressive → merged
videoOnly+audio.

Phase D: SponsorBlock SHA-256 prefix lookup. 250ms position-poll loop
inside PlayerScreen — exoPlayer.seekTo(segment.end) when entering
a sponsor segment. Toast on skip.

Phase E: Verified live on Android 14 emulator. linus tech tips search
returns real results with thumbnails; tapped result opens detail;
hit Play → video plays through ExoPlayer.

Architecture: everything in :strawApp for now (not pushed into :shared
yet — KMP refactor is day-3). Pure-state nav (sealed Screen + stack,
no nav library).

Known polish gaps (day-3): RYD chips render empty on some videos,
description has raw HTML (markdown render needed), no Koin DI yet,
no persistence.

GPL-3.0-or-later per upstream NewPipe.
2026-05-23 19:22:52 -07:00
Sulkta
c8f6c0fe21 Sulkta day-1: straw — KMP/Compose YouTube client fork
Initial Sulkta fork of NewPipe with a new :strawApp module that ships a
clean Compose-based Android APK at applicationId com.sulkta.straw.

What's here:
- :strawApp — thin Android application shell, MaterialTheme + StrawHome
  Composable. Lives alongside legacy :app so we don't break upstream.
- buildSrc — STRAW_APPLICATION_ID/VERSION constants alongside the existing
  NEWPIPE_APPLICATION_ID_OLD/NEW.
- docs/sulkta — RECON.md (NewPipe codebase breakdown) + DECISIONS.md
  (stack + scope decisions).

NewPipe's :shared KMP scaffold is at 892 LOC and renders nothing; this
fork picks up there and races ahead. Day-1 ships a hello APK; day-2 wires
NewPipeExtractor + Media3 player + SponsorBlock + Return YouTube Dislike.

GPL-3.0-or-later per upstream.
2026-05-23 17:37:55 -07:00