Commit graph

61 commits

Author SHA1 Message Date
jtrzupek
1875604c6d fix(mobile): onboarding pager — measure page width so last slide shows "Start browsing"
scrollTo/onScroll used the full screen width, but the ScrollView viewport is narrower
(card margins + padding), so the computed index desynced from the visible slide — the
last slide kept showing "Next"/"Skip" instead of "Start browsing". Measure the real
viewport width via onLayout and use it for paging, scrollTo and index. Caught on the
emulator (uiautomator dump — FLAG_SECURE blocks screenshots).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 10:21:19 +02:00
jtrzupek
db23b63e46 feat(mobile): first-launch tutorial (pages, features, long-presses, player gestures)
A 7-slide carousel shown once on first launch:
- the three tabs (Scenes/Movies/Sites)
- search, filters, saved searches, Performers/Tags/Favorites
- long-press actions (hide/duplicate a scene, remove a wrong performer, link diagnostics)
- player gestures (tap controls, double-tap ±15s, swipe to scrub, unmute)
- favorites, Hidden content, PIN lock, the ? report button, Sites ★ ratings

Gated by a SecureStore flag; replayable from Settings ⚙ → Replay tutorial (via a
tiny onboarding bus). Suppresses the What's-new popup for brand-new users (the tour
covers it) and marks the changelog seen on finish.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 10:08:56 +02:00
jtrzupek
c154deab37 feat(sources): 0-5★ ranking on Sites (freshness/metadata/plays) + playback telemetry
Rates each source on three axes the user asked for:
- freshness: how recently/often new content arrives (newest age + 7d volume)
- richness: metadata coverage (thumbnail/tags/performers/description/studio/duration)
- plays: does it actually play — from real playback telemetry when available,
  else a proxy from the resolve mechanism. 0★ = offline (gates the overall stars,
  so a fresh+rich source that doesn't play still ranks bottom — the hqfap/4k69 case)

Backend:
- playback_events: fire-and-forget telemetry POST from the app per playback attempt
  (origin + success/error + time-to-first-frame), append-only, 30d retention
- source_stats: per-origin computed scores, refreshed by a scheduler job (6h);
  /sources joins it and sorts by stars
- models + local migration 0025; new GOON_SCHED_SOURCE_STATS_HOURS setting

Mobile:
- Sites rows show ★ rating; tap the stars for a breakdown (axes + metadata %, plus
  whether "plays" is measured or estimated)
- PlayerScreen reports playback success/failure per source (native path only —
  symmetric, conservative); origin threaded through Scene/Movie play callsites

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 10:00:59 +02:00
jtrzupek
4afebacad8 feat(mobile): movies — performer filter + 3-column grid
Two Movies-list reports. (1) 1044cd34 'do movies have a metadata base for performers/categories/studio/year': yes — 90% have year, 92% studio, 81% performers, 93% tags, and the filter already covered studio/genre/year. Added the missing dimension: a performer search-and-select in MovieFiltersSheet (backend listMovies + api.ts already accepted performer_ids; only the UI was missing). (2) 0200956f 'use the space better': Movies grid goes 2 -> 3 columns (poster card is flex:1, scales fine) so ~50% more films per screen.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 23:21:22 +02:00
jtrzupek
249ad49430 fix(mobile): double-tap seek no longer pops the center pause control
Report dc4e91fb: double-tapping to skip ±15s also called setControlsVisible(true), throwing the full controls (big center pause button) on screen for 3.5s. Seek already has its own ±15s hint overlay, so the controls pop was redundant — removed it. Single-tap still toggles controls.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 22:27:08 +02:00
jtrzupek
78d26c4bc6 feat(mobile): strip .com/.org clutter from site names
User-report 18105d14: drop the TLD suffix from Sites list + SiteScenes header (hqporner.com -> hqporner, fpo.xxx -> fpo). Logos skipped (needs a per-site logo source) — TLD strip is the quick win.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 16:38:45 +02:00
jtrzupek
8b216018a2 feat(mobile): Hidden content screen — blacklist tags/performers/studios
User-report 86a9ec72 ('remove all gay scenes from randomly popping up'): there was no UI to hide a tag, nor to view/undo the blacklist — even though the 'Hide performer' alert promised 'undo from Settings -> Blacklist' (a screen that never existed). New BlacklistScreen: search-and-add any tag to hide (e.g. a category), plus manage/unhide all blacklisted tags/performers/studios. Reached via Settings -> Content -> Hidden content. Backend already drops blacklisted-entity scenes from every /scenes (device-scoped); this just exposes it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 15:53:09 +02:00
jtrzupek
b0e15935c6 fix(mobile): stop full scene-list refetch on back-navigation (perf)
Returning to the Scenes list from a scene caused a full reload + phone load spike (report 5df48551). Cause: invalidateQueries(['scenes']) in SceneDetail/Player/Performer/Studio handlers — including the silent auto-enrich-thumbnail that fires on opening any thumbnail-less scene — forces react-query to refetch EVERY loaded page of the infinite list. Added refetchType:'none' to all ['scenes'] invalidations: marks stale without refetching the active list, which refreshes on pull-to-refresh / filter change instead. Scene detail (['scene', id]) still updates immediately.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 14:14:07 +02:00
jtrzupek
567a8fb3b5 fix(mobile): scene-list scroll perf + native phone-side fpoxxx resolver
(1) Scroll jank/device load on long scene lists (report 5b7ca1e1): SceneTile is now React.memo'd so typing in search no longer re-renders every mounted tile, and sceneGridProps bounds the render window (windowSize 7 etc.) — required because removeClippedSubviews stays false to avoid thumbnail blanking. Applies to all scene grids. (2) fpoxxx played an ad instead of the video via the WebView fallback (reports f79beefb/cfa207c7). fpoxxx is KVS with an IP-bound + session-bound get_file token (cross-IP 403 confirmed), so it must resolve phone-side: new fpoxxxResolver fetches the page + follows get_file on the device (KVS real_url port for the function/0 case), wired into SceneDetailScreen like sxyprn/eporner. Verified from a residential IP: get_file -> CDN returns 206 video/mp4.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 11:02:21 +02:00
jtrzupek
00f4779abe feat(mobile): column toggle, duration filter, saved searches, screen protection (mobilism feedback)
Batch from user feedback: (1) Grid columns 1/2/3 setting (PreferencesContext, persisted) across all scene grids — default 2 was too small on phones. (2) Min-duration filter chips (5/10/20/30+ min) to hide ad-clips. (3) Saved-search chips + Save button (backed by /saved-searches). (4) Re-enabled screen-capture protection (Recents hide + screenshot block) for distributed users — verified active on emulator (screencap returns 0 bytes). (5) 'Checking for updates' gate before the PIN screen so a background OTA restart no longer causes a double PIN prompt. Changelog entry added. Published OTA runtime 1.1 (a9620b12).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 13:52:27 +02:00
jtrzupek
3714afa22f fix(mobile): capture site/origin text params in bug-report auto-context
SiteScenes passes the tube as origin/name (strings), not UUIDs, so the existing UUID-only auto-context loop dropped them. Reports like 'ingest of this site has been stuck 2 days' (14f3a655) arrived without any site identifier. Add a second loop for known string identity params (origin/name/sitetag/tag/q), length-capped, so per-site/per-performer reports become actionable.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-15 09:35:58 +02:00
jtrzupek
b66dd99eba fix(mobile): show Refresh thumbnail when the hero image actually fails to load
The button keyed on thumbnail_url presence, but a URL can be present yet broken (hqfap
404 → blank hero, no button — report ef0c6a5a). Tie it to the hero Image load state
(onLoad ok / onError broken / no url none) and show Refresh only when the image is
broken or missing. Reconciles 26c114ed (hidden for good previews) with ef0c6a5a.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-14 14:18:44 +02:00
jtrzupek
2a9445fe4a feat(mobile): auto-accept age-gate modal in WebView fallback
4k69 (and similar) show an "Are you 18 or above? Yes/No" modal that blocks the jwplayer
from initialising, so the WebView fallback never extracts a stream. Click the age-gate
accept button by id (#pop_up_18_yes and id*=18_yes/age_yes variants) on the same loop as
the consent/play-poster auto-clickers. Verified on emulator: 4k69 age-gate clears and the
player initialises (ExoPlayer hands off). A VAST preroll is still grabbed instead of the
okcdn content for 4k69 specifically (report 5de3fbc5 stays open) - separate ad-filter work.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-14 11:31:12 +02:00
jtrzupek
08410fddd1 fix(mobile): show Refresh thumbnail only when preview missing or broken
The Refresh thumbnail button appeared on every scene, which is noise for the majority
that already have a good preview (report 26c114ed). Show it only when no source has a
usable thumbnail or the only thumbnails are rotting (sxyprn/trafficdeposit), which is
exactly when a manual refresh helps (the original d3376a71 case).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-14 11:17:18 +02:00
jtrzupek
9269b02a4c feat(mobile): source-code link in Settings + Refresh thumbnail button
- AppLockSettings: a "Source code" row linking the public OSS repo (report 4c5066b8) -
  a trust signal for a sideloaded FOSS app (audit / self-host / contribute).
- SceneDetail: a "Refresh thumbnail" button (force) for scenes whose preview is broken
  or stale (report d3376a71).
- changelog: new What's New entry for this batch.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-13 19:04:11 +02:00
jtrzupek
86b3e88f08 fix(mobile): remount Favorites lists on numColumns change (GOON-11)
The new Scenes tab uses a 2-column FlatList while Performers/Studios/Movies are
1-column. Switching tabs reused the same FlatList instance, so numColumns changed on
the fly and RN threw "Changing numColumns on the fly is not supported" (5 users).
Give the Scenes list a distinct key ("fav-scenes") from the shared single-column key
("fav-list") so React remounts a fresh FlatList across the 1<->2 boundary.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 16:19:19 +02:00
jtrzupek
e618087eae feat(mobile): "What's new" popup after OTA updates
After an OTA bundle is applied, show a one-time popup listing recent changes. The
changelog ships in the bundle (mobile/src/changelog.ts), so it is always in sync with
the code that just arrived. WhatsNewModal compares the newest entry id against the last
one seen (SecureStore); shows unseen entries, marks seen on dismiss, and stays quiet
until the next update adds an entry. First run shows only the newest entry (no history
dump). Mounted over the navigator when signed in.

Each OTA publish should prepend a new entry at the top of CHANGELOG.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 11:41:54 +02:00
jtrzupek
a00acdddfb feat(mobile): "Your messages" inbox on bug FAB + geo-block playback hint
Bug FAB now has two tabs: "Report a bug" (existing) and "Your messages", which lists
this device's reports with any admin reply in a highlighted box. A badge dot on the FAB
shows unread replies; opening the tab marks them seen. Polls every 90s and on open.

PlayerScreen: when the WebView fallback (residential IP) cannot extract a stream within
25s and there is no 404/410, show a one-time hint that the source may be blocked in the
user's region or by their ISP (try another source or a VPN) - so a geo/network block on
the user's side does not read as a broken app.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 11:35:44 +02:00
jtrzupek
aebacc0389 feat(mobile): Favorites "Scenes" tab to view saved scenes
Users could heart individual scenes from SceneDetail, but the Favorites screen only
had Performers/Studios/Movies tabs, so saved scenes were invisible (bug report: got a
bunch of scenes saved but no way to see them). Add a Scenes tab (now the default)
listing favorited scenes as tiles via GET /scene-favorites, long-press to remove.
Adds client.listSceneFavorites().

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 10:06:12 +02:00
jtrzupek
16eb633bde feat(mobile): phone-side resolvers for IP-bound tubes (sxyprn, eporner, voe)
These CDNs bind their signed video URL to the IP that fetched the page, so a
server-side resolve hands the phone a URL bound to the server IP -- the device then
gets a placeholder/403 and falls back through the proxy, streaming the whole video
through the server. Resolve on the device instead (token binds to the phone IP) so
playback goes direct with zero proxy bandwidth.

Ports of the existing backend extractors:
- sxyprnResolver.ts: data-vnfo + boo/ssut51 transform
- epornerResolver.ts: vid+hash -> /xhr/video mp4 sources
- voeResolver.ts: mirror redirect + 7-step payload decoder

Wired into SceneDetailScreen.onPress (sxyprn/eporner) and MovieDetailScreen.playVoe (voe).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 16:14:25 +02:00
jtrzupek
32c18a6d0f fix(mobile): English long-press action labels + clean thumb error placeholder
bug-report c25e9b55: long-press scene actions were in Polish — translate menu,
banner and confirm dialogs to English. Thumb 'error' state (e.g. expired sxyprn
thumbnail 404) now shows the same 🎬 placeholder as 'empty' instead of a ⚠ broken
glyph (bug 2026-06-10).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 10:11:10 +02:00
jtrzupek
200db33d78 feat(mobile): send X-Device-Id, one-time adopt-legacy
GoonClient attaches a stable per-install device id (SecureStore, lazy UUID) on
every request so server-side user state is scoped per device. On first launch
after update, call /me/adopt-legacy once (SecureStore flag) to claim the previous
shared state onto this device — the instance owner should relaunch first.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 08:58:02 +02:00
jtrzupek
904f8984c8 feat(mobile): tile long-press actions (hide / mark-duplicate), drop dead preview
bug-report 5a6844db: the hold-to-preview animated gesture did nothing useful.
Replace it with a long-press action menu on scene tiles:
  - Ukryj scenę → POST /scenes/{id}/hide
  - Oznacz jako duplikat → enter selection mode; tapping another tile merges the
    long-pressed scene INTO the tapped one (POST /scenes/{keep}/merge/{drop}).
SceneActionsProvider holds the selection state + a bottom banner, so it works across
all 5 scene-list screens via the shared SceneTile (no per-screen wiring). Selecting
mode highlights tappable tiles and badges the pending duplicate. Animated thumbnails
kept only as a still-fallback image; has_animated_thumbnail filter removed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 09:52:15 +02:00
jtrzupek
7f36865b5a fix(performer): tag chips → in-place horizontal filter selector
Follow-up to 1a4bf258 feedback (a627637b + 0264a3ff): the flexWrap chip list ate
too much vertical space and tapping navigated away to TagScenes. Rework: single-row
horizontal scroll of toggle-chips that filter the performer's scenes IN-PLACE
(performer_ids + tags in one listScenes query, no navigation). Selected chip is
highlighted with a ✕ affordance; tap again clears. One line tall instead of N rows.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 09:25:02 +02:00
jtrzupek
ffb80c7b60 feat(performer): replace dev Re-scrape button with top-tag chips
bug-report 1a4bf258: "Re-scrape mógłby zniknąć, za to tagi/kategorie by mogły".
Re-scrape was a dev-only bulk thumbnail/tag enrich — noise on the performer page
(per-scene enrich already happens on SceneDetail). Removed it; kept Search.

New GET /performers/{id}/tags aggregates scene_tags across the performer's
live-playback scenes (top N). PerformerScenes renders them as chips → tap navigates
to TagScenes. Search button widened to full row.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-08 11:56:26 +02:00
jtrzupek
8f34a3e2f1 fix(mobile): movie part picker as scrollable modal — Android showed only 3 of N
paradisehill multipart movies passed all N parts to Alert.alert, but Android's
native AlertDialog renders at most 3 buttons → a 35-part movie showed 3 (bug-report
2ebd0690 2026-06-07). Backend correctly returns all 35; the cap was client-side.
Reuse PlaybackQualityModal (now scrollable + title + preserveOrder props, hides
bogus "1p" for non-resolution labels). Also add the missing `raw` field to the
StreamLink type (backend sends it; part_label lives there).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-08 10:25:03 +02:00
jtrzupek
940d4872e3 fix(mobile): removeClippedSubviews=false on grids — stop thumbnails vanishing on scroll
Android FlatList defaults removeClippedSubviews=true, which detaches off-viewport
subviews; expo-image frequently fails to re-render them when they scroll back in →
blank thumbnails (bug-report f181d382 2026-06-07, recurring). Disable on all heavy
image grids: scene grids (Scenes/Site/Studio/Tag/Performer) + movie poster grids.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-08 10:18:48 +02:00
jtrzupek
bc72515227 fix(player): drop "Tap for sound" pill — speaker toggle is enough
User feedback (2026-06-07, report 4bdca61e) on the prior mute change: the always-
visible "Tap for sound" pill is redundant — the 🔇/🔊 toggle in the top controls
is enough. Removed the pill (+ its styles); video still starts muted and the
speaker toggle unmutes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-08 09:20:42 +02:00
jtrzupek
63880feeb1 fix(mobile/api): handle 204 in request() — "Mark as invalid" false failure
The generic request<T>() always called res.json(), which throws on a 204 No
Content body. mark-dead endpoints (scene + movie "Mark as invalid"/broken)
return 204, so the call threw AFTER the backend had already marked the source
dead → user saw a "Failed" alert and the list didn't refresh, even though the
mark succeeded server-side (bug-reports 2026-05-28 Voe, 2026-06-03 scene
1e8dc190). Return undefined for 204 before parsing JSON.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 19:24:29 +02:00
jtrzupek
3339d3cd14 fix(playback): recognize luluvids.top/cdnstream/cdnvids as P.A.C.K.E.R. hosters
mypornerleak embeds luluvids.top (+ cdnstream.top/cdnvids.top) which are
luluvid/streamwish forks on new TLDs, all confirmed P.A.C.K.E.R.-JWPlayer. They
were missing from PACKER_HOSTS, so isPackerHoster() returned false → the phone-
side packer resolver never ran → WebView fallback landed on luluvids.top's
"disable Adblock and enable popup" wall (bug-report 2026-06-07, scene 75aa3316).
filemoon variant (bysezoxexe.com) was already covered.

Verified on emulator (live OTA): mypornerleak source → luluvids.top resolves
phone-side → native ExoPlayer PLAYING (position advancing), no adblock wall.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 16:23:22 +02:00
jtrzupek
b18f07d90e feat(playback): native pornxp.ph via phone-side resolver (kills black screen)
pornxp.ph serves direct <source> mp4 (360/720/1080p) on st.pornxp.sh whose path
token is IP-bound to whoever fetched the PAGE (verified 2026-06-07: VPS-resolved
URL → 403 cross-IP). Backend resolve was therefore impossible, so pornxpph fell
to the WebView fallback which black-screened (bug-report fd06cd86).

Fix: resolve on-device (same pattern as getfileResolver/doodstream) — the phone
fetches the page, so tokens bind to the phone IP and play natively. New
pornxpResolver.ts extracts the <source> mp4s into multi-quality StreamLinks;
SceneDetail short-circuits tube:pornxpph to it before backend resolve, feeding
the existing quality-picker + native player.

Verified on emulator (live OTA): pornxpph scene → quality picker (1080/720/360)
→ native playback PLAYING (no WebView, no ads, no black screen).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 14:58:40 +02:00
jtrzupek
4d14f3946b feat(player): start muted, unmute via button (autoplay-friendly)
Scenes/movies now start with sound OFF; user enables audio via a control
(UX request). NativeVideoPlayer: useVideoPlayer starts muted=true + speaker
toggle in top controls + always-visible "Tap for sound" pill while muted.
WebView path: injected autoplay sets muted=true (also makes muted autoplay
reliable per browser policy → faster CDN extraction); host player controls
handle unmute when the WebView is the actual surface.

Verified on emulator against the live runtime-1.1 OTA bundle: video starts
muted (pill shown), tap unmutes (pill clears).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 14:03:52 +02:00
jtrzupek
77323d23e6 fix(playback): retry DoodStream/playmogo resolve, handle "RELOAD" token response
porndish scenes resolve only to playmogo.com embeds, which are DoodStream clones
(doodcdn.io + pass_md5 + Cloudflare Turnstile). The mobile resolver already
supported playmogo, but DoodStream is flaky from a single shot: the embed is
sometimes Turnstile-gated (no pass_md5), and the pass_md5 endpoint intermittently
returns the literal string "RELOAD" (stale/consumed token) instead of a base URL.
The old code built "RELOAD<suffix>?token=..." -> ExoPlayer "no extractors" ->
WebView -> loading forever (bug 62e78c9a).

Wrap resolveDoodStream in a 3-attempt retry that re-fetches the embed (fresh
token) on retryable failures (gate / RELOAD / empty / stale token), and reject a
non-http pass_md5 body as retryable instead of building a garbage URL. Verified
cross-IP that the pass_md5 -> base -> final flow yields 206 video/mp4 when not
gated; real carrier IPs are gated far less than the test proxy. Strict
improvement: worst case is the existing WebView fallback, best case native play.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 21:14:26 +02:00
jtrzupek
83918e9a8d perf(movies+scenes): direct-play #hash movie hosters; skip empty blacklist filters
Movies: the seekplayer-engine family (easyvidplayer/player4me/seekplayer/
embedseek/upns, ~322k sources) returns a time-bound master.m3u8 on a CDN with a
valid IP-SAN cert that plays cross-IP. Mark it mobile_direct in resolve, and make
MovieDetailScreen prefer direct_url with a proxy fallback (mirrors the scene
path) — previously every movie streamed through the VPS proxy. Paradisehill
multipart parts now go direct too. Device-verified: ExoPlayer plays the raw CDN
direct, zero proxy traffic, no flicker.

Scenes: the three blacklist NOT EXISTS clauses were appended to every filtered
list and evaluated per-row even when all blacklist tables are empty (~3.4s tax on
a deep mega-tag walk). Skip them when the tables are empty (cached check) —
mega-tag list 6.7s -> 3.3s, and every filtered list benefits.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 19:44:41 +02:00
jtrzupek
a5ec6ca991 mobile: page-side get_file resolve for hdporngg/fullmovies (native, no proxy/flicker)
Device logs (not assumptions) pinned the real cause of the hdporngg/fullmovies
flicker: the backend returns a get_file URL, but get_file is bound to the IP that
loaded the *page*. The backend (VPS) loads the page, so the get_file is VPS-bound;
the phone fetching that get_file gets HTTP 410 -> ExoPlayer errors -> falls back to
the proxy via nav.replace (the "flicker"), and ends up streaming through the proxy.
(My earlier "stateless/portable" test was from the VPS — same IP as the page load —
so it wrongly showed 206.)

Fix: when the direct_url is a get_file, the phone re-fetches the *page* itself
(resolveGetFilePage on source.page_url) so the get_file is bound to the phone IP,
picks the requested quality skipping 4K (dead on fpvcdn), follows to the CDN, and
hands ExoPlayer a working URL. On failure it keeps the original (proxy fallback).

Verified on device: [getfile] page-resolve -> get_file 206 -> ExoPlayer PLAYING,
position advancing, no error/proxy/flicker, real video frame rendered.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 00:16:19 +02:00
jtrzupek
e5b6e8968c mobile: resolve get_file redirect client-side (kills hdporngg flicker)
hdporn.gg/fullmovies.xxx return an unresolved get_file direct_url that 302-redirects
to fpvcdn.com with the requester IP baked in. The backend can't resolve it (would
bind fpvcdn to the VPS IP -> mobile 403), so the phone must follow the redirect. But
ExoPlayer errors on that cross-domain get_file->fpvcdn redirect (drops Referer / won't
complete it) -> the native player falls back to the proxy via nav.replace, which the
user sees as a screen-reload "flicker" before playback (and means it's actually playing
through the VPS proxy, not direct).

Fix: resolve the get_file 302 in JS on the phone (so fpvcdn binds to the phone IP)
before navigating to the player, and hand ExoPlayer the final fpvcdn URL directly —
no redirect, no error, no flicker, no proxy. Uses the same redirect:'manual' +
Location-header pattern as the doodstream resolver (works on RN Android). On resolve
failure it keeps the original get_file URL (current behaviour with proxy fallback).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 23:49:40 +02:00
jtrzupek
24fc790691 mobile: skeleton grid while scene lists load (perceived perf)
Scene-list screens showed a small spinner while waiting on the API, so a slow
list read felt like a blank stall. Replace the initial-load spinner on
ScenesScreen and TagScenesScreen with a SceneGridSkeleton — a 2-col grid of
pulsing placeholder tiles laid out 1:1 with SceneTile (16:9 thumb + title + meta
lines). It paints instantly with zero data, so the screen feels responsive even
when the query takes a moment, and the skeleton->content swap doesn't reflow.

Pairs with the backend list-count fix (most filtered lists are now ~0.1s); the
skeleton also masks the residual slow path (enormous tags) so it no longer reads
as a freeze.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 12:03:33 +02:00
jtrzupek
4bc594cde3 chore(mobile/login): drop dead brandDot/title styles from old design
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 09:25:09 +02:00
jtrzupek
7981ba3408 fix(mobile/login): one-tap public pairing, manual URL/key behind Advanced
Pairing is automatic (App.tsx auto-connects to the public instance when no creds are
stored); the login screen only appears after an explicit Sign out. It defaulted to
localhost + empty key, forcing manual entry that no longer reflects how pairing works.
Now it prefills the public backend + shipped key (one-tap 'Connect to public instance')
and tucks the URL/API-key fields under an 'Advanced · self-hosted backend' toggle for
power users.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 09:22:50 +02:00
jtrzupek
bb5a97e288 fix(mobile): unify header branding to logo mark + fix top-tab overflow on narrow phones
- Header showed the 'goon' text wordmark while the login screen leads with the GoonMark
  symbol — switch the header to GoonMark so the logo is consistent across login + main.
- Scenes/Movies/Sites could overlap the header action icons on narrow phones: the mark is
  narrower than the wordmark, row gap reduced 16->10, and the 'Sign out' text replaced with
  a compact icon — frees ~80px so the left (logo+tabs) and right (actions) fit down to ~320px.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-01 21:44:16 +02:00
jtrzupek
7b2f093d85 mobile: recover from mid-playback decode/seek errors (doply NAL)
Bug f6c86847/b1b5e1a2: doply/playmogo plays fine but seeking throws
"source error, invalid NAL length" in ExoPlayer. Investigation (cross-IP,
2026-06-01) showed the stream is well-formed — faststart MP4 (moov before
mdat) on cloudatacdn.com which fully supports HTTP range (206, correct
content-range, repeatable token, no redirect). So it is an ExoPlayer-internal
seek failure, not an HTTP/container problem, and expo-video exposes no
extractor/MIME hint to influence it.

Mitigation: when the native player errors *after* it had already loaded
(i.e. a mid-playback/seek failure, not an initial-load failure) and the error
is not a 404/410, recreate the source via player.replace() and resume at the
last known position — this opens a fresh connection and re-parses moov, which
typically clears the transient decode error. Hard-capped at 2 attempts per
mount to avoid any auto-reload loop; if it still fails it falls through to the
existing proxy/WebView fallback and error UI. Initial-load errors are
untouched, so the resolver and the ~59k working doply sources are unaffected.

Also thread playbackId/entityKind through the resolved-hoster and proxy/WebView
nav.replace calls so those paths get the 404 "Mark broken" affordance too, and
complete the local RouteParams type with headers/fallbackProxyUrl.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-01 11:15:21 +02:00
jtrzupek
967123d5d6 mobile: let players identify & report 404/410 sources
When a host returns 404/410 at playback time (CDN gone, video removed) the
player previously showed only a raw error and a Back button — the user could
not tell it was a dead source or report it without going back to the detail
screen (bug a78cc3b6: "fpo i sxyprn to 404, którego apka nie potrafi
zidentyfikować").

- Thread playback_source.id into Player route params (scenes + movies).
- Native player error overlay: detect 404/410 in the ExoPlayer error, show
  "Source no longer available" and a "Mark broken" button that marks the
  source dead and returns. 403 is excluded (proxy/WebView fallback may save it).
- WebView player: add onHttpError; on a main-document 404/410 show the same
  overlay (Mark broken / Try anyway / Back) instead of the host's 404 page.
  Guarded to the loaded document (host+path) so same-host ad/subresource 404s
  don't false-trigger.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-01 11:04:58 +02:00
jtrzupek
31d9076f27 fix(player): skip VAST preroll ad mp4s in WebView stream scrape
WebView-fallback hosts (pornditt, xhamster, 0dayxx, sxyland, fpoxxx, porndoe)
inject a VAST preroll ad video (trafostatic.com / bkcdn.net / gripi.online / ...)
that loads before the real content. The INJECTED_JS performance scrape grabbed
that ad mp4 and handed it to ExoPlayer, so the native player showed the 30s ad
instead of the video (user bug: "pornditt łapie reklamę zamiast video").

report() now calls isAdHost() and skips ad-network video URLs; extended AD_HOSTS
with the video-ad CDNs. Content CDNs (sacdnssedge etc.) still pass through.
Shipped via OTA runtime 1.1 (update ea4b9901).

NOTE: this fixes ad-scraping for the WebView class generally, but pornditt itself
is separately broken — its content get_file fails to load even in a real desktop
browser from a residential IP (MEDIA_ERR code 4; only the ad mp4 loads) and its
player config is dynamic/obfuscated (no inline flashvars to resolve server-side).
pornditt effectively unplayable for now — see task; deprioritize / fall back to
other sources. yespornvip (clean backend resolve) is unaffected by this.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-01 10:24:17 +02:00
jtrzupek
00ea8c3fd4 fix(player): hide ad-heavy WebView behind opaque cover until stream scraped
User bug: opening a WebView-fallback scene (yespornvip etc.) shows the host's
ad-heavy page while INJECTED_JS auto-plays + scrapes the stream url in the
background. User sees ads instead of a loading state.

Render an opaque cover (theme.bg + spinner "Loading video…") over the WebView
while !extractedUrl. The WebView is still laid out and painted underneath, so
media keeps playing (autoplay via mediaPlaybackRequiresUserAction=false) and the
performance-scan picks up the CDN url — but the user only ever sees a loading
screen, then the native player. Applies to every WebView-fallback host.

Safety: if no stream is scraped within 15s (host needs a real tap to start),
reveal the WebView so the user can interact manually — no worse than before.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 21:47:35 +02:00
jtrzupek
a3be78373f fix(player): scrape real CDN url for KVS hosts, not session-bound get_file
yespornvip (and other KVS / kt_player tubes) play via the WebView fallback:
INJECTED_JS scrapes <video>.src and hands it to ExoPlayer. For KVS, <video>.src
is a get_file/N/<hash>/... intermediate that 302-redirects to the CDN, but that
redirect is bound to the WebView's cookies/session (and is effectively one-shot).
ExoPlayer's separate request gets "Source error: response code 410" (user bug
2026-05-31, scenes Delicious Dulce / Alexis Fawx).

The actual playable CDN url (e.g. tsvideo.sacdnssedge.com/video/ol_<hash>.mp4) is
portable (206 with no cookies/referer) but never appears in <video>.src or
XHR/fetch — only in Performance resource timing (the native media loader fetches
it after the 302). Verified live in Chromium on the exact broken scene.

INJECTED_JS now:
- skips get_file intermediates (INTERMEDIATE_RE) so they're never sent to ExoPlayer
- skips scrubber preview/heatmap/sprite mp4s (PREVIEW_RE)
- scans performance.getEntriesByType('resource') each tick and reports the real
  CDN media url — cross-origin entries expose .name even without Timing-Allow-Origin

Pure JS → shipped via OTA runtime 1.1 (update d4708fed).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 20:07:53 +02:00
jtrzupek
5ae5dbb201 perf(scenes): bounded count + has_more for filtered scene lists
Filtered /scenes (tag/origin/q/studio/performer) ran exhaustive COUNT with
stub-filter EXISTS over 1.7M rows: TAG 5.1s, ORIGIN 4.9s, SEARCH 3.1s.
Mobile relied on `loaded < total` for infinite-scroll, making exact count
mandatory and ruling out approximate shortcuts.

Backend:
- SceneListOut gains has_more (bool) and total_capped (bool), both optional
  for backward compat with old mobile
- Filtered count uses LIMIT _COUNT_CAP+1 (1000) subquery — cost is
  O(min(matches, cap)) instead of O(all). Measured: TAG 5.1s→664ms,
  SEARCH 3.1s→138ms, ORIGIN 4.9s→1.07s (also fixes SiteScenes showing
  global count ~1M instead of per-site count)
- has_more from fetching per_page+1 rows (essentially free); extra row
  stripped before serialisation
- Pure-default list (no filters at all) keeps TTL-cached full count

Mobile:
- getNextPageParam uses has_more ?? fallback to loaded<total
- Display shows "{total}+" when total_capped=true (5 screens)

Verified on emulator: tag "Big Tits" → "1000 scenes" loaded, no 500s,
backward compat confirmed (old APK works against new backend).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-31 19:24:26 +02:00
jtrzupek
b942565a6e i18n(mobile): polish UI strings → English
Tłumaczenie wszystkich user-facing stringów PL→EN (bug-report 2026-05-31
"dalej wszystko po polsku"). Alerty, przyciski, placeholdery, labelki w 12
ekranach/komponentach: BugReportFAB, AppLock(Screen/Settings/PinEntry),
applock biometric prompts, doodstream error msgs, MovieDetail, PlaybackQuality,
Player, SceneDetail, ScenesFilter, SiteScenes. Komentarze w kodzie zostają PL.

Zmiany były WIP drugiego okna (uncommitted); wjechały do bundla 0.2.1 przy
buildzie (były w working tree) — apka zainstalowana już ma EN. Ten commit
utrwala je w gicie żeby nie zginęły. Czysto stringi, zero zmian logiki.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 16:27:55 +02:00
jtrzupek
0281e449fe build(apk): 0.2.0 — expo-font native, runtime 1.1, fonts re-enabled
Option B (rebuild APK) — odblokowuje custom fonty na stałe + sprawia że
przyszłe font-OTA nie crashują.

- runtime 1.0 → 1.1 (app.json + AndroidManifest EXPO_RUNTIME_VERSION): nowy APK
  ma native ExpoFontLoader, więc MUSI mieć inny runtime niż stare instalacje 1.0
  (inaczej font-OTA crashnęłoby stare). 1.0 channel zostaje na d5b87e5c
  (font-stripped) dla starych, 1.1 = nowy APK z fontami.
- version 0.2.0 / versionCode 10 (build.gradle) — in-app updater (/version=0.2.0)
  zaoferuje install starym 0.1.9.
- Fonty przywrócone (useFonts, theme.fonts realne, SceneTile/MoviePosterCard/
  navigation/GoonWordmark fontFamily) — działają bo native jest w APK.
- Build: gradlew assembleRelease (autolinking expo-font, BEZ prebuild — zachowane
  custom native AntiTamper/ApkInstaller), Sentry source-map upload wyłączony
  (SENTRY_DISABLE_AUTO_UPLOAD, brak org/auth — krok poboczny).
- app/main.py /version 0.1.9 → 0.2.0.

ZWERYFIKOWANE na emulatorze: podpis SHA-256 == ALLOWED_APP_SIG_HASH (anti-tamper
OK), ExpoFontLoader w classes3.dex, `ReactNativeJS: Running "main"` bez crasha.
APK live: /static/app-release.apk + goon-v0.2.0.apk + landing webroot.

UWAGA: launcher-icon (native mipmaps) NIE zmienione w tym buildzie — nadal stara
ikona. Nowy oo-icon wymaga regeneracji res/mipmap-* + rebuild (follow-up).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 12:51:32 +02:00
jtrzupek
64506690df fix(ota+mobile): strip expo-font from bundle, runtime back to 1.0
DIAGNOZA NA EMULATORZE (emulator-5554, goon-v0.1.9.apk):
Dwa błędne założenia z poprzednich sesji obalone empirycznie:

1. RUNTIME: APK ma EXPO_RUNTIME_VERSION="1.0" (NIE 0.1.9 — pomyliłem versionName
   z runtime). App akceptuje TYLKO manifest runtime 1.0. Mój wcześniejszy
   "fix" na 0.1.9 (c19da51) był wstecz — app go ignorował. Cofnięte: app.json
   + publish_update RUNTIME_DEFAULT z powrotem na "1.0".

2. CRASH: prawdziwa przyczyna "nic się nie pojawia" — OTA bundle z expo-font
   crashował: "Cannot find native module 'ExpoFontLoader'" → expo-updates
   ErrorRecovery rollback. APK (build 22-maja) nie ma natywnego ExpoFontLoader
   (expo-font dodany 30-maja, PO buildzie APK). OTA NIE MOŻE dostarczyć native
   modułu. Potwierdzone: embedded bundle + served bundle grep = 0 ExpoFontLoader;
   stary font-bundle crashował, font-stripped NIE.

FIX: usunięto useFonts z App.tsx + expo-font import; theme.fonts → undefined
(system font); SceneTile/MoviePosterCard/navigation/GoonWordmark fontFamily →
fontWeight. Wszystko inne (2-col grid, oxblood, logo SVG-RNSVG-jest-w-APK)
zostaje. Custom fonty wrócą przy rebuildzie APK z expo-font (option B).

ZWERYFIKOWANE: bundle d5b87e5c (runtime 1.0, 0 ttf) — emulator launch:
`ReactNativeJS: Running "main"`, zero JS errors, brak ExpoFontLoader crash.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 11:41:29 +02:00
jtrzupek
732401cef2 feat(brand): oo logo — app icons, landing, in-app mark (Gemini-generated)
Wybrane logo: interlocked double-o "oo" (off-white ringi + oxblood doty na warm
charcoal). Wygenerowane przez Gemini 3 Pro Image (Nano Banana), wybrane z 8
wariantow przez Jana.

Native icons (wejda przy nast. APK buildzie, nie OTA):
- assets/icon.png, adaptive-icon.png, splash.png, favicon.png — zlozone z mastera
  przez PIL (mark wyciety po alpha-masce, osadzony na dokladnym #15110D, rozne
  fill-ratio per asset).
- app.json: adaptiveIcon + splash backgroundColor #0E1018 (stary navy) -> #15110D.

In-app (OTA):
- GoonMark SVG przerobiony z pojedynczego ring+dot na double-o (2 ringi + doty),
  spojny z app-icon. Login: mark 92 + wordmark 40.

Landing goon-foss.org (osobny deploy na caddy volume):
- /logo.png + /favicon.png upload, <link rel=icon/apple-touch-icon>, hero brand
  block: dot+text -> <img logo> + dwutonowy "g[oo]n".

OTA: a81ea98c-723d-4b39-9dc0-bbe8736c104e.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-30 22:38:52 +02:00