-
-
Notifications
You must be signed in to change notification settings - Fork 221
[mobileWallLayout] Add play-on-visibility and ordered loading #704
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
DogmaDragon
merged 2 commits into
stashapp:main
from
speckofthecosmos:mobilewalllayout-scrollfeed
Apr 22, 2026
+378
−103
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,29 +1,70 @@ | ||
| # Mobile Wall Layout | ||
| # scrollFeed (mobileWallLayout) | ||
|
|
||
| https://discourse.stashapp.cc/t/mobile-wall-layout/6160 | ||
| Discussion: https://discourse.stashapp.cc/t/mobile-wall-layout/6160 | ||
|
|
||
| Makes the wall-mode gallery render as a single full-width column on mobile | ||
| devices, on the **Markers** (`/scenes/markers`) and **Images** (`/images`) pages. | ||
| Turns Stash's **Markers** (`/scenes/markers`) and **Images** (`/images`) wall | ||
| into a scrollable mobile feed — full-width single-column layout, video | ||
| play-on-visibility, and DOM-ordered loading that keeps the feed watchable | ||
| over cellular and degrading connections. | ||
|
|
||
| By default, Stash's wall mode uses `react-photo-gallery`, which calculates | ||
| `position: absolute` offsets for a multi-column brick layout. On small screens | ||
| this produces items that are too small to comfortably tap and browse. This | ||
| plugin overrides those offsets so each item spans the full width of the screen, | ||
| making marker previews and images easy to scroll through on a phone. | ||
| The plugin is published under the filename `mobileWallLayout` (and that's | ||
| still the internal ID, so existing installs upgrade cleanly). The display | ||
| name it presents in Stash's Plugins panel is `scrollFeed`. | ||
|
|
||
| ## Behaviour | ||
| ## What it does | ||
|
|
||
| - Applies only on **touch-screen devices** (`pointer: coarse`) — correctly | ||
| targets phones and tablets without triggering on narrow desktop browser windows. | ||
| - Activates and deactivates automatically as you navigate between pages. | ||
| - Has no effect on desktop or mouse-driven viewports. | ||
| 1. **Full-width single-column layout on touch devices.** By default, Stash's | ||
| wall uses `react-photo-gallery`, which calculates `position: absolute` | ||
| offsets for a multi-column brick layout. On phones those offsets produce | ||
| items that are too small to tap through comfortably. The plugin injects | ||
| a `<style>` tag wrapped in a `@media (pointer: coarse)` query to override | ||
| them. Touchscreens get the mobile feed; desktop and mouse-driven | ||
| viewports are untouched. | ||
|
|
||
| ## Implementation note | ||
| 2. **Play-on-visibility.** Stash marks marker previews with `autoPlay`, so | ||
| a 20-card page can fire 20 simultaneous playbacks — iOS Safari bogs down | ||
| past its ~20-`<video>` decoder ceiling. An `IntersectionObserver` plays | ||
| each clip at 10% visibility and pauses it when it leaves the viewport. | ||
| In practice 2–3 clips play concurrently, which is what you want when | ||
| scrolling a feed. | ||
|
|
||
| The fix injects a `<style>` tag with `!important` rules wrapped in a | ||
| `@media (pointer: coarse)` query, rather than setting inline styles via | ||
| JavaScript or checking `window.innerWidth` at runtime. This is necessary | ||
| because `react-photo-gallery` continuously recalculates and re-applies its own | ||
| inline styles; a CSS rule with `!important` wins unconditionally regardless of | ||
| render timing. Using `pointer: coarse` instead of a pixel-width threshold | ||
| prevents the fix from activating on narrow desktop windows. | ||
| 3. **DOM-ordered loading.** When the wall mounts, React starts parallel | ||
| fetches for every video on the page. On cellular that splits the uplink | ||
| 20 ways and no video is playable for a long time. The plugin cancels | ||
| those fetches, pushes the videos onto an ordered queue, and re-issues | ||
| them top-down — 2 concurrent at a time, advancing on `canplay` or a | ||
| 500ms fallback. The top clip is playable quickly, and the entire page | ||
| is in-flight within ~5 seconds, so moving into a weaker-signal area | ||
| doesn't leave the bottom of the page with zero bytes. | ||
|
|
||
| ## Target pages | ||
|
|
||
| Active only on `/scenes/markers` and `/images`. Deactivates (removes its | ||
| style tag, disconnects its observers) on navigation away. No effect on any | ||
| other view. | ||
|
|
||
| ## Tuning | ||
|
|
||
| The primary knobs are declared as constants at the top of the load-queue | ||
| section in `mobileWallLayout.js`: | ||
|
|
||
| | Constant | Default | Effect | | ||
| |---|---:|---| | ||
| | `threshold` (IntersectionObserver) | `0.1` | Lower = scroll feels continuous (more clips partially playing); higher = stricter focus on the clip in view. | | ||
| | `_MAX_CONCURRENT_LOADS` | `2` | Higher = entire page finishes sooner but each clip loads slower; lower = top clips get more uncontested bandwidth but the tail waits longer. | | ||
| | `_LOAD_ADVANCE_MS` | `500` | Short = every video starts fetching sooner (better for degrading reception); long = top clips get more solo time before the pipe re-splits. | | ||
|
|
||
| ## Retention | ||
|
|
||
| `react-photo-gallery@8.0.0` does not virtualize — every photo in the | ||
| current page stays in the DOM. Video elements therefore keep their | ||
| downloaded bytes for the lifetime of the page, so scrolling back to a | ||
| clip you've already buffered resumes instantly even if the network has | ||
| since dropped. Retention scope is the current page; page-change remounts | ||
| the gallery and resets state. | ||
|
|
||
| ## Compatibility | ||
|
|
||
| Requires `IntersectionObserver`, `MutationObserver`, `WeakMap`, `WeakSet`, | ||
| `Element.isConnected`. All supported by mobile Safari 12.1+, Chrome, | ||
| Firefox, and Edge. | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.