video: smart multi-layer back button + next-level search/person
Smart back (mirrors music's artist-detail): the top-left back button now
remembers where you actually came from, many layers deep. It keeps an origin
stack ({page} or {detail title}) and stamps each history entry with its layer
depth, so:
- the label is dynamic — '← Back to Search' / '← Back to The Bear' / '← Back to
<person>' — instead of a hardcoded 'Library'/'Back';
- backing out of the first layer returns to the page you started from (Search,
Watchlist, wherever), not always the Library;
- browser Back and our button both unwind the chain one layer at a time, in sync.
Fixes: search → person → back → movie used to mislabel as 'Library' and dump you
in the library.
Next level:
- Search isn't a blank box when idle — a 'Trending this week' rail (TMDB
trending, owned/preview annotated). Returns when you clear the query.
- Person page gets a 'Known For' hero rail (top titles by popularity) above a
full filmography now sorted chronologically (newest first).
Backend: TMDBClient.trending + engine.trending (+library annotation), route
/api/video/trending. Isolated; 237 video-suite tests pass.
This commit is contained in:
parent
87f414c8c7
commit
d6d0dc537c
11 changed files with 231 additions and 19 deletions
|
|
@ -31,3 +31,13 @@ def register_routes(bp):
|
|||
logger.exception("video search failed for %r", q)
|
||||
results = []
|
||||
return jsonify({"results": results, "query": q})
|
||||
|
||||
@bp.route("/trending", methods=["GET"])
|
||||
def video_trending():
|
||||
try:
|
||||
from core.video.enrichment.engine import get_video_enrichment_engine
|
||||
results = get_video_enrichment_engine().trending()
|
||||
except Exception:
|
||||
logger.exception("video trending failed")
|
||||
results = []
|
||||
return jsonify({"results": results})
|
||||
|
|
|
|||
|
|
@ -272,6 +272,31 @@ class TMDBClient:
|
|||
"poster": (self.PROFILE + it["profile_path"]) if it.get("profile_path") else None})
|
||||
return out
|
||||
|
||||
def trending(self, window="week"):
|
||||
"""Trending movies + shows this week — fills the search page when idle."""
|
||||
if not self.api_key:
|
||||
return []
|
||||
import requests
|
||||
r = requests.get(self.BASE + "/trending/all/" + window,
|
||||
params={"api_key": self.api_key}, timeout=15)
|
||||
r.raise_for_status()
|
||||
out = []
|
||||
for it in ((r.json() or {}).get("results") or []):
|
||||
mt, tid = it.get("media_type"), it.get("id")
|
||||
if not tid or mt not in ("movie", "tv"):
|
||||
continue
|
||||
if mt == "movie":
|
||||
out.append({"kind": "movie", "tmdb_id": tid, "title": it.get("title"),
|
||||
"year": (it.get("release_date") or "")[:4] or None,
|
||||
"rating": it.get("vote_average") or None,
|
||||
"poster": (self.POSTER_W + it["poster_path"]) if it.get("poster_path") else None})
|
||||
else:
|
||||
out.append({"kind": "show", "tmdb_id": tid, "title": it.get("name"),
|
||||
"year": (it.get("first_air_date") or "")[:4] or None,
|
||||
"rating": it.get("vote_average") or None,
|
||||
"poster": (self.POSTER_W + it["poster_path"]) if it.get("poster_path") else None})
|
||||
return out[:20]
|
||||
|
||||
def full_detail(self, kind, tmdb_id):
|
||||
"""Complete detail for a TMDB title NOT in the library — shaped like the
|
||||
library detail payload but with direct image URLs (so the same detail UI
|
||||
|
|
|
|||
|
|
@ -182,6 +182,21 @@ class VideoEnrichmentEngine:
|
|||
r["library_id"] = self.db.library_id_for_tmdb(r["kind"], r["tmdb_id"])
|
||||
return results
|
||||
|
||||
def trending(self) -> list:
|
||||
"""Trending titles for the idle search page, annotated owned/not."""
|
||||
w = self.workers.get("tmdb")
|
||||
if not w or not w.enabled:
|
||||
return []
|
||||
try:
|
||||
results = w.client.trending() or []
|
||||
except Exception:
|
||||
logger.exception("video trending failed")
|
||||
return []
|
||||
for r in results:
|
||||
if r.get("tmdb_id"):
|
||||
r["library_id"] = self.db.library_id_for_tmdb(r["kind"], r["tmdb_id"])
|
||||
return results
|
||||
|
||||
def tmdb_detail(self, kind, tmdb_id) -> dict | None:
|
||||
"""Full detail for a TMDB title not in the library — same shape as the
|
||||
library detail (source='tmdb', direct image URLs, nothing owned). If it IS
|
||||
|
|
|
|||
|
|
@ -46,6 +46,7 @@ def test_blueprint_exposes_dashboard_route():
|
|||
assert "/api/video/detail/movie/<int:movie_id>/refresh-art" in rules
|
||||
assert "/api/video/detail/<kind>/<int:item_id>/extras" in rules
|
||||
assert "/api/video/search" in rules
|
||||
assert "/api/video/trending" in rules
|
||||
assert "/api/video/tmdb/<kind>/<int:tmdb_id>" in rules
|
||||
assert "/api/video/tmdb/show/<int:tv_id>/season/<int:season_number>" in rules
|
||||
assert "/api/video/person/<int:tmdb_id>" in rules
|
||||
|
|
|
|||
|
|
@ -256,6 +256,30 @@ def test_engine_search_annotates_library(db):
|
|||
assert "library_id" not in res[2] # people aren't library-matched
|
||||
|
||||
|
||||
def test_engine_trending_annotates_library(db):
|
||||
mid = db.upsert_movie("plex", {"server_id": "m1", "title": "Owned", "tmdb_id": 1})
|
||||
|
||||
class Tmdb:
|
||||
enabled = True
|
||||
def trending(self):
|
||||
return [{"kind": "movie", "tmdb_id": 1, "title": "Owned"},
|
||||
{"kind": "show", "tmdb_id": 2, "title": "Hot show"}]
|
||||
eng = VideoEnrichmentEngine(db, {"tmdb": Tmdb()})
|
||||
res = eng.trending()
|
||||
assert res[0]["library_id"] == mid and res[1]["library_id"] is None
|
||||
|
||||
|
||||
def test_tmdb_trending_parses(monkeypatch):
|
||||
body = {"results": [
|
||||
{"media_type": "movie", "id": 1, "title": "A", "release_date": "2024-01-01", "poster_path": "/a.jpg"},
|
||||
{"media_type": "tv", "id": 2, "name": "B", "first_air_date": "2023-05-05"},
|
||||
{"media_type": "person", "id": 3, "name": "C"}]} # people excluded from the rail
|
||||
monkeypatch.setitem(sys.modules, "requests", types.SimpleNamespace(get=lambda u, **k: _Resp(body)))
|
||||
res = TMDBClient("KEY").trending()
|
||||
assert [r["kind"] for r in res] == ["movie", "show"]
|
||||
assert res[0]["poster"] == "https://image.tmdb.org/t/p/w300/a.jpg"
|
||||
|
||||
|
||||
def test_engine_person_detail_annotates_credits(db):
|
||||
mid = db.upsert_movie("plex", {"server_id": "m1", "title": "Owned", "tmdb_id": 1})
|
||||
|
||||
|
|
|
|||
|
|
@ -394,6 +394,7 @@ def test_search_subpage_and_module():
|
|||
assert "(function" in src and "})();" in src
|
||||
assert "window." not in src # no globals
|
||||
assert "/api/video/search" in src
|
||||
assert "/api/video/trending" in src # idle page shows a trending rail
|
||||
assert "soulsync:video-open-detail" in src # results drill in via the shared event
|
||||
assert "themoviedb.org" not in src and "imdb.com" not in src # stays in-app
|
||||
|
||||
|
|
@ -432,6 +433,18 @@ def test_video_side_registers_detail_pages_and_open_event():
|
|||
assert "soulsync:video-open-detail" in _JS # navigates on the event
|
||||
|
||||
|
||||
def test_smart_back_button_remembers_origin():
|
||||
# The back button label is dynamic (a span we rewrite), and the JS keeps an
|
||||
# origin stack + layer-stamped history so back returns to where you came from
|
||||
# (not always the library).
|
||||
assert _INDEX.count("data-vd-back-label") >= 3 # all three detail back buttons
|
||||
assert "_backStack" in _JS and "currentOrigin" in _JS
|
||||
assert "updateBackLabels" in _JS
|
||||
assert "layer: _backStack.length" in _JS # depth stamped on history state
|
||||
# Fallback must use the recorded first origin, not a hardcoded library jump.
|
||||
assert "_backStack[0]" in _JS
|
||||
|
||||
|
||||
def test_music_worker_orbs_untouched_by_video():
|
||||
# The video orbs are a separate file; the music orbs must not learn about
|
||||
# the video side (one-way isolation — music never depends on video).
|
||||
|
|
|
|||
|
|
@ -871,7 +871,7 @@
|
|||
at load for a per-show glow. Built by video/video-detail.js. -->
|
||||
<div class="vd-page" data-video-detail="show">
|
||||
<button class="vd-back" type="button" data-video-detail-back>
|
||||
<span aria-hidden="true">←</span> Library
|
||||
<span aria-hidden="true">←</span> <span data-vd-back-label>Back</span>
|
||||
</button>
|
||||
<!-- Billboard: full-bleed backdrop, content anchored bottom-left. -->
|
||||
<div class="vd-billboard">
|
||||
|
|
@ -928,7 +928,7 @@
|
|||
<section class="video-subpage" data-video-subpage="video-movie-detail" hidden>
|
||||
<div class="vd-page" data-video-detail="movie">
|
||||
<button class="vd-back" type="button" data-video-detail-back>
|
||||
<span aria-hidden="true">←</span> Library
|
||||
<span aria-hidden="true">←</span> <span data-vd-back-label>Back</span>
|
||||
</button>
|
||||
<div class="vd-billboard">
|
||||
<div class="vd-bb-bg" data-vd-backdrop aria-hidden="true"></div>
|
||||
|
|
@ -1027,7 +1027,7 @@
|
|||
<div class="vp-page" data-video-person data-has-bg="0">
|
||||
<div class="vp-ambient" data-vp-ambient aria-hidden="true"></div>
|
||||
<button class="vd-back" type="button" data-video-detail-back>
|
||||
<span aria-hidden="true">←</span> Back
|
||||
<span aria-hidden="true">←</span> <span data-vd-back-label>Back</span>
|
||||
</button>
|
||||
<div class="vd-loading" data-vp-loading hidden>
|
||||
<div class="loading-spinner"></div><div class="loading-text">Loading…</div>
|
||||
|
|
@ -1043,6 +1043,10 @@
|
|||
<div class="vp-body">
|
||||
<p class="vp-bio" data-vp-bio></p>
|
||||
<button class="vp-bio-more" type="button" data-vp-bio-more hidden>Read more</button>
|
||||
<div class="vp-known-section" data-vp-known-section hidden>
|
||||
<h2 class="vp-credits-title vp-known-title">Known For</h2>
|
||||
<div class="vp-known-rail" data-vp-known></div>
|
||||
</div>
|
||||
<div class="vp-credits-section">
|
||||
<div class="vp-credits-head"><h2 class="vp-credits-title">Filmography</h2>
|
||||
<div class="vp-credit-tabs" data-vp-tabs></div></div>
|
||||
|
|
|
|||
|
|
@ -63,12 +63,24 @@
|
|||
}).join('');
|
||||
}
|
||||
|
||||
function renderKnownFor() {
|
||||
var section = q('[data-vp-known-section]'), host = q('[data-vp-known]');
|
||||
if (!section || !host || !data) return;
|
||||
// The credits arrive popularity-sorted → the top few are the "known for".
|
||||
var top = (data.credits || []).slice(0, 10);
|
||||
section.hidden = top.length < 3; // only worth a rail if there are a few
|
||||
host.innerHTML = top.map(creditCard).join('');
|
||||
}
|
||||
|
||||
function renderCredits() {
|
||||
var host = q('[data-vp-credits]');
|
||||
if (!host || !data) return;
|
||||
var credits = (data.credits || []).filter(function (c) {
|
||||
return tab === 'all' || c.kind === tab;
|
||||
});
|
||||
// Full filmography reads best chronologically (newest first); Known For
|
||||
// already covers the popular ones.
|
||||
credits.sort(function (a, b) { return (b.date || '').localeCompare(a.date || ''); });
|
||||
host.innerHTML = credits.map(creditCard).join('');
|
||||
}
|
||||
|
||||
|
|
@ -105,7 +117,7 @@
|
|||
if (bio) { bio.textContent = d.biography || ''; bio.hidden = !d.biography; bio.classList.remove('vp-bio--open'); }
|
||||
if (more) { more.hidden = !((d.biography || '').length > 320); more.textContent = 'Read more'; }
|
||||
|
||||
renderTabs(); renderCredits();
|
||||
renderKnownFor(); renderTabs(); renderCredits();
|
||||
var sub = document.querySelector('.video-subpage[data-video-subpage="video-person-detail"]');
|
||||
if (sub) sub.scrollTop = 0;
|
||||
}
|
||||
|
|
@ -118,6 +130,8 @@
|
|||
var m = q('[data-vp-meta]'); if (m) m.innerHTML = '';
|
||||
var c = q('[data-vp-credits]'); if (c) c.innerHTML = '';
|
||||
var t = q('[data-vp-tabs]'); if (t) t.innerHTML = '';
|
||||
var ks = q('[data-vp-known-section]'); if (ks) ks.hidden = true;
|
||||
var k = q('[data-vp-known]'); if (k) k.innerHTML = '';
|
||||
fetch(PERSON_URL + id, { headers: { 'Accept': 'application/json' } })
|
||||
.then(function (r) { return r.ok ? r.json() : null; })
|
||||
.then(function (d) {
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@
|
|||
var reqSeq = 0; // guards against out-of-order responses
|
||||
var timer = null;
|
||||
var wired = false;
|
||||
var trendingCache = null; // null = not fetched; [] = fetched/empty
|
||||
|
||||
function $(sel) { return document.querySelector(sel); }
|
||||
function esc(s) {
|
||||
|
|
@ -94,6 +95,34 @@
|
|||
host.innerHTML = html;
|
||||
}
|
||||
|
||||
// Idle state: a "Trending this week" rail so the page isn't a blank box.
|
||||
function renderTrending() {
|
||||
var host = $('[data-video-search-results]');
|
||||
if (!host || !trendingCache || !trendingCache.length) return;
|
||||
show('[data-video-search-hint]', false);
|
||||
show('[data-video-search-empty]', false);
|
||||
host.innerHTML = '<div class="vsr-group"><h2 class="vsr-group-title">' +
|
||||
'<span class="vsr-group-ic" aria-hidden="true">🔥</span>Trending this week</h2>' +
|
||||
'<div class="vsr-grid">' + trendingCache.map(titleCard).join('') + '</div></div>';
|
||||
}
|
||||
function loadTrending() {
|
||||
if (trendingCache !== null) { if (!lastQuery) renderTrending(); return; }
|
||||
fetch('/api/video/trending', { headers: { 'Accept': 'application/json' } })
|
||||
.then(function (r) { return r.ok ? r.json() : null; })
|
||||
.then(function (d) {
|
||||
trendingCache = (d && d.results) ? d.results : [];
|
||||
if (!lastQuery) renderTrending();
|
||||
})
|
||||
.catch(function () { trendingCache = []; });
|
||||
}
|
||||
function showIdle() {
|
||||
if (trendingCache && trendingCache.length) { renderTrending(); return; }
|
||||
show('[data-video-search-empty]', false);
|
||||
show('[data-video-search-hint]', true);
|
||||
var host = $('[data-video-search-results]'); if (host) host.innerHTML = '';
|
||||
loadTrending();
|
||||
}
|
||||
|
||||
function runSearch(q) {
|
||||
var seq = ++reqSeq;
|
||||
show('[data-video-search-loading]', true);
|
||||
|
|
@ -118,9 +147,7 @@
|
|||
if (!q) {
|
||||
reqSeq++; // cancel any in-flight render
|
||||
show('[data-video-search-loading]', false);
|
||||
show('[data-video-search-empty]', false);
|
||||
show('[data-video-search-hint]', true);
|
||||
var host = $('[data-video-search-results]'); if (host) host.innerHTML = '';
|
||||
showIdle(); // back to the trending rail
|
||||
return;
|
||||
}
|
||||
timer = setTimeout(function () { runSearch(q); }, 320);
|
||||
|
|
@ -162,6 +189,7 @@
|
|||
wire();
|
||||
var input = $('[data-video-search-input]');
|
||||
if (input) { try { input.focus(); } catch (err) { /* ignore */ } }
|
||||
if (!lastQuery) loadTrending(); // fill the idle page
|
||||
}
|
||||
|
||||
function init() {
|
||||
|
|
|
|||
|
|
@ -454,6 +454,10 @@ body[data-side="video"] .dashboard-header-sweep {
|
|||
backdrop-filter: blur(10px); transition: all 0.2s ease;
|
||||
}
|
||||
.vd-back:hover { background: rgba(0,0,0,0.7); transform: translateX(-2px); }
|
||||
.vd-back [data-vd-back-label] {
|
||||
display: inline-block; max-width: 230px; overflow: hidden; text-overflow: ellipsis;
|
||||
white-space: nowrap; vertical-align: bottom;
|
||||
}
|
||||
|
||||
/* ── Billboard ─────────────────────────────────────────────────────────────── */
|
||||
.vd-billboard { position: relative; width: 100%; min-height: 76vh; display: flex; align-items: flex-end; overflow: hidden; }
|
||||
|
|
@ -1023,6 +1027,16 @@ body[data-side="video"] .dashboard-header-sweep {
|
|||
}
|
||||
.vp-bio-more[hidden] { display: none; }
|
||||
|
||||
/* Known For — a horizontal hero rail of the person's best-known titles */
|
||||
.vp-known-section { position: relative; z-index: 2; margin-bottom: 38px; }
|
||||
.vp-known-title { margin: 0 0 16px; }
|
||||
.vp-known-rail {
|
||||
display: flex; gap: 18px; overflow-x: auto; padding: 4px 2px 14px; scroll-snap-type: x proximity;
|
||||
}
|
||||
.vp-known-rail::-webkit-scrollbar { height: 8px; }
|
||||
.vp-known-rail::-webkit-scrollbar-thumb { background: rgba(255, 255, 255, 0.15); border-radius: 4px; }
|
||||
.vp-known-rail .vsr-card { flex: 0 0 152px; width: 152px; scroll-snap-align: start; }
|
||||
|
||||
.vp-credits-section { position: relative; z-index: 2; }
|
||||
.vp-credits-head {
|
||||
display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 14px; margin-bottom: 20px;
|
||||
|
|
|
|||
|
|
@ -50,14 +50,68 @@
|
|||
document.dispatchEvent(new CustomEvent('soulsync:video-open-detail',
|
||||
{ detail: { kind: r.kind, id: r.id, source: r.source, _restore: true } }));
|
||||
}
|
||||
|
||||
// ── Smart back button (mirrors music's artist-detail) ─────────────────────
|
||||
// Browser history does the real (multi-layer) navigation; this stack only
|
||||
// tracks WHERE EACH detail layer was opened FROM, so the button can label
|
||||
// itself ("← Back to Search" / "← Back to The Bear") and so backing out of
|
||||
// the first layer returns to the right page — not always the library. Each
|
||||
// entry: {type:'page', pageId} or {type:'detail', label}. The pushed history
|
||||
// state carries its layer depth so browser Back stays in sync too.
|
||||
var _backStack = [];
|
||||
|
||||
function detailTitleOf(pageId) {
|
||||
if (pageId === 'video-person-detail') {
|
||||
var n = document.querySelector('[data-video-person] [data-vp-name]');
|
||||
return n ? (n.textContent || '').trim() : '';
|
||||
}
|
||||
var host = pageId === 'video-movie-detail' ? '[data-video-detail="movie"]' : '[data-video-detail="show"]';
|
||||
var t = document.querySelector(host + ' [data-vd-title]');
|
||||
return t ? (t.textContent || '').trim() : '';
|
||||
}
|
||||
function currentOrigin() {
|
||||
var page = document.body.getAttribute('data-video-page');
|
||||
if (DETAIL_PAGES[page]) return { type: 'detail', label: detailTitleOf(page) };
|
||||
return { type: 'page', pageId: page };
|
||||
}
|
||||
function backLabelText() {
|
||||
var top = _backStack[_backStack.length - 1];
|
||||
if (!top) return 'Back';
|
||||
if (top.type === 'detail') return top.label ? ('Back to ' + top.label) : 'Back';
|
||||
return 'Back to ' + pageMeta(top.pageId).label;
|
||||
}
|
||||
function updateBackLabels() {
|
||||
var text = backLabelText();
|
||||
var labels = document.querySelectorAll('[data-vd-back-label]');
|
||||
for (var i = 0; i < labels.length; i++) labels[i].textContent = text;
|
||||
}
|
||||
function backFallback() {
|
||||
// No browser history to pop (deep link) — go to the recorded first origin.
|
||||
var dest = (_backStack[0] && _backStack[0].type === 'page') ? _backStack[0].pageId : 'video-library';
|
||||
_backStack = [];
|
||||
navigate(dest);
|
||||
updateBackLabels();
|
||||
}
|
||||
|
||||
function onPopState() {
|
||||
var r = parseDetailPath(window.location.pathname);
|
||||
if (r) { restoreDetail(r); return; }
|
||||
// Left the detail URL — if we're still on the video side showing a detail,
|
||||
// fall back to the library (Back out of the detail).
|
||||
if (r) {
|
||||
// Synced to the layer depth stamped on the history entry (handles both
|
||||
// our back button and the browser's Back).
|
||||
var st = window.history.state;
|
||||
var layer = (st && st.videoDetail && st.videoDetail.layer) || _backStack.length;
|
||||
if (_backStack.length > layer) _backStack.length = layer;
|
||||
restoreDetail(r);
|
||||
updateBackLabels();
|
||||
return;
|
||||
}
|
||||
// Left the detail URL — return to where the chain started, not always library.
|
||||
if (document.body.getAttribute('data-side') === 'video' &&
|
||||
DETAIL_PAGES[document.body.getAttribute('data-video-page')]) {
|
||||
navigate('video-library');
|
||||
var dest = (_backStack[0] && _backStack[0].type === 'page') ? _backStack[0].pageId : 'video-library';
|
||||
_backStack = [];
|
||||
navigate(dest);
|
||||
updateBackLabels();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -220,6 +274,7 @@
|
|||
(function (btn) {
|
||||
btn.addEventListener('click', function (e) {
|
||||
e.preventDefault();
|
||||
_backStack = []; // sidebar nav is a fresh entry point
|
||||
navigate(btn.getAttribute('data-video-page'));
|
||||
});
|
||||
})(navButtons[j]);
|
||||
|
|
@ -232,6 +287,7 @@
|
|||
(function (el) {
|
||||
el.addEventListener('click', function (e) {
|
||||
e.preventDefault();
|
||||
_backStack = []; // in-page jump is a fresh entry point
|
||||
navigate(el.getAttribute('data-video-goto'));
|
||||
});
|
||||
})(gotos[k]);
|
||||
|
|
@ -242,27 +298,35 @@
|
|||
// and push a real URL — unless we're restoring from the URL (_restore).
|
||||
document.addEventListener('soulsync:video-open-detail', function (e) {
|
||||
var d = e && e.detail; if (!d) return;
|
||||
// Capture the origin BEFORE navigating away from the current page.
|
||||
var origin = d._restore ? null : currentOrigin();
|
||||
if (d.kind === 'movie') navigate('video-movie-detail');
|
||||
else if (d.kind === 'show') navigate('video-show-detail');
|
||||
else if (d.kind === 'person') navigate('video-person-detail');
|
||||
else return;
|
||||
if (!d._restore) {
|
||||
_backStack.push(origin);
|
||||
var state = { videoDetail: { kind: d.kind, id: d.id, source: d.source || 'library',
|
||||
layer: _backStack.length } };
|
||||
var path = buildDetailPath(d.source, d.kind, d.id);
|
||||
if (window.location.pathname !== path) {
|
||||
history.pushState({ videoDetail: { kind: d.kind, id: d.id, source: d.source || 'library' } },
|
||||
'', path);
|
||||
}
|
||||
if (window.location.pathname !== path) history.pushState(state, '', path);
|
||||
else history.replaceState(state, '', path);
|
||||
}
|
||||
updateBackLabels();
|
||||
});
|
||||
|
||||
// The detail "← Library" back button is real Back (returns to wherever you
|
||||
// drilled in from); falls back to the library if there's no in-app history.
|
||||
// The detail back button is real browser Back (so it unwinds the whole
|
||||
// drill-in chain, layer by layer); only when there's no in-app history to
|
||||
// pop does it fall back to the recorded first origin.
|
||||
document.addEventListener('click', function (e) {
|
||||
var back = e.target.closest('[data-video-detail-back]');
|
||||
if (!back) return;
|
||||
e.preventDefault();
|
||||
if (history.state && history.state.videoDetail) history.back();
|
||||
else navigate('video-library');
|
||||
if (window.history.length > 1 && window.history.state && window.history.state.videoDetail) {
|
||||
window.history.back();
|
||||
} else {
|
||||
backFallback();
|
||||
}
|
||||
});
|
||||
|
||||
window.addEventListener('popstate', onPopState);
|
||||
|
|
|
|||
Loading…
Reference in a new issue