Files
mymovies/plugin.js
T

393 lines
17 KiB
JavaScript

(function () {
'use strict';
var PLUGIN = { name: 'mymovies', title: 'My Movies', menu_action: 'mymovies' };
var DEFAULT_BASE = 'https://movies-lmp-18.dynv6.net/';
function base() {
var b = Lampa.Storage.get(PLUGIN.name + '_base', DEFAULT_BASE);
return b.replace(/\/+$/, '') + '/';
}
function resolve(p) {
if (!p) return '';
if (/^https?:\/\//.test(p)) return p;
if (/^magnet:/.test(p)) return p;
return base() + p.replace(/^\/+/, '');
}
function esc(s) {
return String(s == null ? '' : s)
.replace(/&/g, '&amp;').replace(/</g, '&lt;')
.replace(/>/g, '&gt;').replace(/"/g, '&quot;');
}
// Load catalog via Lampa native network layer (TV-safe, fetch fails on some devices)
function loadCatalog(cb) {
var url = base() + 'movies/catalog.json';
if (Lampa.Reguest && Lampa.Reguest.get) {
Lampa.Reguest.get(url, function (html) {
try {
cb(null, JSON.parse(html));
} catch (e) {
cb(e, null);
}
}, function (err) {
cb(err || new Error('request failed'), null);
});
} else {
fetch(url).then(function (r) { return r.json(); })
.then(function (d) { cb(null, d); })
.catch(function (e) { cb(e, null); });
}
}
// Plugin styles.
// NOTE: Lampa.Template.add() only stores the string in memory — it does NOT
// inject <style> into the page. So we inject CSS manually into <head>.
// Movie cards use NATIVE Lampa markup/styles (.card / .card__img / .card__title),
// so they look identical to every other Lampa section.
function injectStyles() {
if (window.__mymovies_css_injected) return;
window.__mymovies_css_injected = true;
// Inject CSS into <head> so it actually applies.
// Only layout wrappers + detail screen; the movie cards use NATIVE Lampa styles (.card).
var css =
'.mymovies{padding:2.2em 3.4em;overflow-y:auto;height:100%;box-sizing:border-box}' +
'.movies-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(170px,1fr));gap:1.4em}' +
'.mymovies-detail{padding:3em;max-width:980px;box-sizing:border-box}' +
'.detail-head{display:flex;gap:2.2em;flex-wrap:wrap;align-items:flex-start}' +
'.detail-poster{width:240px;max-width:40vw;border-radius:12px;display:block;background:#1b1b1f}' +
'.detail-info{flex:1;min-width:260px}' +
'.detail-info h1{margin:0 0 .35em;font-size:2em;line-height:1.15}' +
'.detail-orig{opacity:.6;font-size:1.1em;margin-bottom:.6em}' +
'.detail-meta{display:flex;flex-wrap:wrap;gap:.6em;margin:.4em 0 1em}' +
'.detail-meta>span{padding:.25em .7em;border-radius:6px;background:rgba(255,255,255,.08);font-size:.95em}' +
'.detail-desc{opacity:.85;line-height:1.5;font-size:1.05em;margin-top:.6em;max-width:60ch}' +
'.detail-actions{display:flex;gap:1em;margin-top:1.8em;flex-wrap:wrap}' +
'.detail-actions .btn{padding:.7em 1.8em;border-radius:8px;background:rgba(255,255,255,.1);font-size:1.1em;cursor:pointer}' +
'.detail-actions .btn.focus,.detail-actions .btn:hover{background:var(--accent-color,#3a7afe);color:#fff}' +
'.mymovies-empty{padding:2em;opacity:.6;font-size:1.2em}';
$('<style id="mymovies-style">').text(css).appendTo('head');
}
// ---------- Component: movie list ----------
// Framework contract (ActivitySlide.create):
// component.create(body) -> store the activity body
// body.append(component.render(true)) -> render() returns a DOM node
function MyMovies() {
var comp = this;
var body;
var grid;
this.create = function (b) {
body = b;
};
this.destroy = function () {
};
// Wire up keyboard/remote navigation (pattern from Shikimori plugin).
this.start = function () {
var apply = function () {
var g = $(comp.activity && comp.activity.object ? '.activity--' + (comp.activity.name || 'mymovies') : '.mymovies').find('.movies-grid');
if (!g.length) g = $('.mymovies .movies-grid');
var cards = g.find('.card.selector');
if (!cards.length) return;
Lampa.Controller.add('content', {
toggle: function () {
Lampa.Controller.collectionSet(g);
Lampa.Controller.collectionFocus(cards[0], g);
},
right: function () { Navigator.move('right'); },
left: function () {
if (Navigator.canmove('left')) Navigator.move('left');
else Lampa.Controller.toggle('menu');
},
up: function () { Navigator.move('up'); },
down: function () { Navigator.move('down'); },
enter: function () {
var el = g.find('.card.selector.focus')[0] || cards[0];
if (!el) return;
var id = $(el).attr('data-id');
var m = comp._movies ? comp._movies.find(function (x) { return x.id === id; }) : null;
if (m) openMovie(m);
},
back: function () { Lampa.Activity.backward(); },
gone: function () { Lampa.Controller.clear('content'); }
});
Lampa.Controller.toggle('content');
Lampa.Controller.collectionSet(g);
Lampa.Controller.collectionFocus(cards[0], g);
};
// cards may not be rendered yet (async catalog load); retry shortly
apply();
setTimeout(apply, 300);
setTimeout(apply, 800);
};
this.render = function () {
injectStyles();
var html = document.createElement('div');
html.className = 'mymovies';
grid = document.createElement('div');
grid.className = 'movies-grid';
html.appendChild(grid);
if (comp.activity) comp.activity.loader(true);
loadCatalog(function (err, data) {
if (comp.activity) comp.activity.loader(false);
if (err) {
console.error('[mymovies] catalog error', err);
grid.innerHTML = '<div class="mymovies-empty">Catalog load error</div>';
return;
}
var movies = (data && data.movies) || [];
movies = sortMovies(movies);
comp._movies = movies;
window.__mymovies_catalog = movies;
console.log('[mymovies] loaded', movies.length, 'films');
renderMovies($(grid), movies);
// re-apply navigation once cards exist (activity.start may have run before load)
comp.start();
});
return html;
};
}
Lampa.Component.add(PLUGIN.name, MyMovies);
// Keep user-defined display order (Supergirl, Interstellar, Inception, Matrix)
var DESIRED_ORDER = ['supergirl-2026', 'interstellar', 'inception', 'matrix'];
function sortMovies(movies) {
return movies.slice().sort(function (a, b) {
var ia = DESIRED_ORDER.indexOf(a.id);
var ib = DESIRED_ORDER.indexOf(b.id);
if (ia === -1) ia = 999;
if (ib === -1) ib = 999;
return ia - ib;
});
}
function renderMovies(grid, movies) {
grid.empty();
if (!movies.length) {
grid.append('<div class="mymovies-empty">Catalog is empty</div>');
return;
}
movies.forEach(function (m) {
// Native Lampa card markup so it looks exactly like every other section.
// card--loaded makes Lampa show the poster (native .card__img is hidden until then).
var card = $(
'<div class="card selector card--loaded" data-id="' + esc(m.id) + '">' +
'<div class="card__view">' +
'<img src="' + resolve(m.poster || m.cover) + '" class="card__img" />' +
'<div class="card__icons"><div class="card__icons-inner"></div></div>' +
'</div>' +
'<div class="card__title">' + esc(m.title || m.id) + '</div>' +
(m.year ? '<div class="card__age">' + esc(String(m.year)) + '</div>' : '') +
'</div>');
card.on('hover:enter', function () { openMovie(m); });
grid.append(card);
});
}
// ---------- Component: movie detail ----------
var selected = null;
function resolveMovie(m) {
// Lampa sometimes wraps the pushed object; normalize to the real film.
if (m && m.object && (m.object.id || m.object.poster || m.object.title)) return m.object;
return m;
}
function MyMoviesDetail() {
var comp = this;
var body;
this.create = function (b) {
body = b;
};
this.destroy = function () {
};
this.start = function () {
var apply = function () {
var btns = $('.mymovies-detail .detail-actions .btn.selector');
if (!btns.length) return;
Lampa.Controller.add('mymovies_detail', {
toggle: function () {
Lampa.Controller.collectionSet($('.mymovies-detail .detail-actions'));
Lampa.Controller.collectionFocus(btns[0], $('.mymovies-detail .detail-actions'));
},
back: function () {
Lampa.Activity.backward();
},
right: function () { Navigator.move('right'); },
left: function () { Navigator.move('left'); },
enter: function () {
var el = $('.mymovies-detail .detail-actions .btn.selector.focus')[0] || btns[0];
if (!el) return;
if (el.getAttribute('data-act') === 'download') download(m);
else play(m);
},
gone: function () { Lampa.Controller.clear('mymovies_detail'); }
});
Lampa.Controller.toggle('mymovies_detail');
};
apply();
setTimeout(apply, 300);
};
this.render = function () {
var raw = (comp.activity && comp.activity.object) || selected;
var m = resolveMovie(raw);
// Fallback: lookup full record from the cached catalog by id.
if ((!m || !m.poster) && window.__mymovies_catalog) {
var id = m && m.id;
var found = window.__mymovies_catalog.filter(function (x) { return x.id === id; })[0];
if (found) m = found;
}
if (!m || !m.title) { Lampa.Activity.back(); return document.createElement('div'); }
injectStyles();
var html = document.createElement('div');
html.className = 'mymovies-detail';
var head = document.createElement('div');
head.className = 'detail-head';
var poster = document.createElement('img');
poster.className = 'detail-poster';
poster.src = resolve(m.poster || m.cover);
poster.alt = m.title || '';
head.appendChild(poster);
var info = document.createElement('div');
info.className = 'detail-info';
var h1 = document.createElement('h1');
h1.textContent = m.title;
info.appendChild(h1);
if (m.original_title) { var d = document.createElement('div'); d.className = 'detail-orig'; d.textContent = m.original_title; info.appendChild(d); }
var meta = document.createElement('div');
meta.className = 'detail-meta';
if (m.year) { var s = document.createElement('span'); s.textContent = m.year; meta.appendChild(s); }
if (m.genre && m.genre.length) { var s = document.createElement('span'); s.textContent = m.genre.join(', '); meta.appendChild(s); }
if (m.rating) { var s = document.createElement('span'); s.textContent = '★ ' + m.rating; meta.appendChild(s); }
if (meta.children.length) info.appendChild(meta);
if (m.description) { var d = document.createElement('div'); d.className = 'detail-desc'; d.textContent = m.description; info.appendChild(d); }
head.appendChild(info);
html.appendChild(head);
var actions = document.createElement('div');
actions.className = 'detail-actions';
var btnWatch = document.createElement('div');
btnWatch.className = 'btn selector';
btnWatch.setAttribute('data-act', 'play');
btnWatch.textContent = 'Watch';
actions.appendChild(btnWatch);
if (m.torrent || m.magnet) {
var btnDl = document.createElement('div');
btnDl.className = 'btn selector';
btnDl.setAttribute('data-act', 'download');
btnDl.textContent = 'Download';
actions.appendChild(btnDl);
$(btnDl).on('hover:enter', function () { download(m); });
}
html.appendChild(actions);
$(btnWatch).on('hover:enter', function () { play(m); });
return html;
};
}
Lampa.Component.add(PLUGIN.name + '_detail', MyMoviesDetail);
function openMovie(m) {
selected = m;
Lampa.Activity.push({
url: '',
component: PLUGIN.name + '_detail',
title: m.title,
page: m.title,
object: m
});
}
function play(m) {
var url = m.magnet ? m.magnet : resolve(m.torrent);
Lampa.Player.play({
url: url,
title: m.title,
img: resolve(m.poster || m.cover),
season: 0,
episode: 0
});
}
function download(m) {
var url = m.magnet ? m.magnet : resolve(m.torrent);
if (!url) return;
// Open the torrent/magnet in the system (player or download manager)
if (Lampa.Player && Lampa.Player.play) {
Lampa.Player.play({ url: url, title: m.title, img: resolve(m.poster || m.cover), season: 0, episode: 0 });
} else {
window.open(url, '_blank');
}
}
// ---------- Menu item ----------
// Lampa builds the main menu on the 'menu:start' event, e.body is the jQuery menu root.
// Remove ALL old copies from the DOM first, then add exactly one button.
function makeButton() {
var button = $(
'<li class="menu__item selector" data-action="' + PLUGIN.menu_action + '">' +
'<div class="menu__ico">' +
'<svg class="sprite-folder"><use xlink:href="#sprite-folder"></use></svg>' +
'</div>' +
'<div class="menu__text">' + PLUGIN.title + '</div>' +
'</li>'
);
button.on('hover:enter', function () {
Lampa.Activity.push({
url: '',
component: PLUGIN.name,
title: PLUGIN.title,
page: PLUGIN.title
});
});
return button;
}
var menuSubscribed = false;
function addMenuButton() {
// remove ALL copies everywhere in DOM, then add exactly one
$('[data-action="' + PLUGIN.menu_action + '"]').remove();
$('.menu .menu__list').eq(0).append(makeButton());
if (Lampa.Selector) Lampa.Selector.update();
}
function start() {
if (menuSubscribed) return;
menuSubscribed = true;
Lampa.Listener.follow('menu', function (e) {
if (e.type === 'start' && e.body) {
var list = e.body.find('.menu__list');
if (!list.length) return;
// guard against duplicate copies in this menu body
if (list.find('[data-action="' + PLUGIN.menu_action + '"]').length) return;
addMenuButton();
}
});
// fallback: re-check shortly after load (covers late menu builds)
setTimeout(function () {
if ($('.menu .menu__list').length && !$('[data-action="' + PLUGIN.menu_action + '"]').length) {
addMenuButton();
}
}, 3000);
}
if (window.Lampa && Lampa.Listener && Lampa.Activity) {
start();
} else {
document.addEventListener('lampa', start);
}
})();