Каталог плагинов: полный локальный mirror в ./plugins (один directory-mount в wwwroot/plugins), доки и вычистка неиспользуемых модулей
- plugins/: скачаны все 30 плагинов store локально (sources.json - карта исходных URL);
патчи: showy.js (абсолютный хост подгрузки), pubtorr/td/torrentmanager - тела вместо лоадеров,
modss.js - local-patched копия (из каталога исключена, upstream мёртв)
- extensions.json: 32 позиции, все ссылки {localhost}/plugins/<файл>.js, удалены мёртвые
(Skaz TV x3, export.js, hotkeys/gold_theme/select_weapon/want/mult, MODS's, o.js, др.)
- compose: ./plugins:/lampac/wwwroot/plugins (один маунт вместо 30 file-mount)
- вычистка: удалены неиспользуемые mods (DatabaseEditor, ExternalBind, LogUserRequest-Lite,
Telemetry, Transcoding, WatchTogether) и дубль каталога modules/
- config/init.conf: секции Tracks/AdminPanel/Music, customPlugins без gst.js
- mods/Tracks: правки Controller.cs/plugin.js
- README.md / AGENT.md: mirror, кеш static, правила mount
This commit is contained in:
@@ -0,0 +1,365 @@
|
||||
(function () {
|
||||
"use strict";
|
||||
// ===== Logging Helper =====
|
||||
function log(message, data) {
|
||||
console.log("[Title Plugin]", message, data || "");
|
||||
}
|
||||
|
||||
function logError(message, error) {
|
||||
console.error("[Title Plugin]", message, error || "");
|
||||
}
|
||||
|
||||
// ===== Safe Property Access =====
|
||||
function safeGet(obj, path, defaultValue) {
|
||||
try {
|
||||
return path.split('.').reduce((acc, part) => acc && acc[part], obj) || defaultValue;
|
||||
} catch (e) {
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
|
||||
// ===== Version Check =====
|
||||
function checkCompatibility() {
|
||||
if (!window.Lampa) {
|
||||
logError("Lampa not found");
|
||||
return false;
|
||||
}
|
||||
if (!Lampa.Lang || !Lampa.Storage) {
|
||||
logError("Required APIs not available");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
// ===== Локалізація =====
|
||||
Lampa.Lang.add({
|
||||
title_plugin: {
|
||||
ru: "Title Plugin",
|
||||
en: "Title Plugin",
|
||||
uk: "Title Plugin",
|
||||
be: "Title Plugin",
|
||||
},
|
||||
|
||||
reset_to_default: {
|
||||
ru: "Сбросить по умолчанию",
|
||||
en: "Reset to Default",
|
||||
uk: "Скинути за замовчуванням",
|
||||
be: "Скінуць па змаўчанні",
|
||||
},
|
||||
|
||||
show_en: {
|
||||
ru: 'Показывать <img src="https://flagcdn.com/us.svg" style="width:1.15em;height:auto;vertical-align:middle;"> EN',
|
||||
en: 'Show <img src="https://flagcdn.com/us.svg" style="width:1.15em;height:auto;vertical-align:middle;"> EN',
|
||||
uk: 'Показувати <img src="https://flagcdn.com/us.svg" style="width:1.15em;height:auto;vertical-align:middle;"> EN',
|
||||
be: 'Паказваць <img src="https://flagcdn.com/us.svg" style="width:1.15em;height:auto;vertical-align:middle;"> EN',
|
||||
},
|
||||
|
||||
show_tl: {
|
||||
ru: "Показывать Romaji (transliteration)",
|
||||
en: "Show Romaji (transliteration)",
|
||||
uk: "Показувати Romaji (transliteration)",
|
||||
be: "Паказваць Romaji (transliteration)",
|
||||
},
|
||||
|
||||
show_uk: {
|
||||
ru: 'Показывать <img src="https://flagcdn.com/ua.svg" style="width:1.15em;height:auto;vertical-align:middle;"> UA',
|
||||
en: 'Show <img src="https://flagcdn.com/ua.svg" style="width:1.15em;height:auto;vertical-align:middle;"> UA',
|
||||
uk: 'Показувати <img src="https://flagcdn.com/ua.svg" style="width:1.15em;height:auto;vertical-align:middle;"> UA',
|
||||
be: 'Паказваць <img src="https://flagcdn.com/ua.svg" style="width:1.15em;height:auto;vertical-align:middle;"> UA',
|
||||
},
|
||||
|
||||
show_be: {
|
||||
ru: 'Показывать <img src="https://flagcdn.com/by.svg" style="width:1.15em;height:auto;vertical-align:middle;"> BE',
|
||||
en: 'Show <img src="https://flagcdn.com/by.svg" style="width:1.15em;height:auto;vertical-align:middle;"> BE',
|
||||
uk: 'Показувати <img src="https://flagcdn.com/by.svg" style="width:1.15em;height:auto;vertical-align:middle;"> BE',
|
||||
be: 'Паказваць <img src="https://flagcdn.com/by.svg" style="width:1.15em;height:auto;vertical-align:middle;"> BE',
|
||||
},
|
||||
show_ru: {
|
||||
ru: 'Показывать <img src="https://flagcdn.com/ru.svg" style="width:1.15em;height:auto;vertical-align:middle;"> RU',
|
||||
en: 'Show <img src="https://flagcdn.com/ru.svg" style="width:1.15em;height:auto;vertical-align:middle;"> RU',
|
||||
uk: 'Показувати <img src="https://flagcdn.com/ru.svg" style="width:1.15em;height:auto;vertical-align:middle;"> RU',
|
||||
be: 'Паказваць <img src="https://flagcdn.com/ru.svg" style="width:1.15em;height:auto;vertical-align:middle;"> RU',
|
||||
},
|
||||
});
|
||||
|
||||
const LANGS = ["en", "tl", "uk", "be", "ru"];
|
||||
const STORAGE_ORDER_KEY = "title_plugin_order";
|
||||
const STORAGE_HIDDEN_KEY = "title_plugin_hidden";
|
||||
|
||||
function startPlugin() {
|
||||
// ===== Settings button =====
|
||||
Lampa.SettingsApi.addParam({
|
||||
component: "interface",
|
||||
param: { type: "button" },
|
||||
field: {
|
||||
name: Lampa.Lang.translate("title_plugin"),
|
||||
description: "Title Plugin settings",
|
||||
},
|
||||
onChange: openEditor,
|
||||
});
|
||||
|
||||
// ===== Title display logic =====
|
||||
const CACHE_TTL = 30 * 24 * 60 * 60 * 1000;
|
||||
let titleCache = Lampa.Storage.get("title_cache") || {};
|
||||
|
||||
function showTitles(card) {
|
||||
try {
|
||||
var orig = card.original_title || card.original_name;
|
||||
var altTitles = card.alternative_titles || {};
|
||||
var alt = altTitles.titles || altTitles.results || [];
|
||||
|
||||
function countryFlag(code) {
|
||||
if (!code) return '';
|
||||
return ' <img src="https://flagcdn.com/' + code.toLowerCase() + '.svg"'
|
||||
+ ' style="width:1.15em;height:auto;vertical-align:inherit;" alt="' + code.toUpperCase() + '">';
|
||||
}
|
||||
|
||||
var TRANSLIT_TYPES = ['Transliteration', 'romaji', 'Romanization', 'Latynization',
|
||||
'pinyin', 'kana', 'romaji_japanese', 'romaji_korean', 'romaji_chinese', 'latinization'];
|
||||
|
||||
var translitObj = alt.find(function (t) { return TRANSLIT_TYPES.indexOf(t.type) !== -1; });
|
||||
var translit = (translitObj && (translitObj.title || (translitObj.data && (translitObj.data.title || translitObj.data.name)))) || '';
|
||||
|
||||
var ru = '', en = '', uk = '', be = '';
|
||||
var now = Date.now();
|
||||
var cache = titleCache[card.id];
|
||||
|
||||
if (cache && now - cache.timestamp < CACHE_TTL) {
|
||||
en = cache.en || '';
|
||||
uk = cache.uk || '';
|
||||
be = cache.be || '';
|
||||
ru = cache.ru || '';
|
||||
translit = translit || cache.tl || '';
|
||||
console.log('[TitlePlugin] cache hit for id:', card.id);
|
||||
}
|
||||
|
||||
function findLang(list, codes) {
|
||||
var t = list.find(function (t) { return codes.indexOf(t.iso_3166_1) !== -1 || codes.indexOf(t.iso_639_1) !== -1; });
|
||||
return t && t.data && (t.data.title || t.data.name) || '';
|
||||
}
|
||||
|
||||
function applyAndRender(tr) {
|
||||
var translitData = tr.find(function (t) { return ['Transliteration', 'romaji'].indexOf(t.type) !== -1; });
|
||||
if (translitData) translit = (translitData.title || (translitData.data && (translitData.data.title || translitData.data.name))) || translit;
|
||||
|
||||
en = en || findLang(tr, ['US', 'en']);
|
||||
uk = uk || findLang(tr, ['UA', 'uk']);
|
||||
be = be || findLang(tr, ['BY', 'be']);
|
||||
ru = ru || findLang(tr, ['RU', 'ru']);
|
||||
|
||||
// fallback из alternative_titles
|
||||
en = en || (alt.find(function (t) { return t.iso_3166_1 === 'US'; }) || {}).title || '';
|
||||
uk = uk || (alt.find(function (t) { return t.iso_3166_1 === 'UA'; }) || {}).title || '';
|
||||
be = be || (alt.find(function (t) { return t.iso_3166_1 === 'BY'; }) || {}).title || '';
|
||||
ru = ru || (alt.find(function (t) { return t.iso_3166_1 === 'RU'; }) || {}).title || '';
|
||||
|
||||
titleCache[card.id] = { ru: ru, en: en, tl: translit, uk: uk, be: be, timestamp: now };
|
||||
try { Lampa.Storage.set('title_cache', titleCache); } catch (e) { console.error('[TitlePlugin] storage error:', e); }
|
||||
|
||||
renderTitles();
|
||||
}
|
||||
|
||||
function renderTitles() {
|
||||
var active = Lampa.Activity.active();
|
||||
if (!active || !active.activity) {
|
||||
console.warn('[TitlePlugin] no active activity');
|
||||
return;
|
||||
}
|
||||
var render = active.activity.render();
|
||||
if (!render) { console.warn('[TitlePlugin] render is null'); return; }
|
||||
|
||||
$('.original_title', render).remove();
|
||||
|
||||
var showOrder = Lampa.Storage.get(STORAGE_ORDER_KEY, LANGS.slice());
|
||||
var hiddenLangs = Lampa.Storage.get(STORAGE_HIDDEN_KEY, []);
|
||||
var originCountry = (card.origin_country || [])[0] || '';
|
||||
var lines = ['<div style="font-size:1.25em;">' + orig + ' ' + countryFlag(originCountry) + '</div>'];
|
||||
var langFlags = { ru: 'RU', en: 'US', uk: 'UA', be: 'BY' };
|
||||
var langVals = { en: en, uk: uk, be: be, ru: ru };
|
||||
|
||||
showOrder.forEach(function (lang) {
|
||||
if (hiddenLangs.indexOf(lang) !== -1) return;
|
||||
var val = lang === 'tl' ? translit : langVals[lang];
|
||||
if (val) lines.push('<div style="font-size:1.25em;">' + val + ' ' + countryFlag(langFlags[lang]) + '</div>');
|
||||
});
|
||||
|
||||
$('.full-start-new__title', render).after(
|
||||
'<div class="original_title" style="margin-bottom:7px;text-align:right;"><div>'
|
||||
+ lines.join('') + '</div></div>'
|
||||
);
|
||||
}
|
||||
|
||||
if (!ru || !en || !translit || !uk || !be) {
|
||||
var type = card.first_air_date ? 'tv' : 'movie';
|
||||
var tmdbCacheKey = type + '_' + card.id;
|
||||
|
||||
// Спільний кеш трансляцій з іншими плагінами (наприклад rezkacomment.js)
|
||||
window.__tmdbTranslationsCache = window.__tmdbTranslationsCache || {};
|
||||
if (window.__tmdbTranslationsCache[tmdbCacheKey]) {
|
||||
console.log('[TitlePlugin] using shared translations cache for', tmdbCacheKey);
|
||||
applyAndRender(window.__tmdbTranslationsCache[tmdbCacheKey]);
|
||||
} else {
|
||||
console.log('[TitlePlugin] fetching translations for', type, card.id);
|
||||
Lampa.Api.sources.tmdb.get(
|
||||
type + '/' + card.id + '?append_to_response=translations',
|
||||
{},
|
||||
function (data) {
|
||||
var tr = (data.translations && data.translations.translations) || [];
|
||||
window.__tmdbTranslationsCache[tmdbCacheKey] = tr;
|
||||
applyAndRender(tr);
|
||||
},
|
||||
function (e) {
|
||||
console.error('[TitlePlugin] tmdb error:', e);
|
||||
applyAndRender([]);
|
||||
}
|
||||
);
|
||||
}
|
||||
} else {
|
||||
renderTitles();
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('[TitlePlugin] showTitles crash:', e.message, e.stack);
|
||||
}
|
||||
}
|
||||
|
||||
// ===== Listener =====
|
||||
if (!window.title_plugin) {
|
||||
window.title_plugin = true;
|
||||
Lampa.Listener.follow("full", (e) => {
|
||||
if (e.type !== "complite" || !e.data.movie) return;
|
||||
showTitles(e.data.movie);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ===== Editor modal =====
|
||||
function openEditor() {
|
||||
const order = Lampa.Storage.get(STORAGE_ORDER_KEY, LANGS.slice());
|
||||
const hidden = Lampa.Storage.get(STORAGE_HIDDEN_KEY, []);
|
||||
const list = $('<div class="menu-edit-list"></div>');
|
||||
const style = $(`
|
||||
<style>
|
||||
.menu-edit-list__item { display:flex; align-items:center; justify-content:space-between; margin:0.3em 0; }
|
||||
.menu-edit-list__title { flex:1; font-size:1.1em; padding-left:0.5em; }
|
||||
.menu-edit-list__move { width:1.8em; height:1.8em; display:flex; align-items:center; justify-content:center; cursor:pointer; font-weight:bold; user-select:none; }
|
||||
.menu-edit-list__toggle { width:1.8em; height:1.8em; display:flex; align-items:center; justify-content:center; cursor:pointer; margin-left:0.5em; border:2px solid rgba(255,255,255,0.5); border-radius:3px; }
|
||||
.menu-edit-list__toggle .dot { width:1em; height:1em; }
|
||||
.folder-reset-button { background: rgba(200,100,100,0.3); margin-top:1em; border-radius:0.3em; padding:0.5em; text-align:center; cursor:pointer; }
|
||||
</style>
|
||||
`);
|
||||
$("body").append(style);
|
||||
|
||||
order.forEach((lang) => {
|
||||
const title = Lampa.Lang.translate("show_" + lang);
|
||||
const isHidden = hidden.includes(lang);
|
||||
const item = $(`
|
||||
<div class="menu-edit-list__item">
|
||||
<div class="menu-edit-list__title">${title}</div>
|
||||
<div class="menu-edit-list__move move-up selector">▲</div>
|
||||
<div class="menu-edit-list__move move-down selector">▼</div>
|
||||
<div class="menu-edit-list__toggle selector">
|
||||
<svg width="26" height="26" viewBox="0 0 26 26" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="1.89111" y="1.78369" width="21.793" height="21.793" rx="3.5" stroke="currentColor" stroke-width="3"/>
|
||||
<path d="M7.44873 12.9658L10.8179 16.3349L18.1269 9.02588" stroke="currentColor" stroke-width="3" class="dot" opacity="${isHidden ? 0 : 1
|
||||
}" stroke-linecap="round"/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>`);
|
||||
|
||||
// ===== Toggle hide/show =====
|
||||
item.find(".menu-edit-list__toggle").on("hover:enter", function () {
|
||||
const idx = hidden.indexOf(lang);
|
||||
if (idx !== -1) {
|
||||
hidden.splice(idx, 1);
|
||||
item.find(".dot").attr("opacity", "1");
|
||||
} else {
|
||||
hidden.push(lang);
|
||||
item.find(".dot").attr("opacity", "0");
|
||||
}
|
||||
Lampa.Storage.set(STORAGE_HIDDEN_KEY, hidden);
|
||||
});
|
||||
|
||||
// ===== Move up/down =====
|
||||
item.find(".move-up").on("hover:enter", function () {
|
||||
const prev = item.prev();
|
||||
if (prev.length) {
|
||||
item.insertBefore(prev);
|
||||
updateOrder();
|
||||
}
|
||||
});
|
||||
item.find(".move-down").on("hover:enter", function () {
|
||||
const next = item.next();
|
||||
if (next.length) {
|
||||
item.insertAfter(next);
|
||||
updateOrder();
|
||||
}
|
||||
});
|
||||
|
||||
list.append(item);
|
||||
});
|
||||
|
||||
const resetBtn = $(
|
||||
'<div class="selector folder-reset-button" style="margin-top:1em;padding:1em;text-align:center;">' +
|
||||
Lampa.Lang.translate("reset_to_default") +
|
||||
"</div>"
|
||||
);
|
||||
resetBtn.on("hover:enter", function () {
|
||||
Lampa.Storage.set(STORAGE_ORDER_KEY, LANGS.slice());
|
||||
Lampa.Storage.set(STORAGE_HIDDEN_KEY, []);
|
||||
Lampa.Modal.close();
|
||||
Lampa.Controller.toggle("settings");
|
||||
});
|
||||
list.append(resetBtn);
|
||||
|
||||
function updateOrder() {
|
||||
const newOrder = [];
|
||||
list.find(".menu-edit-list__item").each(function () {
|
||||
const title = $(this).find(".menu-edit-list__title").text();
|
||||
const key = LANGS.find(
|
||||
(l) => Lampa.Lang.translate("show_" + l) === title
|
||||
);
|
||||
if (key) newOrder.push(key);
|
||||
});
|
||||
Lampa.Storage.set(STORAGE_ORDER_KEY, newOrder);
|
||||
}
|
||||
|
||||
Lampa.Modal.open({
|
||||
title: Lampa.Lang.translate("title_plugin"),
|
||||
html: list,
|
||||
size: "small",
|
||||
scroll_to_center: true,
|
||||
onBack: function () {
|
||||
Lampa.Modal.close();
|
||||
Lampa.Controller.toggle("settings");
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// ===== Init plugin =====
|
||||
if (window.appready) {
|
||||
if (typeof Lampa !== 'undefined' &&
|
||||
Lampa.SettingsApi &&
|
||||
Lampa.Api &&
|
||||
Lampa.Api.sources &&
|
||||
Lampa.Api.sources.tmdb &&
|
||||
typeof $ !== 'undefined') {
|
||||
startPlugin();
|
||||
} else {
|
||||
console.error('Title Plugin: Required Lampa APIs not available');
|
||||
}
|
||||
} else {
|
||||
Lampa.Listener.follow("app", (e) => {
|
||||
if (e.type === "ready") {
|
||||
if (typeof Lampa !== 'undefined' &&
|
||||
Lampa.SettingsApi &&
|
||||
Lampa.Api &&
|
||||
Lampa.Api.sources &&
|
||||
Lampa.Api.sources.tmdb &&
|
||||
typeof $ !== 'undefined') {
|
||||
startPlugin();
|
||||
} else {
|
||||
console.error('Title Plugin: Required Lampa APIs not available');
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
})();
|
||||
Reference in New Issue
Block a user