Merge pull request #1290 from skoruppa/fixes-2
Collection translations, TMDB enrichment for pause overlay & collections, CW fixes
This commit is contained in:
commit
664c98f84c
19 changed files with 768 additions and 178 deletions
7
app/proguard-rules.pro
vendored
7
app/proguard-rules.pro
vendored
|
|
@ -100,3 +100,10 @@
|
|||
# Native code reflects into multiple classes/methods under is.xyz.mpv,
|
||||
# so keep the whole package to avoid JNI lookup crashes after R8.
|
||||
-keep class is.xyz.mpv.** { *; }
|
||||
|
||||
# ── Missing class stubs (referenced by cloudstream3 / jsoup / newpipe) ────────
|
||||
-dontwarn org.mozilla.javascript.**
|
||||
-dontwarn com.google.re2j.**
|
||||
-dontwarn javax.script.**
|
||||
-dontwarn okhttp3.internal.sse.**
|
||||
-dontwarn org.jsoup.helper.Re2jRegex
|
||||
|
|
|
|||
|
|
@ -871,9 +871,9 @@ object AddonWebPage {
|
|||
</div>
|
||||
|
||||
<div class="tabs">
|
||||
<button class="tab active" type="button" onclick="switchTab('addons')">Addons</button>
|
||||
<button class="tab" type="button" onclick="switchTab('catalogs')">Home Layout</button>
|
||||
<button class="tab" type="button" onclick="switchTab('collections')">Collections</button>
|
||||
<button class="tab active" type="button" onclick="switchTab('addons')">${context.getString(R.string.web_tab_addons)}</button>
|
||||
<button class="tab" type="button" onclick="switchTab('catalogs')">${context.getString(R.string.web_tab_home_layout)}</button>
|
||||
<button class="tab" type="button" onclick="switchTab('collections')">${context.getString(R.string.web_tab_collections)}</button>
|
||||
</div>
|
||||
|
||||
<div class="tab-content active" id="tab-addons">
|
||||
|
|
@ -895,8 +895,8 @@ object AddonWebPage {
|
|||
<div class="section-block">
|
||||
<div class="section-label">${context.getString(R.string.web_home_catalogs)}</div>
|
||||
<div class="add-section" style="display:flex;gap:0.5rem">
|
||||
<button class="btn" onclick="enableAllCatalogs()" style="flex:1">Enable All</button>
|
||||
<button class="btn" onclick="disableAllCatalogs()" style="flex:1">Disable All</button>
|
||||
<button class="btn" onclick="enableAllCatalogs()" style="flex:1">${context.getString(R.string.web_btn_enable_all)}</button>
|
||||
<button class="btn" onclick="disableAllCatalogs()" style="flex:1">${context.getString(R.string.web_btn_disable_all)}</button>
|
||||
</div>
|
||||
<ul class="addon-list" id="catalogList"></ul>
|
||||
<div class="empty-state" id="catalogEmptyState">${context.getString(R.string.web_no_catalogs)}</div>
|
||||
|
|
@ -905,28 +905,28 @@ object AddonWebPage {
|
|||
|
||||
<div class="tab-content" id="tab-collections">
|
||||
<div class="section-block">
|
||||
<div class="section-label">Collections</div>
|
||||
<div class="section-label">${context.getString(R.string.web_tab_collections)}</div>
|
||||
<div class="add-section" style="display:flex;gap:0.5rem">
|
||||
<button class="btn" onclick="enableAllCollections()" style="flex:1">Show All</button>
|
||||
<button class="btn" onclick="disableAllCollections()" style="flex:1">Hide All</button>
|
||||
<button class="btn" onclick="enableAllCollections()" style="flex:1">${context.getString(R.string.web_btn_show_all)}</button>
|
||||
<button class="btn" onclick="disableAllCollections()" style="flex:1">${context.getString(R.string.web_btn_hide_all)}</button>
|
||||
</div>
|
||||
<div class="add-section" style="display:flex;gap:0.5rem">
|
||||
<button class="btn" onclick="addCollection()" style="flex:1">+ New Collection</button>
|
||||
<button class="btn" onclick="exportCollections()" style="flex:1">Export</button>
|
||||
<button class="btn" onclick="showImportModal()" style="flex:1">Import</button>
|
||||
<button class="btn" onclick="addCollection()" style="flex:1">${context.getString(R.string.web_btn_new_collection)}</button>
|
||||
<button class="btn" onclick="exportCollections()" style="flex:1">${context.getString(R.string.web_btn_export)}</button>
|
||||
<button class="btn" onclick="showImportModal()" style="flex:1">${context.getString(R.string.web_btn_import)}</button>
|
||||
</div>
|
||||
<div id="collectionsList"></div>
|
||||
<div class="empty-state" id="collectionsEmptyState">No collections yet</div>
|
||||
<div class="empty-state" id="collectionsEmptyState">${context.getString(R.string.web_no_collections)}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="import-overlay" id="importOverlay">
|
||||
<div class="import-modal">
|
||||
<div style="font-size:1.1rem;font-weight:700;margin-bottom:1rem">Import Collections</div>
|
||||
<div style="font-size:1.1rem;font-weight:700;margin-bottom:1rem">${context.getString(R.string.web_import_collections_title)}</div>
|
||||
<div style="display:flex;gap:0.5rem;margin-bottom:1rem">
|
||||
<button class="btn import-tab-btn active" onclick="switchImportTab('paste')">Paste</button>
|
||||
<button class="btn import-tab-btn" onclick="switchImportTab('file')">File</button>
|
||||
<button class="btn import-tab-btn" onclick="switchImportTab('url')">URL</button>
|
||||
<button class="btn import-tab-btn active" onclick="switchImportTab('paste')">${context.getString(R.string.web_import_tab_paste)}</button>
|
||||
<button class="btn import-tab-btn" onclick="switchImportTab('file')">${context.getString(R.string.web_import_tab_file)}</button>
|
||||
<button class="btn import-tab-btn" onclick="switchImportTab('url')">${context.getString(R.string.web_import_tab_url)}</button>
|
||||
</div>
|
||||
<div id="import-tab-paste" class="import-tab active">
|
||||
<textarea id="importJsonInput" placeholder="Paste collections JSON here..." style="width:100%;min-height:120px;background:rgba(255,255,255,0.05);border:1px solid rgba(255,255,255,0.12);border-radius:8px;padding:0.75rem;color:#fff;font-family:monospace;font-size:0.8rem;resize:vertical"></textarea>
|
||||
|
|
@ -973,6 +973,39 @@ var pollTimer = null;
|
|||
var pollStartTime = 0;
|
||||
var POLL_TIMEOUT = 120000;
|
||||
var POLL_INTERVAL = 1500;
|
||||
|
||||
var i18n = {
|
||||
newCollection: '${context.getString(R.string.collections_new).replace("'", "\\'")}',
|
||||
backdrop: '${context.getString(R.string.collections_editor_backdrop).replace("'", "\\'")}',
|
||||
pinAbove: '${context.getString(R.string.collections_editor_pin_above).replace("'", "\\'")}',
|
||||
focusGlow: '${context.getString(R.string.collections_editor_focus_glow).replace("'", "\\'")}',
|
||||
viewMode: '${context.getString(R.string.collections_editor_view_mode).replace("'", "\\'")}',
|
||||
tabs: '${context.getString(R.string.collections_editor_view_mode_tabs).replace("'", "\\'")}',
|
||||
rows: '${context.getString(R.string.collections_editor_view_mode_rows).replace("'", "\\'")}',
|
||||
followHome: '${context.getString(R.string.collections_editor_view_mode_follow).replace("'", "\\'")}',
|
||||
showAllTab: '${context.getString(R.string.collections_editor_show_all_tab).replace("'", "\\'")}',
|
||||
cover: '${context.getString(R.string.collections_editor_cover).replace("'", "\\'")}',
|
||||
coverNone: '${context.getString(R.string.collections_editor_cover_none).replace("'", "\\'")}',
|
||||
coverEmoji: '${context.getString(R.string.collections_editor_cover_emoji).replace("'", "\\'")}',
|
||||
coverImage: '${context.getString(R.string.collections_editor_cover_image_url).replace("'", "\\'")}',
|
||||
focusGif: '${context.getString(R.string.collections_editor_focus_gif).replace("'", "\\'")}',
|
||||
playGif: '${context.getString(R.string.collections_editor_play_gif).replace("'", "\\'")}',
|
||||
tileShape: '${context.getString(R.string.collections_editor_tile_shape).replace("'", "\\'")}',
|
||||
hideTitle: '${context.getString(R.string.collections_editor_hide_title).replace("'", "\\'")}',
|
||||
catalogs: '${context.getString(R.string.collections_editor_catalogs).replace("'", "\\'")}',
|
||||
addCatalog: '${context.getString(R.string.collections_editor_add_catalog).replace("'", "\\'")}',
|
||||
addFolder: '${context.getString(R.string.collections_editor_add_folder).replace("'", "\\'")}',
|
||||
folders: '${context.getString(R.string.collections_editor_folders).replace("'", "\\'")}',
|
||||
hidden: '${context.getString(R.string.web_badge_disabled).replace("'", "\\'")}',
|
||||
display: '${context.getString(R.string.collections_editor_display).replace("'", "\\'")}',
|
||||
shape: '${context.getString(R.string.collections_editor_tile_shape).replace("'", "\\'")}',
|
||||
shapePoster: '${context.getString(R.string.collections_editor_shape_poster).replace("'", "\\'")}',
|
||||
shapeWide: '${context.getString(R.string.collections_editor_shape_wide).replace("'", "\\'")}',
|
||||
shapeSquare: '${context.getString(R.string.collections_editor_shape_square).replace("'", "\\'")}',
|
||||
tapToPickEmoji: '${context.getString(R.string.collections_editor_cover_emoji).replace("'", "\\'")}',
|
||||
added: '${context.getString(R.string.web_btn_add).replace("'", "\\'")}',
|
||||
add: '+ ${context.getString(R.string.web_btn_add).replace("'", "\\'")}'
|
||||
};
|
||||
var connectionLost = false;
|
||||
var consecutiveErrors = 0;
|
||||
var activeTab = 'addons';
|
||||
|
|
@ -1247,7 +1280,7 @@ function renderCatalogs() {
|
|||
'</div>' +
|
||||
'<div class="catalog-info">' +
|
||||
'<div class="catalog-name">' + escapeHtml(formatCatalogTitle(catalog.catalogName, catalog.type)) +
|
||||
(isCollection ? '<span class="badge-collection">Collection</span>' : '') +
|
||||
(isCollection ? '<span class="badge-collection">${context.getString(R.string.web_badge_collection).replace("'", "\\'")}</span>' : '') +
|
||||
(catalog.isDisabled ? '<span class="badge-disabled">${context.getString(R.string.web_badge_disabled).replace("'", "\\'")}</span>' : '') +
|
||||
'</div>' +
|
||||
'<div class="catalog-meta">' + escapeHtml(catalog.addonName) + '</div>' +
|
||||
|
|
@ -1577,7 +1610,7 @@ function isCollectionDisabled(ci) {
|
|||
}
|
||||
|
||||
function addCollection() {
|
||||
collections.push({ id: generateId(), title: 'New Collection', backdropImageUrl: null, pinToTop: false, focusGlowEnabled: true, viewMode: 'TABBED_GRID', showAllTab: true, folders: [] });
|
||||
collections.push({ id: generateId(), title: i18n.newCollection, backdropImageUrl: null, pinToTop: false, focusGlowEnabled: true, viewMode: 'TABBED_GRID', showAllTab: true, folders: [] });
|
||||
expandedCollection = collections.length - 1;
|
||||
expandedFolder = null;
|
||||
renderCollections();
|
||||
|
|
@ -1782,7 +1815,7 @@ function renderCollections() {
|
|||
'<div class="collection-header collapse-header" onclick="toggleCollectionExpand(' + ci + ')">' +
|
||||
'<span class="collapse-arrow' + (isExpanded ? ' open' : '') + '">▶</span>' +
|
||||
'<input class="collection-title-input" value="' + escapeAttr(col.title) + '" onchange="updateCollectionTitle(' + ci + ',this.value);updateSaveButtonState()" onclick="event.stopPropagation()" placeholder="Collection name">' +
|
||||
(disabled ? '<span class="badge-collection-disabled">Hidden</span>' : '') +
|
||||
(disabled ? '<span class="badge-collection-disabled">' + i18n.hidden + '</span>' : '') +
|
||||
(errors.length > 0 ? '<span style="font-size:0.6rem;font-weight:700;color:rgba(255,180,60,0.9);background:rgba(255,180,60,0.12);padding:0.2rem 0.5rem;border-radius:100px;flex-shrink:0">' + errors.length + ' issue' + (errors.length > 1 ? 's' : '') + '</span>' : '') +
|
||||
'<div class="col-actions" onclick="event.stopPropagation()">' +
|
||||
'<button class="btn-order" onclick="moveCollection(' + ci + ',-1)"' + (ci === 0 ? ' disabled' : '') + '>' +
|
||||
|
|
@ -1802,7 +1835,7 @@ function renderCollections() {
|
|||
|
||||
if (!isExpanded) {
|
||||
card.innerHTML = headerHtml +
|
||||
'<div class="folder-summary">' + folderCount + ' folder' + (folderCount !== 1 ? 's' : '') + '</div>';
|
||||
'<div class="folder-summary">' + i18n.folders + ': ' + folderCount + '</div>';
|
||||
container.appendChild(card);
|
||||
return;
|
||||
}
|
||||
|
|
@ -1811,12 +1844,12 @@ function renderCollections() {
|
|||
var settingsHtml =
|
||||
'<div class="col-settings">' +
|
||||
'<div class="col-setting-row">' +
|
||||
'<span class="col-meta-label">Backdrop</span>' +
|
||||
'<span class="col-meta-label">' + i18n.backdrop + '</span>' +
|
||||
'<img id="col-backdrop-preview-' + ci + '" src="' + escapeAttr(col.backdropImageUrl || '') + '" style="' + (col.backdropImageUrl ? '' : 'display:none') + '" onerror="this.style.display=\'none\'">' +
|
||||
'<input type="url" placeholder="Image URL (optional)" value="' + escapeAttr(col.backdropImageUrl || '') + '" oninput="updateCollectionBackdrop(' + ci + ',this.value)">' +
|
||||
'</div>' +
|
||||
'<div class="col-setting-row">' +
|
||||
'<span class="toggle-label">Pin above catalogs</span>' +
|
||||
'<span class="toggle-label">' + i18n.pinAbove + '</span>' +
|
||||
'<label class="toggle-switch" onclick="event.stopPropagation()">' +
|
||||
'<input type="checkbox"' + (col.pinToTop ? ' checked' : '') + ' onchange="updateCollectionPinToTop(' + ci + ',this.checked)">' +
|
||||
'<span class="toggle-track"></span>' +
|
||||
|
|
@ -1824,7 +1857,7 @@ function renderCollections() {
|
|||
'</label>' +
|
||||
'</div>' +
|
||||
'<div class="col-setting-row">' +
|
||||
'<span class="toggle-label">Focus glow on cards</span>' +
|
||||
'<span class="toggle-label">' + i18n.focusGlow + '</span>' +
|
||||
'<label class="toggle-switch" onclick="event.stopPropagation()">' +
|
||||
'<input type="checkbox"' + (col.focusGlowEnabled !== false ? ' checked' : '') + ' onchange="updateCollectionFocusGlow(' + ci + ',this.checked)">' +
|
||||
'<span class="toggle-track"></span>' +
|
||||
|
|
@ -1832,16 +1865,16 @@ function renderCollections() {
|
|||
'</label>' +
|
||||
'</div>' +
|
||||
'<div class="col-setting-row">' +
|
||||
'<span class="col-meta-label">View Mode</span>' +
|
||||
'<span class="col-meta-label">' + i18n.viewMode + '</span>' +
|
||||
'<div class="cover-mode-picker">' +
|
||||
'<button class="cover-mode-btn' + ((col.viewMode === 'TABBED_GRID' || !col.viewMode) ? ' active' : '') + '" onclick="updateCollectionViewMode(' + ci + ',\'TABBED_GRID\')">Tabs</button>' +
|
||||
'<button class="cover-mode-btn' + (col.viewMode === 'ROWS' ? ' active' : '') + '" onclick="updateCollectionViewMode(' + ci + ',\'ROWS\')">Rows</button>' +
|
||||
'<button class="cover-mode-btn' + (col.viewMode === 'FOLLOW_LAYOUT' ? ' active' : '') + '" onclick="updateCollectionViewMode(' + ci + ',\'FOLLOW_LAYOUT\')">Follow Home</button>' +
|
||||
'<button class="cover-mode-btn' + ((col.viewMode === 'TABBED_GRID' || !col.viewMode) ? ' active' : '') + '" onclick="updateCollectionViewMode(' + ci + ',\'TABBED_GRID\')">' + i18n.tabs + '</button>' +
|
||||
'<button class="cover-mode-btn' + (col.viewMode === 'ROWS' ? ' active' : '') + '" onclick="updateCollectionViewMode(' + ci + ',\'ROWS\')">' + i18n.rows + '</button>' +
|
||||
'<button class="cover-mode-btn' + (col.viewMode === 'FOLLOW_LAYOUT' ? ' active' : '') + '" onclick="updateCollectionViewMode(' + ci + ',\'FOLLOW_LAYOUT\')">' + i18n.followHome + '</button>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
((col.viewMode === 'TABBED_GRID' || !col.viewMode) ?
|
||||
'<div class="col-setting-row">' +
|
||||
'<span class="toggle-label">Show "All" tab</span>' +
|
||||
'<span class="toggle-label">' + i18n.showAllTab + '</span>' +
|
||||
'<label class="toggle-switch" onclick="event.stopPropagation()">' +
|
||||
'<input type="checkbox"' + (col.showAllTab !== false ? ' checked' : '') + ' onchange="updateCollectionShowAllTab(' + ci + ',this.checked)">' +
|
||||
'<span class="toggle-track"></span>' +
|
||||
|
|
@ -1927,12 +1960,12 @@ function renderCollections() {
|
|||
(isFolderExpanded ?
|
||||
'<div class="folder-settings">' +
|
||||
'<div class="folder-settings-group">' +
|
||||
'<div class="folder-settings-group-label">Cover</div>' +
|
||||
'<div class="folder-settings-group-label">' + i18n.cover + '</div>' +
|
||||
'<div class="folder-setting-item">' +
|
||||
'<div class="cover-mode-picker">' +
|
||||
'<button class="cover-mode-btn' + (coverMode === 'none' ? ' active' : '') + '" onclick="setFolderCoverMode(' + ci + ',' + fi + ',\'none\')">None</button>' +
|
||||
'<button class="cover-mode-btn' + (coverMode === 'emoji' ? ' active' : '') + '" onclick="setFolderCoverMode(' + ci + ',' + fi + ',\'emoji\')">Emoji</button>' +
|
||||
'<button class="cover-mode-btn' + (coverMode === 'image' ? ' active' : '') + '" onclick="setFolderCoverMode(' + ci + ',' + fi + ',\'image\')">Image</button>' +
|
||||
'<button class="cover-mode-btn' + (coverMode === 'none' ? ' active' : '') + '" onclick="setFolderCoverMode(' + ci + ',' + fi + ',\'none\')">' + i18n.coverNone + '</button>' +
|
||||
'<button class="cover-mode-btn' + (coverMode === 'emoji' ? ' active' : '') + '" onclick="setFolderCoverMode(' + ci + ',' + fi + ',\'emoji\')">' + i18n.coverEmoji + '</button>' +
|
||||
'<button class="cover-mode-btn' + (coverMode === 'image' ? ' active' : '') + '" onclick="setFolderCoverMode(' + ci + ',' + fi + ',\'image\')">' + i18n.coverImage + '</button>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
(coverMode === 'emoji' ?
|
||||
|
|
@ -1940,7 +1973,7 @@ function renderCollections() {
|
|||
'<button class="emoji-picker-btn" onclick="toggleEmojiPicker(' + ci + ',' + fi + ')">' +
|
||||
(folder.coverEmoji ? escapeHtml(folder.coverEmoji) : '😀') +
|
||||
'</button>' +
|
||||
'<span style="font-size:0.78rem;color:rgba(255,255,255,0.3);flex:1">Tap to pick emoji</span>' +
|
||||
'<span style="font-size:0.78rem;color:rgba(255,255,255,0.3);flex:1">' + i18n.tapToPickEmoji + '</span>' +
|
||||
'</div>' +
|
||||
'<div id="emoji-grid-' + ci + '-' + fi + '" class="emoji-grid-wrap" style="margin:0 0.75rem 0.5rem">' +
|
||||
'<input class="emoji-grid-search" placeholder="Search emoji..." oninput="filterEmoji(' + ci + ',' + fi + ',this.value)">' +
|
||||
|
|
@ -1955,7 +1988,7 @@ function renderCollections() {
|
|||
'<input type="url" placeholder="Focused GIF URL (optional)" value="' + escapeAttr(folder.focusGifUrl || '') + '" oninput="updateFolderFocusGifUrl(' + ci + ',' + fi + ',this.value)">' +
|
||||
'</div>' +
|
||||
'<div class="folder-setting-item">' +
|
||||
'<span class="toggle-label">Play GIF on focus</span>' +
|
||||
'<span class="toggle-label">' + i18n.playGif + '</span>' +
|
||||
'<label class="toggle-switch">' +
|
||||
'<input type="checkbox"' + (folder.focusGifEnabled !== false ? ' checked' : '') + ' onchange="updateFolderFocusGifEnabled(' + ci + ',' + fi + ',this.checked)">' +
|
||||
'<span class="toggle-track"></span>' +
|
||||
|
|
@ -1964,17 +1997,17 @@ function renderCollections() {
|
|||
'</div>' +
|
||||
'</div>' +
|
||||
'<div class="folder-settings-group">' +
|
||||
'<div class="folder-settings-group-label">Display</div>' +
|
||||
'<div class="folder-settings-group-label">' + i18n.display + '</div>' +
|
||||
'<div class="folder-setting-item">' +
|
||||
'<span class="folder-setting-label">Shape</span>' +
|
||||
'<span class="folder-setting-label">' + i18n.shape + '</span>' +
|
||||
'<select onchange="updateFolderTileShape(' + ci + ',' + fi + ',this.value)">' +
|
||||
'<option value="POSTER"' + (folder.tileShape === 'POSTER' ? ' selected' : '') + '>Poster</option>' +
|
||||
'<option value="LANDSCAPE"' + (folder.tileShape === 'LANDSCAPE' ? ' selected' : '') + '>Landscape</option>' +
|
||||
'<option value="SQUARE"' + ((folder.tileShape === 'SQUARE' || !folder.tileShape) ? ' selected' : '') + '>Square</option>' +
|
||||
'<option value="POSTER"' + (folder.tileShape === 'POSTER' ? ' selected' : '') + '>' + i18n.shapePoster + '</option>' +
|
||||
'<option value="LANDSCAPE"' + (folder.tileShape === 'LANDSCAPE' ? ' selected' : '') + '>' + i18n.shapeWide + '</option>' +
|
||||
'<option value="SQUARE"' + ((folder.tileShape === 'SQUARE' || !folder.tileShape) ? ' selected' : '') + '>' + i18n.shapeSquare + '</option>' +
|
||||
'</select>' +
|
||||
'</div>' +
|
||||
'<div class="folder-setting-item">' +
|
||||
'<span class="toggle-label">Hide title</span>' +
|
||||
'<span class="toggle-label">' + i18n.hideTitle + '</span>' +
|
||||
'<label class="toggle-switch">' +
|
||||
'<input type="checkbox" id="ht-' + ci + '-' + fi + '"' + (folder.hideTitle ? ' checked' : '') + ' onchange="updateFolderHideTitle(' + ci + ',' + fi + ',this.checked)">' +
|
||||
'<span class="toggle-track"></span>' +
|
||||
|
|
@ -1983,7 +2016,7 @@ function renderCollections() {
|
|||
'</div>' +
|
||||
'</div>' +
|
||||
'<div class="folder-settings-group">' +
|
||||
'<div class="folder-settings-group-label">Active Sources</div>' +
|
||||
'<div class="folder-settings-group-label">' + i18n.catalogs + '</div>' +
|
||||
'<div style="padding:0.5rem 0.75rem">' +
|
||||
'<input class="source-search-input" placeholder="Filter active sources..." oninput="filterActiveSources(' + ci + ',' + fi + ',this.value)" id="active-src-search-' + ci + '-' + fi + '">' +
|
||||
'<div id="active-src-list-' + ci + '-' + fi + '" style="max-height:180px;overflow-y:auto;border:1px solid rgba(255,255,255,0.05);border-radius:8px;margin-top:0.25rem">' +
|
||||
|
|
@ -1993,7 +2026,7 @@ function renderCollections() {
|
|||
'</div>' +
|
||||
'</div>' +
|
||||
'<div class="folder-settings-group" style="margin-top:0.5rem">' +
|
||||
'<div class="folder-settings-group-label">Add Sources</div>' +
|
||||
'<div class="folder-settings-group-label">' + i18n.addCatalog + '</div>' +
|
||||
'<div style="padding:0.5rem 0.75rem">' +
|
||||
'<input class="source-search-input" placeholder="Search catalogs..." oninput="filterCatalogSources(' + ci + ',' + fi + ',this.value)" id="src-search-' + ci + '-' + fi + '">' +
|
||||
'<div id="src-list-' + ci + '-' + fi + '" style="max-height:200px;overflow-y:auto;border:1px solid rgba(255,255,255,0.05);border-radius:8px;margin-top:0.25rem">' + sourceListHtml + '</div>' +
|
||||
|
|
@ -2005,7 +2038,7 @@ function renderCollections() {
|
|||
});
|
||||
|
||||
card.innerHTML = headerHtml + settingsHtml + foldersHtml +
|
||||
'<div style="padding:0.5rem 1rem 0.875rem"><button class="btn" onclick="addFolder(' + ci + ')" style="width:100%;padding:0.6rem;font-size:0.8rem">+ Add Folder</button></div>';
|
||||
'<div style="padding:0.5rem 1rem 0.875rem"><button class="btn" onclick="addFolder(' + ci + ')" style="width:100%;padding:0.6rem;font-size:0.8rem">+ ' + i18n.addFolder + '</button></div>';
|
||||
|
||||
container.appendChild(card);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -286,7 +286,11 @@ class WatchProgressSyncService @Inject constructor(
|
|||
}
|
||||
}
|
||||
.groupBy { it.contentId }
|
||||
.mapValues { (_, episodes) -> episodes.maxByOrNull { it.lastWatched } }
|
||||
.mapValues { (_, episodes) -> episodes.maxWithOrNull(
|
||||
compareBy<WatchProgress> { it.lastWatched }
|
||||
.thenBy { it.season ?: 0 }
|
||||
.thenBy { it.episode ?: 0 }
|
||||
) }
|
||||
|
||||
latestEpisodeByContent.forEach { (contentId, latestEpisode) ->
|
||||
val latest = latestEpisode ?: return@forEach
|
||||
|
|
|
|||
|
|
@ -30,6 +30,9 @@ import kotlinx.coroutines.CoroutineScope
|
|||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
|
|
@ -298,8 +301,7 @@ class TraktProgressService @Inject constructor(
|
|||
|
||||
val optimistic = progress.copy(
|
||||
progressPercent = derivedPercent,
|
||||
source = WatchProgress.SOURCE_TRAKT_PLAYBACK,
|
||||
lastWatched = now
|
||||
source = WatchProgress.SOURCE_TRAKT_PLAYBACK
|
||||
)
|
||||
|
||||
optimisticProgress.update { current ->
|
||||
|
|
@ -369,8 +371,7 @@ class TraktProgressService @Inject constructor(
|
|||
|
||||
fun observeWatchedShowSeeds(): Flow<List<WatchProgress>> {
|
||||
return combine(
|
||||
watchedShowSeedsState
|
||||
.onStart { emit(getWatchedShowSeedsSnapshot(forceRefresh = false)) },
|
||||
watchedShowSeedsState,
|
||||
hiddenProgressShowIds
|
||||
) { seeds, _ ->
|
||||
seeds.filter { !isShowHiddenFromProgress(it.contentId) }
|
||||
|
|
@ -796,17 +797,21 @@ class TraktProgressService @Inject constructor(
|
|||
getWatchedMoviesSnapshot(forceRefresh = true)
|
||||
}
|
||||
|
||||
if (force && hasLoadedWatchedShowSeeds) {
|
||||
getWatchedShowSeedsSnapshot(forceRefresh = true)
|
||||
}
|
||||
val needSeedsRefresh = force ||
|
||||
(watchedShowSeedsStale && hasLoadedWatchedShowSeeds)
|
||||
coroutineScope {
|
||||
val progressDeferred = async { fetchAllProgressSnapshot(force = force) }
|
||||
val seedsDeferred = if (needSeedsRefresh) {
|
||||
async { getWatchedShowSeedsSnapshot(forceRefresh = true) }
|
||||
} else null
|
||||
|
||||
val snapshot = fetchAllProgressSnapshot(force = force)
|
||||
remoteProgress.value = snapshot
|
||||
hasLoadedRemoteProgress.value = true
|
||||
reconcileOptimistic(snapshot)
|
||||
hydrateMetadata(snapshot)
|
||||
if (!force && watchedShowSeedsStale && hasLoadedWatchedShowSeeds) {
|
||||
getWatchedShowSeedsSnapshot(forceRefresh = true)
|
||||
val snapshot = progressDeferred.await()
|
||||
seedsDeferred?.await()
|
||||
|
||||
remoteProgress.value = snapshot
|
||||
hasLoadedRemoteProgress.value = true
|
||||
reconcileOptimistic(snapshot)
|
||||
hydrateMetadata(snapshot)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1357,13 +1362,22 @@ class TraktProgressService @Inject constructor(
|
|||
}
|
||||
|
||||
private suspend fun fetchAllProgressSnapshot(force: Boolean = false): List<WatchProgress> {
|
||||
val recentCompletedEpisodes = fetchRecentEpisodeHistorySnapshot()
|
||||
val playbackStartAt = recentWatchWindowMs()?.let { windowMs ->
|
||||
toTraktUtcDateTime(System.currentTimeMillis() - windowMs)
|
||||
}
|
||||
val inProgressMovies = getPlayback("movies", force = force, startAt = playbackStartAt).mapNotNull { mapPlaybackMovie(it) }
|
||||
val inProgressEpisodes = getPlayback("episodes", force = force, startAt = playbackStartAt)
|
||||
.mapNotNull { mapPlaybackEpisode(it, applyAddonRemap = true) }
|
||||
|
||||
val (recentCompletedEpisodes, inProgressMovies, inProgressEpisodes) = coroutineScope {
|
||||
val historyDeferred = async { fetchRecentEpisodeHistorySnapshot() }
|
||||
val moviesDeferred = async {
|
||||
getPlayback("movies", force = force, startAt = playbackStartAt)
|
||||
.mapNotNull { mapPlaybackMovie(it) }
|
||||
}
|
||||
val episodesDeferred = async {
|
||||
getPlayback("episodes", force = force, startAt = playbackStartAt)
|
||||
.mapNotNull { mapPlaybackEpisode(it, applyAddonRemap = true) }
|
||||
}
|
||||
Triple(historyDeferred.await(), moviesDeferred.await(), episodesDeferred.await())
|
||||
}
|
||||
|
||||
val mergedByKey = linkedMapOf<String, WatchProgress>()
|
||||
|
||||
|
|
@ -1390,7 +1404,8 @@ class TraktProgressService @Inject constructor(
|
|||
"${progress.contentId}_s${progress.season}e${progress.episode}" in completedEpisodeKeys
|
||||
}
|
||||
|
||||
return mergedByKey.values.sortedByDescending { it.lastWatched }
|
||||
val finalSnapshot = mergedByKey.values.sortedByDescending { it.lastWatched }
|
||||
return finalSnapshot
|
||||
}
|
||||
|
||||
private suspend fun fetchRecentEpisodeHistorySnapshot(): List<WatchProgress> {
|
||||
|
|
|
|||
|
|
@ -345,15 +345,42 @@ class WatchProgressRepositoryImpl @Inject constructor(
|
|||
mergeNextUpSeeds(canonicalSeeds, optimisticSeeds)
|
||||
}
|
||||
} else {
|
||||
watchProgressPreferences.allProgress.map { items ->
|
||||
items.filter { progress ->
|
||||
progress.isCompleted() &&
|
||||
progress.contentType.equals("series", ignoreCase = true) &&
|
||||
progress.season != null &&
|
||||
progress.episode != null &&
|
||||
progress.season != 0 &&
|
||||
!isMalformedNextUpSeedContentId(progress.contentId)
|
||||
}
|
||||
// Use watched items (fully synced with pagination) to build seeds
|
||||
// instead of watch progress (limited to 1000 entries).
|
||||
watchedItemsPreferences.allItems.map { items ->
|
||||
items
|
||||
.filter { item ->
|
||||
(item.contentType.equals("series", ignoreCase = true) ||
|
||||
item.contentType.equals("tv", ignoreCase = true)) &&
|
||||
item.season != null &&
|
||||
item.episode != null &&
|
||||
item.season != 0 &&
|
||||
!isMalformedNextUpSeedContentId(item.contentId)
|
||||
}
|
||||
.groupBy { it.contentId }
|
||||
.mapNotNull { (_, episodes) ->
|
||||
val latest = episodes.maxWithOrNull(
|
||||
compareBy<WatchedItem> { it.watchedAt }
|
||||
.thenBy { it.season ?: 0 }
|
||||
.thenBy { it.episode ?: 0 }
|
||||
) ?: return@mapNotNull null
|
||||
WatchProgress(
|
||||
contentId = latest.contentId,
|
||||
contentType = latest.contentType,
|
||||
name = latest.title,
|
||||
poster = null,
|
||||
backdrop = null,
|
||||
logo = null,
|
||||
videoId = latest.contentId,
|
||||
season = latest.season,
|
||||
episode = latest.episode,
|
||||
episodeTitle = null,
|
||||
position = 1L,
|
||||
duration = 1L,
|
||||
lastWatched = latest.watchedAt,
|
||||
progressPercent = 100f
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -623,12 +623,12 @@ private fun CollectionsEntryCard(onClick: () -> Unit) {
|
|||
Spacer(modifier = Modifier.width(16.dp))
|
||||
Column {
|
||||
Text(
|
||||
text = "Collections",
|
||||
text = stringResource(R.string.collections_card_title),
|
||||
style = MaterialTheme.typography.titleMedium.copy(fontWeight = FontWeight.Bold),
|
||||
color = NuvioColors.TextPrimary
|
||||
)
|
||||
Text(
|
||||
text = "Group catalogs into folders on your home screen",
|
||||
text = stringResource(R.string.collections_card_subtitle),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = NuvioColors.TextSecondary
|
||||
)
|
||||
|
|
|
|||
|
|
@ -71,6 +71,8 @@ import com.nuvio.tv.domain.model.FolderViewMode
|
|||
import com.nuvio.tv.domain.model.PosterShape
|
||||
import com.nuvio.tv.ui.components.LoadingIndicator
|
||||
import com.nuvio.tv.ui.theme.NuvioColors
|
||||
import com.nuvio.tv.R
|
||||
import androidx.compose.ui.res.stringResource
|
||||
|
||||
@OptIn(ExperimentalTvMaterial3Api::class)
|
||||
@Composable
|
||||
|
|
@ -215,7 +217,7 @@ fun CollectionEditorScreen(
|
|||
) {
|
||||
item(key = "header") {
|
||||
Text(
|
||||
text = if (uiState.isNew) "New Collection" else "Edit Collection",
|
||||
text = if (uiState.isNew) stringResource(R.string.collections_new) else stringResource(R.string.collections_editor_edit_collection),
|
||||
style = MaterialTheme.typography.headlineMedium,
|
||||
color = NuvioColors.TextPrimary
|
||||
)
|
||||
|
|
@ -224,7 +226,7 @@ fun CollectionEditorScreen(
|
|||
|
||||
item(key = "title") {
|
||||
Text(
|
||||
text = "Row Title",
|
||||
text = stringResource(R.string.collections_editor_row_title),
|
||||
style = MaterialTheme.typography.labelLarge,
|
||||
color = NuvioColors.TextSecondary
|
||||
)
|
||||
|
|
@ -238,11 +240,11 @@ fun CollectionEditorScreen(
|
|||
value = uiState.title,
|
||||
onValueChange = { viewModel.setTitle(it) },
|
||||
modifier = Modifier.weight(1f),
|
||||
placeholder = "Collection name"
|
||||
placeholder = stringResource(R.string.collections_editor_placeholder_name)
|
||||
)
|
||||
val canSaveCollection = uiState.title.isNotBlank() && uiState.folders.isNotEmpty()
|
||||
NuvioButton(onClick = { viewModel.save { onBack() } }, enabled = canSaveCollection) {
|
||||
Text("Save")
|
||||
Text(stringResource(R.string.collections_editor_save))
|
||||
}
|
||||
}
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
|
@ -250,7 +252,7 @@ fun CollectionEditorScreen(
|
|||
|
||||
item(key = "backdrop") {
|
||||
Text(
|
||||
text = "Backdrop Image",
|
||||
text = stringResource(R.string.collections_editor_backdrop),
|
||||
style = MaterialTheme.typography.labelLarge,
|
||||
color = NuvioColors.TextSecondary
|
||||
)
|
||||
|
|
@ -259,7 +261,7 @@ fun CollectionEditorScreen(
|
|||
value = uiState.backdropImageUrl,
|
||||
onValueChange = { viewModel.setBackdropImageUrl(it) },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
placeholder = "Backdrop image URL (optional)"
|
||||
placeholder = stringResource(R.string.collections_editor_placeholder_backdrop)
|
||||
)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
}
|
||||
|
|
@ -290,13 +292,13 @@ fun CollectionEditorScreen(
|
|||
) {
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
text = "Pin Above Catalogs",
|
||||
text = stringResource(R.string.collections_editor_pin_above),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
color = NuvioColors.TextPrimary
|
||||
)
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
Text(
|
||||
text = "Show this collection above all regular home catalogs. Multiple pinned collections follow collection creation order.",
|
||||
text = stringResource(R.string.collections_editor_pin_above_desc),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = NuvioColors.TextSecondary
|
||||
)
|
||||
|
|
@ -343,13 +345,13 @@ fun CollectionEditorScreen(
|
|||
) {
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
text = "Focus Glow",
|
||||
text = stringResource(R.string.collections_editor_focus_glow),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
color = NuvioColors.TextPrimary
|
||||
)
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
Text(
|
||||
text = "Use the native TV glow on this collection's home-screen custom catalog cards",
|
||||
text = stringResource(R.string.collections_editor_focus_glow_desc),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = NuvioColors.TextSecondary
|
||||
)
|
||||
|
|
@ -372,7 +374,7 @@ fun CollectionEditorScreen(
|
|||
|
||||
item(key = "view_mode") {
|
||||
Text(
|
||||
text = "View Mode",
|
||||
text = stringResource(R.string.collections_editor_view_mode),
|
||||
style = MaterialTheme.typography.labelLarge,
|
||||
color = NuvioColors.TextSecondary
|
||||
)
|
||||
|
|
@ -382,9 +384,9 @@ fun CollectionEditorScreen(
|
|||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
val viewModes = listOf(
|
||||
FolderViewMode.TABBED_GRID to "Tabs",
|
||||
FolderViewMode.ROWS to "Rows",
|
||||
FolderViewMode.FOLLOW_LAYOUT to "Follow Home Layout"
|
||||
FolderViewMode.TABBED_GRID to stringResource(R.string.collections_editor_view_mode_tabs),
|
||||
FolderViewMode.ROWS to stringResource(R.string.collections_editor_view_mode_rows),
|
||||
FolderViewMode.FOLLOW_LAYOUT to stringResource(R.string.collections_editor_view_mode_follow)
|
||||
)
|
||||
viewModes.forEach { (mode, label) ->
|
||||
val isSelected = uiState.viewMode == mode
|
||||
|
|
@ -442,13 +444,13 @@ fun CollectionEditorScreen(
|
|||
) {
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
text = "Show \"All\" Tab",
|
||||
text = stringResource(R.string.collections_editor_show_all_tab),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
color = NuvioColors.TextPrimary
|
||||
)
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
Text(
|
||||
text = "Combine all catalogs into one tab",
|
||||
text = stringResource(R.string.collections_editor_show_all_tab_desc),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = NuvioColors.TextSecondary
|
||||
)
|
||||
|
|
@ -477,7 +479,7 @@ fun CollectionEditorScreen(
|
|||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
text = "Folders",
|
||||
text = stringResource(R.string.collections_editor_folders),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
color = NuvioColors.TextPrimary
|
||||
)
|
||||
|
|
@ -510,9 +512,9 @@ fun CollectionEditorScreen(
|
|||
item(key = "add_folder") {
|
||||
Box(modifier = Modifier.padding(start = 8.dp, end = 8.dp, top = 4.dp)) {
|
||||
NuvioButton(onClick = { viewModel.addFolder() }) {
|
||||
Icon(imageVector = Icons.Default.Add, contentDescription = "Add")
|
||||
Icon(imageVector = Icons.Default.Add, contentDescription = stringResource(R.string.cd_add))
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Text("Add Folder")
|
||||
Text(stringResource(R.string.collections_editor_add_folder))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -551,7 +553,7 @@ private fun FolderListItem(
|
|||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
Text(
|
||||
text = "${folder.tileShape.name.lowercase().replaceFirstChar { it.uppercase() }} - ${folder.catalogSources.size} catalog${if (folder.catalogSources.size != 1) "s" else ""}",
|
||||
text = "${folder.tileShape.name.lowercase().replaceFirstChar { it.uppercase() }} - ${stringResource(R.string.collections_editor_catalog_count, folder.catalogSources.size)}",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = NuvioColors.TextTertiary
|
||||
)
|
||||
|
|
@ -559,16 +561,16 @@ private fun FolderListItem(
|
|||
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(2.dp)) {
|
||||
NuvioButton(onClick = onMoveUp) {
|
||||
Icon(Icons.Default.KeyboardArrowUp, "Move Up", tint = if (!isFirst) NuvioColors.TextSecondary else NuvioColors.TextTertiary)
|
||||
Icon(Icons.Default.KeyboardArrowUp, stringResource(R.string.cd_move_up), tint = if (!isFirst) NuvioColors.TextSecondary else NuvioColors.TextTertiary)
|
||||
}
|
||||
NuvioButton(onClick = onMoveDown) {
|
||||
Icon(Icons.Default.KeyboardArrowDown, "Move Down", tint = if (!isLast) NuvioColors.TextSecondary else NuvioColors.TextTertiary)
|
||||
Icon(Icons.Default.KeyboardArrowDown, stringResource(R.string.cd_move_down), tint = if (!isLast) NuvioColors.TextSecondary else NuvioColors.TextTertiary)
|
||||
}
|
||||
NuvioButton(onClick = onEdit) {
|
||||
Icon(Icons.Default.Edit, "Edit", tint = NuvioColors.TextSecondary)
|
||||
Icon(Icons.Default.Edit, stringResource(R.string.cd_edit), tint = NuvioColors.TextSecondary)
|
||||
}
|
||||
NuvioButton(onClick = onDelete) {
|
||||
Icon(Icons.Default.Delete, "Delete", tint = NuvioColors.TextSecondary)
|
||||
Icon(Icons.Default.Delete, stringResource(R.string.cd_delete), tint = NuvioColors.TextSecondary)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -623,17 +625,17 @@ private fun FolderEditorContent(
|
|||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
text = "Edit Folder",
|
||||
text = stringResource(R.string.collections_editor_edit_folder),
|
||||
style = MaterialTheme.typography.headlineMedium,
|
||||
color = NuvioColors.TextPrimary
|
||||
)
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
NuvioButton(onClick = { viewModel.cancelFolderEdit() }) {
|
||||
Text("Cancel")
|
||||
Text(stringResource(R.string.collections_cancel))
|
||||
}
|
||||
val canSaveFolder = (uiState.editingFolder?.catalogSources?.isNotEmpty() == true)
|
||||
NuvioButton(onClick = { viewModel.saveFolderEdit() }, enabled = canSaveFolder) {
|
||||
Text("Save")
|
||||
Text(stringResource(R.string.collections_editor_save))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -662,13 +664,13 @@ private fun FolderEditorContent(
|
|||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
item {
|
||||
Text("Folder Title", style = MaterialTheme.typography.labelLarge, color = NuvioColors.TextSecondary)
|
||||
Text(stringResource(R.string.collections_editor_folder_title), style = MaterialTheme.typography.labelLarge, color = NuvioColors.TextSecondary)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
NuvioTextField(
|
||||
value = folder.title,
|
||||
onValueChange = { viewModel.updateFolderTitle(it) },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
placeholder = "Folder name",
|
||||
placeholder = stringResource(R.string.collections_editor_placeholder_folder),
|
||||
focusRequester = titleFocusRequester
|
||||
)
|
||||
}
|
||||
|
|
@ -681,7 +683,7 @@ private fun FolderEditorContent(
|
|||
else -> "none"
|
||||
}
|
||||
|
||||
Text("Cover", style = MaterialTheme.typography.labelLarge, color = NuvioColors.TextSecondary)
|
||||
Text(stringResource(R.string.collections_editor_cover), style = MaterialTheme.typography.labelLarge, color = NuvioColors.TextSecondary)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||
Button(
|
||||
|
|
@ -703,7 +705,7 @@ private fun FolderEditorContent(
|
|||
)
|
||||
),
|
||||
shape = ButtonDefaults.shape(RoundedCornerShape(12.dp))
|
||||
) { Text("None") }
|
||||
) { Text(stringResource(R.string.collections_editor_cover_none)) }
|
||||
|
||||
Button(
|
||||
onClick = { viewModel.showEmojiPicker() },
|
||||
|
|
@ -726,9 +728,9 @@ private fun FolderEditorContent(
|
|||
shape = ButtonDefaults.shape(RoundedCornerShape(12.dp))
|
||||
) {
|
||||
if (hasEmoji) {
|
||||
Text("${folder.coverEmoji} Emoji")
|
||||
Text("${folder.coverEmoji} ${stringResource(R.string.collections_editor_cover_emoji)}")
|
||||
} else {
|
||||
Text("Emoji")
|
||||
Text(stringResource(R.string.collections_editor_cover_emoji))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -751,7 +753,7 @@ private fun FolderEditorContent(
|
|||
)
|
||||
),
|
||||
shape = ButtonDefaults.shape(RoundedCornerShape(12.dp))
|
||||
) { Text("Image URL") }
|
||||
) { Text(stringResource(R.string.collections_editor_cover_image_url)) }
|
||||
}
|
||||
|
||||
if (coverMode == "image") {
|
||||
|
|
@ -778,7 +780,7 @@ private fun FolderEditorContent(
|
|||
) {
|
||||
AsyncImage(
|
||||
model = folder.coverImageUrl,
|
||||
contentDescription = "Preview",
|
||||
contentDescription = stringResource(R.string.cd_preview),
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.clip(RoundedCornerShape(12.dp)),
|
||||
|
|
@ -790,13 +792,13 @@ private fun FolderEditorContent(
|
|||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
Text("Focus GIF", style = MaterialTheme.typography.labelLarge, color = NuvioColors.TextSecondary)
|
||||
Text(stringResource(R.string.collections_editor_focus_gif), style = MaterialTheme.typography.labelLarge, color = NuvioColors.TextSecondary)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
NuvioTextField(
|
||||
value = folder.focusGifUrl.orEmpty(),
|
||||
onValueChange = { viewModel.updateFolderFocusGifUrl(it) },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
placeholder = "Animated GIF URL (plays only while focused)"
|
||||
placeholder = stringResource(R.string.collections_editor_placeholder_gif)
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
|
|
@ -823,7 +825,7 @@ private fun FolderEditorContent(
|
|||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Text("Play GIF on Focus", style = MaterialTheme.typography.bodyLarge, color = NuvioColors.TextPrimary)
|
||||
Text(stringResource(R.string.collections_editor_play_gif), style = MaterialTheme.typography.bodyLarge, color = NuvioColors.TextPrimary)
|
||||
Switch(
|
||||
checked = folder.focusGifEnabled,
|
||||
onCheckedChange = { viewModel.updateFolderFocusGifEnabled(it) }
|
||||
|
|
@ -833,7 +835,7 @@ private fun FolderEditorContent(
|
|||
}
|
||||
|
||||
item {
|
||||
Text("Tile Shape", style = MaterialTheme.typography.labelLarge, color = NuvioColors.TextSecondary)
|
||||
Text(stringResource(R.string.collections_editor_tile_shape), style = MaterialTheme.typography.labelLarge, color = NuvioColors.TextSecondary)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
val shapeFocusRequesters = remember { PosterShape.entries.associateWith { FocusRequester() } }
|
||||
Row(
|
||||
|
|
@ -844,9 +846,9 @@ private fun FolderEditorContent(
|
|||
) {
|
||||
PosterShape.entries.forEach { shape ->
|
||||
val label = when (shape) {
|
||||
PosterShape.POSTER -> "Poster"
|
||||
PosterShape.LANDSCAPE -> "Wide"
|
||||
PosterShape.SQUARE -> "Square"
|
||||
PosterShape.POSTER -> stringResource(R.string.collections_editor_shape_poster)
|
||||
PosterShape.LANDSCAPE -> stringResource(R.string.collections_editor_shape_wide)
|
||||
PosterShape.SQUARE -> stringResource(R.string.collections_editor_shape_square)
|
||||
}
|
||||
val isSelected = folder.tileShape == shape
|
||||
Button(
|
||||
|
|
@ -902,13 +904,13 @@ private fun FolderEditorContent(
|
|||
) {
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
text = "Hide Title",
|
||||
text = stringResource(R.string.collections_editor_hide_title),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
color = NuvioColors.TextPrimary
|
||||
)
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
Text(
|
||||
text = "Only show the cover image",
|
||||
text = stringResource(R.string.collections_editor_hide_title_desc),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = NuvioColors.TextSecondary
|
||||
)
|
||||
|
|
@ -934,9 +936,9 @@ private fun FolderEditorContent(
|
|||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text("Catalogs", style = MaterialTheme.typography.labelLarge, color = NuvioColors.TextSecondary)
|
||||
Text(stringResource(R.string.collections_editor_catalogs), style = MaterialTheme.typography.labelLarge, color = NuvioColors.TextSecondary)
|
||||
Text(
|
||||
"${folder.catalogSources.size} catalog${if (folder.catalogSources.size != 1) "s" else ""}",
|
||||
"${folder.catalogSources.size} ${stringResource(R.string.collections_editor_catalogs).lowercase()}",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = NuvioColors.TextTertiary
|
||||
)
|
||||
|
|
@ -976,7 +978,7 @@ private fun FolderEditorContent(
|
|||
color = if (isMissing) NuvioColors.Error else NuvioColors.TextPrimary
|
||||
)
|
||||
Text(
|
||||
text = if (isMissing) "Addon not installed: ${source.addonId}" else "${source.type} - ${catalog.addonName}",
|
||||
text = if (isMissing) stringResource(R.string.collections_editor_addon_missing, source.addonId) else "${source.type} - ${catalog.addonName}",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = if (isMissing) NuvioColors.Error.copy(alpha = 0.7f) else NuvioColors.TextTertiary
|
||||
)
|
||||
|
|
@ -998,7 +1000,7 @@ private fun FolderEditorContent(
|
|||
),
|
||||
shape = ButtonDefaults.shape(RoundedCornerShape(12.dp))
|
||||
) {
|
||||
Icon(Icons.Default.KeyboardArrowUp, "Move Up", tint = if (index > 0) NuvioColors.TextSecondary else NuvioColors.TextTertiary)
|
||||
Icon(Icons.Default.KeyboardArrowUp, stringResource(R.string.cd_move_up), tint = if (index > 0) NuvioColors.TextSecondary else NuvioColors.TextTertiary)
|
||||
}
|
||||
Button(
|
||||
onClick = { viewModel.moveCatalogSourceDown(index) },
|
||||
|
|
@ -1016,7 +1018,7 @@ private fun FolderEditorContent(
|
|||
),
|
||||
shape = ButtonDefaults.shape(RoundedCornerShape(12.dp))
|
||||
) {
|
||||
Icon(Icons.Default.KeyboardArrowDown, "Move Down", tint = if (index < folder.catalogSources.size - 1) NuvioColors.TextSecondary else NuvioColors.TextTertiary)
|
||||
Icon(Icons.Default.KeyboardArrowDown, stringResource(R.string.cd_move_down), tint = if (index < folder.catalogSources.size - 1) NuvioColors.TextSecondary else NuvioColors.TextTertiary)
|
||||
}
|
||||
Button(
|
||||
onClick = {
|
||||
|
|
@ -1038,7 +1040,7 @@ private fun FolderEditorContent(
|
|||
),
|
||||
shape = ButtonDefaults.shape(RoundedCornerShape(12.dp))
|
||||
) {
|
||||
Icon(Icons.Default.Close, "Remove")
|
||||
Icon(Icons.Default.Close, stringResource(R.string.cd_remove))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1047,9 +1049,9 @@ private fun FolderEditorContent(
|
|||
|
||||
item {
|
||||
NuvioButton(onClick = { viewModel.showCatalogPicker() }) {
|
||||
Icon(Icons.Default.Add, "Add")
|
||||
Icon(Icons.Default.Add, stringResource(R.string.cd_add))
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Text("Add Catalog")
|
||||
Text(stringResource(R.string.collections_editor_add_catalog))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1075,11 +1077,11 @@ private fun CatalogPickerContent(
|
|||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
text = "Select Catalogs",
|
||||
text = stringResource(R.string.collections_editor_select_catalogs),
|
||||
style = MaterialTheme.typography.headlineMedium,
|
||||
color = NuvioColors.TextPrimary
|
||||
)
|
||||
NuvioButton(onClick = onBack) { Text("Done") }
|
||||
NuvioButton(onClick = onBack) { Text(stringResource(R.string.collections_editor_done)) }
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
|
@ -1144,7 +1146,7 @@ private fun CatalogPickerContent(
|
|||
} else {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Add,
|
||||
contentDescription = "Add",
|
||||
contentDescription = stringResource(R.string.cd_add),
|
||||
tint = NuvioColors.TextTertiary
|
||||
)
|
||||
}
|
||||
|
|
@ -1214,11 +1216,11 @@ private fun EmojiPickerContent(
|
|||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
text = "Choose Emoji",
|
||||
text = stringResource(R.string.collections_editor_choose_emoji),
|
||||
style = MaterialTheme.typography.headlineMedium,
|
||||
color = NuvioColors.TextPrimary
|
||||
)
|
||||
NuvioButton(onClick = onBack) { Text("Back") }
|
||||
NuvioButton(onClick = onBack) { Text(stringResource(R.string.collections_editor_back)) }
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
|
|
|||
|
|
@ -63,6 +63,8 @@ import com.nuvio.tv.data.local.ValidationResult
|
|||
import com.nuvio.tv.domain.model.Collection
|
||||
import com.nuvio.tv.ui.components.LoadingIndicator
|
||||
import com.nuvio.tv.ui.theme.NuvioColors
|
||||
import com.nuvio.tv.R
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
|
@ -159,7 +161,7 @@ fun CollectionManagementScreen(
|
|||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
text = "Collections",
|
||||
text = stringResource(R.string.collections_header),
|
||||
style = MaterialTheme.typography.headlineMedium,
|
||||
color = NuvioColors.TextPrimary
|
||||
)
|
||||
|
|
@ -191,23 +193,29 @@ fun CollectionManagementScreen(
|
|||
val uri = resolver.insert(existingUri, values)
|
||||
uri?.let { resolver.openOutputStream(it)?.use { os -> os.write(json.toByteArray()) } }
|
||||
}
|
||||
exportMessage = "Saved to Downloads"
|
||||
exportMessage = "saved"
|
||||
} catch (_: Exception) {
|
||||
exportMessage = "Export failed"
|
||||
exportMessage = "failed"
|
||||
}
|
||||
}
|
||||
}) {
|
||||
Text(exportMessage ?: "Export File")
|
||||
Text(exportMessage?.let {
|
||||
when (it) {
|
||||
"saved" -> stringResource(R.string.collections_saved_downloads)
|
||||
"failed" -> stringResource(R.string.collections_export_failed)
|
||||
else -> it
|
||||
}
|
||||
} ?: stringResource(R.string.collections_export_file))
|
||||
}
|
||||
}
|
||||
NuvioButton(onClick = { viewModel.showImportDialog() }) {
|
||||
Text("Import")
|
||||
Text(stringResource(R.string.collections_import))
|
||||
}
|
||||
NuvioButton(
|
||||
onClick = { onNavigateToEditor(null) },
|
||||
modifier = Modifier.focusRequester(newButtonFocusRequester)
|
||||
) {
|
||||
Text("New Collection")
|
||||
Text(stringResource(R.string.collections_new))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -220,7 +228,7 @@ fun CollectionManagementScreen(
|
|||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(
|
||||
text = "No collections yet. Create one to organize your catalogs.",
|
||||
text = stringResource(R.string.collections_empty),
|
||||
color = NuvioColors.TextSecondary,
|
||||
style = MaterialTheme.typography.bodyLarge
|
||||
)
|
||||
|
|
@ -282,12 +290,12 @@ private fun ImportContent(
|
|||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
text = "Import Collections",
|
||||
text = stringResource(R.string.collections_import_header),
|
||||
style = MaterialTheme.typography.headlineMedium,
|
||||
color = NuvioColors.TextPrimary
|
||||
)
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
NuvioButton(onClick = onBack) { Text("Cancel") }
|
||||
NuvioButton(onClick = onBack) { Text(stringResource(R.string.collections_cancel)) }
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -300,7 +308,7 @@ private fun ImportContent(
|
|||
) {
|
||||
item {
|
||||
Text(
|
||||
text = "Import collections from JSON. Catalogs from addons you don't have installed will show a warning.",
|
||||
text = stringResource(R.string.collections_import_description),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = NuvioColors.TextSecondary
|
||||
)
|
||||
|
|
@ -311,9 +319,9 @@ private fun ImportContent(
|
|||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
ImportMode.entries.filter { it != ImportMode.PASTE }.forEach { mode ->
|
||||
val label = when (mode) {
|
||||
ImportMode.PASTE -> "Paste JSON"
|
||||
ImportMode.FILE -> "From File"
|
||||
ImportMode.URL -> "From URL"
|
||||
ImportMode.PASTE -> stringResource(R.string.collections_mode_paste)
|
||||
ImportMode.FILE -> stringResource(R.string.collections_mode_file)
|
||||
ImportMode.URL -> stringResource(R.string.collections_mode_url)
|
||||
}
|
||||
if (mode == importMode) {
|
||||
Button(
|
||||
|
|
@ -349,9 +357,9 @@ private fun ImportContent(
|
|||
when (importMode) {
|
||||
ImportMode.PASTE -> {
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
NuvioButton(onClick = onPaste) { Text("Paste from Clipboard") }
|
||||
NuvioButton(onClick = onPaste) { Text(stringResource(R.string.collections_paste_clipboard)) }
|
||||
if (importText.isNotBlank()) {
|
||||
NuvioButton(onClick = onValidate) { Text("Validate") }
|
||||
NuvioButton(onClick = onValidate) { Text(stringResource(R.string.collections_validate)) }
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -381,7 +389,7 @@ private fun ImportContent(
|
|||
decorationBox = { innerTextField ->
|
||||
if (importText.isEmpty()) {
|
||||
Text(
|
||||
text = "Paste collections JSON here...",
|
||||
text = stringResource(R.string.collections_paste_hint),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = NuvioColors.TextTertiary
|
||||
)
|
||||
|
|
@ -395,17 +403,17 @@ private fun ImportContent(
|
|||
|
||||
ImportMode.FILE -> {
|
||||
Text(
|
||||
text = "Reads nuvio-collections.json from the Downloads folder.",
|
||||
text = stringResource(R.string.collections_file_description),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = NuvioColors.TextTertiary
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
NuvioButton(onClick = onPickFile) { Text("Load File") }
|
||||
NuvioButton(onClick = onPickFile) { Text(stringResource(R.string.collections_load_file)) }
|
||||
|
||||
if (importText.isNotBlank()) {
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
Text(
|
||||
text = "File loaded (${importText.length} characters)",
|
||||
text = stringResource(R.string.collections_file_loaded, importText.length),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = NuvioColors.TextSecondary
|
||||
)
|
||||
|
|
@ -452,7 +460,7 @@ private fun ImportContent(
|
|||
Spacer(modifier = Modifier.height(12.dp))
|
||||
|
||||
NuvioButton(onClick = onFetchUrl) {
|
||||
Text(if (isLoadingImport) "Fetching..." else "Fetch URL")
|
||||
Text(if (isLoadingImport) stringResource(R.string.collections_fetching) else stringResource(R.string.collections_fetch_url))
|
||||
}
|
||||
|
||||
if (isLoadingImport) {
|
||||
|
|
@ -478,20 +486,19 @@ private fun ImportContent(
|
|||
) {
|
||||
Column(modifier = Modifier.padding(16.dp)) {
|
||||
Text(
|
||||
text = "Valid JSON",
|
||||
text = stringResource(R.string.collections_valid_json),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
color = NuvioColors.Primary
|
||||
)
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
Text(
|
||||
text = "${validationResult.collectionCount} collection${if (validationResult.collectionCount != 1) "s" else ""}, " +
|
||||
"${validationResult.folderCount} folder${if (validationResult.folderCount != 1) "s" else ""}",
|
||||
text = stringResource(R.string.collections_valid_summary, validationResult.collectionCount, validationResult.folderCount),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = NuvioColors.TextSecondary
|
||||
)
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
NuvioButton(onClick = onConfirmImport) {
|
||||
Text("Confirm Import")
|
||||
Text(stringResource(R.string.collections_confirm_import))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -542,7 +549,7 @@ private fun CollectionListItem(
|
|||
color = NuvioColors.TextPrimary
|
||||
)
|
||||
Text(
|
||||
text = "${collection.folders.size} folder${if (collection.folders.size != 1) "s" else ""}",
|
||||
text = stringResource(R.string.collections_folder_count, collection.folders.size),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = NuvioColors.TextTertiary
|
||||
)
|
||||
|
|
@ -566,7 +573,7 @@ private fun CollectionListItem(
|
|||
),
|
||||
shape = ButtonDefaults.shape(RoundedCornerShape(12.dp))
|
||||
) {
|
||||
Icon(Icons.Default.KeyboardArrowUp, "Move Up")
|
||||
Icon(Icons.Default.KeyboardArrowUp, stringResource(R.string.cd_move_up))
|
||||
}
|
||||
|
||||
Button(
|
||||
|
|
@ -586,7 +593,7 @@ private fun CollectionListItem(
|
|||
),
|
||||
shape = ButtonDefaults.shape(RoundedCornerShape(12.dp))
|
||||
) {
|
||||
Icon(Icons.Default.KeyboardArrowDown, "Move Down")
|
||||
Icon(Icons.Default.KeyboardArrowDown, stringResource(R.string.cd_move_down))
|
||||
}
|
||||
|
||||
Button(
|
||||
|
|
@ -605,7 +612,7 @@ private fun CollectionListItem(
|
|||
),
|
||||
shape = ButtonDefaults.shape(RoundedCornerShape(12.dp))
|
||||
) {
|
||||
Icon(Icons.Default.Edit, "Edit")
|
||||
Icon(Icons.Default.Edit, stringResource(R.string.cd_edit))
|
||||
}
|
||||
|
||||
Button(
|
||||
|
|
@ -624,7 +631,7 @@ private fun CollectionListItem(
|
|||
),
|
||||
shape = ButtonDefaults.shape(RoundedCornerShape(12.dp))
|
||||
) {
|
||||
Icon(Icons.Default.Delete, "Delete")
|
||||
Icon(Icons.Default.Delete, stringResource(R.string.cd_delete))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -53,6 +53,8 @@ import com.nuvio.tv.domain.model.HomeLayout
|
|||
import com.nuvio.tv.ui.components.CatalogRowSection
|
||||
import com.nuvio.tv.ui.components.ContentCard
|
||||
import com.nuvio.tv.ui.components.LoadingIndicator
|
||||
import com.nuvio.tv.R
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import com.nuvio.tv.ui.components.PosterCardDefaults
|
||||
import com.nuvio.tv.ui.components.PosterCardStyle
|
||||
import com.nuvio.tv.ui.screens.home.ClassicHomeContent
|
||||
|
|
@ -94,13 +96,17 @@ fun FolderDetailScreen(
|
|||
{ item -> uiState.movieWatchedStatus[com.nuvio.tv.ui.screens.home.homeItemStatusKey(item.id, item.apiType)] == true }
|
||||
}
|
||||
|
||||
val enrichingItemId by viewModel.enrichingItemId.collectAsStateWithLifecycle()
|
||||
|
||||
if (uiState.viewMode == FolderViewMode.FOLLOW_LAYOUT) {
|
||||
FollowLayoutContent(
|
||||
uiState = uiState,
|
||||
focusState = followLayoutFocusState,
|
||||
enrichingItemId = enrichingItemId,
|
||||
onNavigateToDetail = onNavigateToDetail,
|
||||
onSaveFocusState = viewModel::saveFollowLayoutFocusState,
|
||||
onSaveGridFocusState = viewModel::saveFollowLayoutGridFocusState
|
||||
onSaveGridFocusState = viewModel::saveFollowLayoutGridFocusState,
|
||||
onItemFocus = viewModel::onItemFocused
|
||||
)
|
||||
} else {
|
||||
Column(
|
||||
|
|
@ -257,12 +263,18 @@ private fun TabbedGridContent(
|
|||
horizontalAlignment = Alignment.Start
|
||||
) {
|
||||
Text(
|
||||
text = tab.label,
|
||||
text = if (tab.isAllTab) stringResource(R.string.collections_tab_all) else tab.label,
|
||||
style = MaterialTheme.typography.labelLarge
|
||||
)
|
||||
if (tab.typeLabel.isNotBlank()) {
|
||||
val localizedType = when {
|
||||
tab.isAllTab -> stringResource(R.string.collections_tab_combined)
|
||||
tab.rawType.lowercase() == "movie" -> stringResource(R.string.type_movie)
|
||||
tab.rawType.lowercase() == "series" -> stringResource(R.string.type_series)
|
||||
else -> tab.typeLabel
|
||||
}
|
||||
Text(
|
||||
text = tab.typeLabel,
|
||||
text = localizedType,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = NuvioColors.TextTertiary
|
||||
)
|
||||
|
|
@ -425,6 +437,9 @@ private fun RowsContent(
|
|||
}
|
||||
}
|
||||
|
||||
val strTypeMovie = stringResource(R.string.type_movie)
|
||||
val strTypeSeries = stringResource(R.string.type_series)
|
||||
|
||||
LazyColumn(
|
||||
state = columnListState,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
|
|
@ -433,11 +448,25 @@ private fun RowsContent(
|
|||
) {
|
||||
sourceTabs.forEachIndexed { index, tab ->
|
||||
item(key = "row_${index}_${tab.label}") {
|
||||
val localizedTypeLabel = remember(tab.rawType, strTypeMovie, strTypeSeries) {
|
||||
when (tab.rawType.lowercase()) {
|
||||
"movie" -> strTypeMovie
|
||||
"series" -> strTypeSeries
|
||||
else -> tab.rawType.replaceFirstChar { it.uppercase() }
|
||||
}
|
||||
}
|
||||
val rowTitle = remember(tab.label, localizedTypeLabel) {
|
||||
if (tab.label != tab.typeLabel && localizedTypeLabel.isNotEmpty()) {
|
||||
"${tab.label} - $localizedTypeLabel"
|
||||
} else {
|
||||
tab.label
|
||||
}
|
||||
}
|
||||
when {
|
||||
tab.isLoading -> {
|
||||
Column(modifier = Modifier.fillMaxWidth()) {
|
||||
Text(
|
||||
text = tab.label,
|
||||
text = rowTitle,
|
||||
style = MaterialTheme.typography.headlineSmall,
|
||||
color = NuvioColors.TextPrimary,
|
||||
modifier = Modifier.padding(start = 48.dp, end = 48.dp, bottom = 12.dp)
|
||||
|
|
@ -455,7 +484,7 @@ private fun RowsContent(
|
|||
tab.error != null -> {
|
||||
Column(modifier = Modifier.fillMaxWidth()) {
|
||||
Text(
|
||||
text = tab.label,
|
||||
text = rowTitle,
|
||||
style = MaterialTheme.typography.headlineSmall,
|
||||
color = NuvioColors.TextPrimary,
|
||||
modifier = Modifier.padding(start = 48.dp, end = 48.dp, bottom = 12.dp)
|
||||
|
|
@ -483,7 +512,7 @@ private fun RowsContent(
|
|||
onItemClick = onNavigateToDetail,
|
||||
showPosterLabels = true,
|
||||
showAddonName = false,
|
||||
showCatalogTypeSuffix = false,
|
||||
showCatalogTypeSuffix = true,
|
||||
isItemWatched = isItemWatched,
|
||||
listState = listState,
|
||||
focusedItemIndex = if (
|
||||
|
|
@ -510,9 +539,11 @@ private fun RowsContent(
|
|||
private fun FollowLayoutContent(
|
||||
uiState: FolderDetailUiState,
|
||||
focusState: HomeScreenFocusState,
|
||||
enrichingItemId: String? = null,
|
||||
onNavigateToDetail: (String, String, String) -> Unit,
|
||||
onSaveFocusState: (Int, Int, Int, Int, Map<String, Int>) -> Unit,
|
||||
onSaveGridFocusState: (Int, Int, String?) -> Unit
|
||||
onSaveGridFocusState: (Int, Int, String?) -> Unit,
|
||||
onItemFocus: (MetaPreview) -> Unit = {}
|
||||
) {
|
||||
val homeState = uiState.followLayoutHomeState
|
||||
|
||||
|
|
@ -569,6 +600,7 @@ private fun FollowLayoutContent(
|
|||
HomeLayout.MODERN -> ModernHomeContent(
|
||||
uiState = homeState,
|
||||
focusState = focusState,
|
||||
enrichingItemId = enrichingItemId,
|
||||
trailerPreviewUrls = emptyMap(),
|
||||
trailerPreviewAudioUrls = emptyMap(),
|
||||
onNavigateToDetail = onNavigateToDetail,
|
||||
|
|
@ -578,6 +610,7 @@ private fun FollowLayoutContent(
|
|||
onRemoveContinueWatching = noOpRemoveCw,
|
||||
isCatalogItemWatched = isItemWatched,
|
||||
onNavigateToFolderDetail = noOpFolderDetail,
|
||||
onItemFocus = onItemFocus,
|
||||
onSaveFocusState = onSaveFocusState
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -47,6 +47,7 @@ data class FolderDetailUiState(
|
|||
data class FolderTab(
|
||||
val label: String,
|
||||
val typeLabel: String = "",
|
||||
val rawType: String = "",
|
||||
val catalogRow: CatalogRow? = null,
|
||||
val isLoading: Boolean = true,
|
||||
val error: String? = null,
|
||||
|
|
@ -68,7 +69,13 @@ class FolderDetailViewModel @Inject constructor(
|
|||
private val catalogRepository: CatalogRepository,
|
||||
private val layoutPreferenceDataStore: LayoutPreferenceDataStore,
|
||||
private val watchProgressRepository: WatchProgressRepository,
|
||||
private val watchedSeriesStateHolder: com.nuvio.tv.data.local.WatchedSeriesStateHolder
|
||||
private val watchedSeriesStateHolder: com.nuvio.tv.data.local.WatchedSeriesStateHolder,
|
||||
private val tmdbService: com.nuvio.tv.core.tmdb.TmdbService,
|
||||
private val tmdbMetadataService: com.nuvio.tv.core.tmdb.TmdbMetadataService,
|
||||
private val tmdbSettingsDataStore: com.nuvio.tv.data.local.TmdbSettingsDataStore,
|
||||
private val mdbListRepository: com.nuvio.tv.data.repository.MDBListRepository,
|
||||
private val mdbListSettingsDataStore: com.nuvio.tv.data.local.MDBListSettingsDataStore,
|
||||
private val metaRepository: com.nuvio.tv.domain.repository.MetaRepository
|
||||
) : ViewModel() {
|
||||
|
||||
private val collectionId: String = savedStateHandle["collectionId"] ?: ""
|
||||
|
|
@ -79,6 +86,10 @@ class FolderDetailViewModel @Inject constructor(
|
|||
|
||||
private var movieWatchedJob: Job? = null
|
||||
private var seriesWatchedJob: Job? = null
|
||||
private var enrichFocusJob: Job? = null
|
||||
private val enrichedItemIds = java.util.Collections.synchronizedSet(mutableSetOf<String>())
|
||||
private val _enrichingItemId = MutableStateFlow<String?>(null)
|
||||
val enrichingItemId: StateFlow<String?> = _enrichingItemId.asStateFlow()
|
||||
|
||||
private val _rowsFocusState = MutableStateFlow(com.nuvio.tv.ui.screens.home.HomeScreenFocusState())
|
||||
val rowsFocusState: StateFlow<com.nuvio.tv.ui.screens.home.HomeScreenFocusState> = _rowsFocusState.asStateFlow()
|
||||
|
|
@ -127,8 +138,10 @@ class FolderDetailViewModel @Inject constructor(
|
|||
val sourceTabs = folder.catalogSources.map { source ->
|
||||
val addon = addons.find { it.id == source.addonId }
|
||||
val catalog = addon?.catalogs?.find { it.id == source.catalogId && it.apiType == source.type }
|
||||
?: addon?.catalogs?.find { it.id == source.catalogId.substringBefore(",") && it.apiType == source.type }
|
||||
?: addons.firstNotNullOfOrNull { a -> a.catalogs.find { it.id == source.catalogId && it.apiType == source.type } }
|
||||
val (name, typeLabel) = buildTabLabels(source, catalog?.name)
|
||||
FolderTab(label = name, typeLabel = typeLabel, isLoading = true)
|
||||
FolderTab(label = name, typeLabel = typeLabel, rawType = source.type, isLoading = true)
|
||||
}
|
||||
|
||||
val tabs = if (showAll) {
|
||||
|
|
@ -266,13 +279,27 @@ class FolderDetailViewModel @Inject constructor(
|
|||
return@launch
|
||||
}
|
||||
|
||||
val catalog = addon.catalogs.find { it.id == source.catalogId && it.apiType == source.type }
|
||||
val catalogName = catalog?.name ?: source.catalogId
|
||||
var catalog = addon.catalogs.find { it.id == source.catalogId && it.apiType == source.type }
|
||||
?: addon.catalogs.find { it.id == source.catalogId.substringBefore(",") && it.apiType == source.type }
|
||||
// If the catalog wasn't found in the declared addon, search all installed addons.
|
||||
var effectiveAddon: com.nuvio.tv.domain.model.Addon = addon
|
||||
if (catalog == null) {
|
||||
for (a in addons) {
|
||||
val match = a.catalogs.find { it.id == source.catalogId && it.apiType == source.type }
|
||||
if (match != null) {
|
||||
effectiveAddon = a
|
||||
catalog = match
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
val tab = _uiState.value.tabs.getOrNull(tabIndex)
|
||||
val catalogName = catalog?.name ?: tab?.label?.takeIf { it != tab?.typeLabel } ?: source.catalogId
|
||||
|
||||
catalogRepository.getCatalog(
|
||||
addonBaseUrl = addon.baseUrl,
|
||||
addonId = addon.id,
|
||||
addonName = addon.displayName,
|
||||
addonBaseUrl = effectiveAddon.baseUrl,
|
||||
addonId = effectiveAddon.id,
|
||||
addonName = effectiveAddon.displayName,
|
||||
catalogId = source.catalogId,
|
||||
catalogName = catalogName,
|
||||
type = source.type,
|
||||
|
|
@ -447,4 +474,139 @@ class FolderDetailViewModel @Inject constructor(
|
|||
}
|
||||
return name to typeLabel
|
||||
}
|
||||
|
||||
fun onItemFocused(item: MetaPreview) {
|
||||
// Clear enriching for previous item immediately.
|
||||
if (_enrichingItemId.value != null && _enrichingItemId.value != item.id) {
|
||||
_enrichingItemId.value = null
|
||||
}
|
||||
if (item.id in enrichedItemIds) return
|
||||
|
||||
// Check if any enrichment source is active — if so, signal enriching immediately
|
||||
// so hero content hides until enrichment completes.
|
||||
val viewMode = _uiState.value.viewMode
|
||||
if (viewMode == FolderViewMode.FOLLOW_LAYOUT && _uiState.value.homeLayout == HomeLayout.MODERN) {
|
||||
_enrichingItemId.value = item.id
|
||||
}
|
||||
|
||||
enrichFocusJob?.cancel()
|
||||
enrichFocusJob = viewModelScope.launch(kotlinx.coroutines.Dispatchers.IO) {
|
||||
kotlinx.coroutines.delay(350)
|
||||
val tmdbSettings = tmdbSettingsDataStore.settings.first()
|
||||
val mdbSettings = mdbListSettingsDataStore.settings.first()
|
||||
val homeLayout = _uiState.value.homeLayout
|
||||
val tmdbEnabled = tmdbSettings.enabled &&
|
||||
(homeLayout != HomeLayout.MODERN || tmdbSettings.modernHomeEnabled)
|
||||
val mdbEnabled = mdbSettings.enabled && mdbSettings.apiKey.isNotBlank()
|
||||
val externalMetaEnabled = layoutPreferenceDataStore.preferExternalMetaAddonDetail.first()
|
||||
if (!tmdbEnabled && !mdbEnabled && !externalMetaEnabled) {
|
||||
if (_enrichingItemId.value == item.id) _enrichingItemId.value = null
|
||||
return@launch
|
||||
}
|
||||
|
||||
val mdbRating = if (mdbEnabled) {
|
||||
runCatching { mdbListRepository.getImdbRatingForItem(item.id, item.apiType) }.getOrNull()
|
||||
} else null
|
||||
|
||||
var enrichment: com.nuvio.tv.core.tmdb.TmdbEnrichment? = null
|
||||
if (tmdbEnabled) {
|
||||
val tmdbId = runCatching { tmdbService.ensureTmdbId(item.id, item.apiType) }.getOrNull()
|
||||
if (tmdbId != null) {
|
||||
enrichment = runCatching {
|
||||
tmdbMetadataService.fetchEnrichment(
|
||||
tmdbId = tmdbId,
|
||||
contentType = item.type,
|
||||
language = tmdbSettings.language
|
||||
)
|
||||
}.getOrNull()
|
||||
}
|
||||
}
|
||||
|
||||
if (enrichment == null && mdbRating == null && !externalMetaEnabled) return@launch
|
||||
enrichedItemIds.add(item.id)
|
||||
|
||||
// Apply TMDB + MDB enrichment if available.
|
||||
if (enrichment != null || mdbRating != null) {
|
||||
val finalEnrichment = enrichment
|
||||
val finalMdbRating = mdbRating
|
||||
|
||||
updateItemInTabs(item.id) { merged ->
|
||||
var result = merged
|
||||
if (finalEnrichment != null) {
|
||||
if (tmdbSettings.useBasicInfo) {
|
||||
result = result.copy(
|
||||
name = finalEnrichment.localizedTitle ?: result.name,
|
||||
description = finalEnrichment.description ?: result.description,
|
||||
genres = if (finalEnrichment.genres.isNotEmpty()) finalEnrichment.genres else result.genres,
|
||||
imdbRating = finalEnrichment.rating?.toFloat() ?: result.imdbRating
|
||||
)
|
||||
}
|
||||
if (tmdbSettings.useArtwork) {
|
||||
result = result.copy(
|
||||
background = finalEnrichment.backdrop ?: result.background,
|
||||
logo = finalEnrichment.logo ?: result.logo
|
||||
)
|
||||
}
|
||||
if (tmdbSettings.useReleaseDates) {
|
||||
result = result.copy(
|
||||
releaseInfo = finalEnrichment.releaseInfo ?: result.releaseInfo
|
||||
)
|
||||
}
|
||||
if (tmdbSettings.useDetails) {
|
||||
result = result.copy(
|
||||
ageRating = finalEnrichment.ageRating ?: result.ageRating,
|
||||
status = finalEnrichment.status ?: result.status
|
||||
)
|
||||
}
|
||||
}
|
||||
if (finalMdbRating != null && result.imdbRating == null) {
|
||||
result = result.copy(imdbRating = finalMdbRating.toFloat())
|
||||
}
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
// External meta addon fallback when TMDB didn't enrich.
|
||||
if (enrichment == null && externalMetaEnabled) {
|
||||
val metaResult = metaRepository.getMetaFromAllAddons(item.apiType, item.id)
|
||||
.first { it is NetworkResult.Success || it is NetworkResult.Error }
|
||||
if (metaResult is NetworkResult.Success) {
|
||||
val meta = metaResult.data
|
||||
updateItemInTabs(item.id) { merged ->
|
||||
merged.copy(
|
||||
name = meta.name.takeIf { it.isNotBlank() } ?: merged.name,
|
||||
description = meta.description?.takeIf { it.isNotBlank() } ?: merged.description,
|
||||
background = meta.background?.takeIf { it.isNotBlank() } ?: merged.background,
|
||||
logo = meta.logo?.takeIf { it.isNotBlank() } ?: merged.logo,
|
||||
genres = meta.genres.takeIf { it.isNotEmpty() } ?: merged.genres,
|
||||
imdbRating = meta.imdbRating ?: merged.imdbRating,
|
||||
releaseInfo = meta.releaseInfo?.takeIf { it.isNotBlank() } ?: merged.releaseInfo
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sync enriched tabs into followLayoutHomeState for FOLLOW_LAYOUT mode.
|
||||
if (_enrichingItemId.value == item.id) _enrichingItemId.value = null
|
||||
rebuildFollowLayoutState()
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateItemInTabs(itemId: String, transform: (MetaPreview) -> MetaPreview) {
|
||||
_uiState.update { state ->
|
||||
var changed = false
|
||||
val updatedTabs = state.tabs.map { tab ->
|
||||
val row = tab.catalogRow ?: return@map tab
|
||||
val idx = row.items.indexOfFirst { it.id == itemId }
|
||||
if (idx < 0) return@map tab
|
||||
val merged = transform(row.items[idx])
|
||||
if (merged == row.items[idx]) return@map tab
|
||||
changed = true
|
||||
val items = row.items.toMutableList()
|
||||
items[idx] = merged
|
||||
tab.copy(catalogRow = row.copy(items = items))
|
||||
}
|
||||
if (changed) state.copy(tabs = updatedTabs) else state
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1144,7 +1144,7 @@ class MetaDetailsViewModel @Inject constructor(
|
|||
video.copy(
|
||||
title = ep?.title ?: video.title,
|
||||
overview = ep?.overview ?: video.overview,
|
||||
released = ep?.airDate ?: video.released,
|
||||
released = if (settings.useReleaseDates) ep?.airDate ?: video.released else video.released,
|
||||
thumbnail = ep?.thumbnail ?: video.thumbnail,
|
||||
runtime = ep?.runtimeMinutes
|
||||
)
|
||||
|
|
|
|||
|
|
@ -82,7 +82,13 @@ internal fun HomeViewModel.observeTmdbSettingsPipeline() {
|
|||
tmdbSettingsDataStore.settings
|
||||
.distinctUntilChanged()
|
||||
.collectLatest { settings ->
|
||||
val languageChanged = currentTmdbSettings.language != settings.language
|
||||
currentTmdbSettings = settings
|
||||
if (languageChanged) {
|
||||
// Allow re-enrichment with the new language on next focus.
|
||||
prefetchedTmdbIds.clear()
|
||||
prefetchedExternalMetaIds.clear()
|
||||
}
|
||||
scheduleUpdateCatalogRows()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1098,7 +1098,7 @@ internal fun mergeContinueWatchingItems(
|
|||
filteredNextUpItems.forEach { combined.add(it.info.sortTimestamp to it) }
|
||||
|
||||
val seen = mutableSetOf<String>()
|
||||
return combined
|
||||
val result = combined
|
||||
.sortedByDescending { it.first }
|
||||
.map { it.second }
|
||||
.filter { item ->
|
||||
|
|
@ -1108,6 +1108,8 @@ internal fun mergeContinueWatchingItems(
|
|||
}
|
||||
contentId.isBlank() || seen.add(contentId)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
private suspend fun HomeViewModel.buildNextUpItem(
|
||||
|
|
@ -1274,7 +1276,7 @@ private suspend fun HomeViewModel.enrichNextUpItem(
|
|||
} else {
|
||||
null
|
||||
}
|
||||
val released = tmdbData?.airDate
|
||||
val released = (if (currentTmdbSettings.useReleaseDates) tmdbData?.airDate else null)
|
||||
?: video?.released?.trim()?.takeIf { it.isNotEmpty() }
|
||||
?: item.info.released
|
||||
val releaseDate = parseEpisodeReleaseDate(released)
|
||||
|
|
|
|||
|
|
@ -174,7 +174,16 @@ fun ModernHomeContent(
|
|||
trailerPlaybackTarget == FocusedPosterTrailerPlaybackTarget.HERO_MEDIA)
|
||||
val visibleHomeRows = remember(uiState.homeRows, uiState.catalogRows) {
|
||||
if (uiState.homeRows.isNotEmpty()) {
|
||||
uiState.homeRows
|
||||
val latestCatalogByKey = uiState.catalogRows.associateBy { catalogRowKey(it) }
|
||||
uiState.homeRows.map { homeRow ->
|
||||
when (homeRow) {
|
||||
is HomeRow.Catalog -> {
|
||||
val latest = latestCatalogByKey[catalogRowKey(homeRow.row)]
|
||||
if (latest != null && latest !== homeRow.row) HomeRow.Catalog(latest) else homeRow
|
||||
}
|
||||
else -> homeRow
|
||||
}
|
||||
}
|
||||
} else {
|
||||
uiState.catalogRows.filter { it.items.isNotEmpty() }.map { HomeRow.Catalog(it) }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -52,6 +52,9 @@ class PlayerRuntimeController(
|
|||
internal val layoutPreferenceDataStore: com.nuvio.tv.data.local.LayoutPreferenceDataStore,
|
||||
internal val watchedItemsPreferences: com.nuvio.tv.data.local.WatchedItemsPreferences,
|
||||
internal val trackPreferenceDataStore: com.nuvio.tv.data.local.TrackPreferenceDataStore,
|
||||
internal val tmdbService: com.nuvio.tv.core.tmdb.TmdbService,
|
||||
internal val tmdbMetadataService: com.nuvio.tv.core.tmdb.TmdbMetadataService,
|
||||
internal val tmdbSettingsDataStore: com.nuvio.tv.data.local.TmdbSettingsDataStore,
|
||||
savedStateHandle: SavedStateHandle,
|
||||
internal val scope: CoroutineScope
|
||||
) {
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package com.nuvio.tv.ui.screens.player
|
|||
|
||||
import com.nuvio.tv.R
|
||||
import com.nuvio.tv.core.network.NetworkResult
|
||||
import com.nuvio.tv.domain.model.ContentType
|
||||
import com.nuvio.tv.domain.model.Meta
|
||||
import com.nuvio.tv.domain.model.Stream
|
||||
import kotlinx.coroutines.delay
|
||||
|
|
@ -28,6 +29,10 @@ internal fun PlayerRuntimeController.fetchMetaDetails(id: String?, type: String?
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
scope.launch {
|
||||
enrichDescriptionFromTmdb(id, type)
|
||||
}
|
||||
}
|
||||
|
||||
internal fun PlayerRuntimeController.applyMetaDetails(meta: Meta) {
|
||||
|
|
@ -63,6 +68,82 @@ internal fun PlayerRuntimeController.updateEpisodeDescription() {
|
|||
if (!overview.isNullOrBlank()) {
|
||||
_uiState.update { it.copy(description = overview) }
|
||||
}
|
||||
|
||||
// Re-enrich from TMDB for the new episode.
|
||||
scope.launch {
|
||||
enrichDescriptionFromTmdb(contentId, contentType)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun PlayerRuntimeController.enrichDescriptionFromTmdb(id: String?, type: String?) {
|
||||
if (id.isNullOrBlank() || type.isNullOrBlank()) return
|
||||
val settings = tmdbSettingsDataStore.settings.first()
|
||||
if (!settings.enabled || !settings.useBasicInfo) return
|
||||
|
||||
val tmdbId = runCatching { tmdbService.ensureTmdbId(id, type) }.getOrNull() ?: return
|
||||
val contentType = when (type.lowercase()) {
|
||||
"series", "tv" -> ContentType.SERIES
|
||||
else -> ContentType.MOVIE
|
||||
}
|
||||
val enrichment = runCatching {
|
||||
tmdbMetadataService.fetchEnrichment(
|
||||
tmdbId = tmdbId,
|
||||
contentType = contentType,
|
||||
language = settings.language
|
||||
)
|
||||
}.getOrNull() ?: return
|
||||
|
||||
val isSeries = type.lowercase() in listOf("series", "tv")
|
||||
val season = currentSeason
|
||||
val episode = currentEpisode
|
||||
|
||||
// For series, try to get episode-level overview and title from TMDB.
|
||||
val episodeEnrichment = if (isSeries && season != null && episode != null) {
|
||||
runCatching {
|
||||
tmdbMetadataService.fetchEpisodeEnrichment(
|
||||
tmdbId = tmdbId,
|
||||
seasonNumbers = listOf(season),
|
||||
language = settings.language
|
||||
)[season to episode]
|
||||
}.getOrNull()
|
||||
} else null
|
||||
|
||||
val tmdbDescription = episodeEnrichment?.overview ?: enrichment.description
|
||||
if (settings.useBasicInfo && !tmdbDescription.isNullOrBlank()) {
|
||||
_uiState.update { it.copy(description = tmdbDescription) }
|
||||
}
|
||||
|
||||
// Enrich title from TMDB (localized).
|
||||
if (settings.useBasicInfo) {
|
||||
val tmdbTitle = enrichment.localizedTitle
|
||||
if (!tmdbTitle.isNullOrBlank()) {
|
||||
_uiState.update { it.copy(title = tmdbTitle) }
|
||||
}
|
||||
}
|
||||
|
||||
// Enrich logo from TMDB if artwork is enabled.
|
||||
if (settings.useArtwork) {
|
||||
val tmdbLogo = enrichment.logo
|
||||
if (!tmdbLogo.isNullOrBlank()) {
|
||||
_uiState.update { it.copy(logo = tmdbLogo) }
|
||||
}
|
||||
}
|
||||
|
||||
// Also enrich episode title from TMDB if available.
|
||||
if (settings.useBasicInfo) {
|
||||
val tmdbEpisodeTitle = episodeEnrichment?.title
|
||||
if (!tmdbEpisodeTitle.isNullOrBlank()) {
|
||||
_uiState.update { it.copy(currentEpisodeTitle = tmdbEpisodeTitle) }
|
||||
}
|
||||
}
|
||||
|
||||
// Enrich cast from TMDB if addon didn't provide any.
|
||||
if (settings.useBasicInfo && enrichment.castMembers.isNotEmpty()) {
|
||||
_uiState.update { state ->
|
||||
if (state.castMembers.isEmpty()) state.copy(castMembers = enrichment.castMembers)
|
||||
else state
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal fun PlayerRuntimeController.recomputeNextEpisode(resetVisibility: Boolean) {
|
||||
|
|
|
|||
|
|
@ -16,6 +16,9 @@ import com.nuvio.tv.domain.repository.AddonRepository
|
|||
import com.nuvio.tv.domain.repository.MetaRepository
|
||||
import com.nuvio.tv.domain.repository.StreamRepository
|
||||
import com.nuvio.tv.domain.repository.WatchProgressRepository
|
||||
import com.nuvio.tv.core.tmdb.TmdbService
|
||||
import com.nuvio.tv.core.tmdb.TmdbMetadataService
|
||||
import com.nuvio.tv.data.local.TmdbSettingsDataStore
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
|
@ -39,6 +42,9 @@ class PlayerViewModel @Inject constructor(
|
|||
private val layoutPreferenceDataStore: com.nuvio.tv.data.local.LayoutPreferenceDataStore,
|
||||
private val watchedItemsPreferences: com.nuvio.tv.data.local.WatchedItemsPreferences,
|
||||
private val trackPreferenceDataStore: com.nuvio.tv.data.local.TrackPreferenceDataStore,
|
||||
private val tmdbService: TmdbService,
|
||||
private val tmdbMetadataService: TmdbMetadataService,
|
||||
private val tmdbSettingsDataStore: TmdbSettingsDataStore,
|
||||
savedStateHandle: SavedStateHandle
|
||||
) : ViewModel() {
|
||||
|
||||
|
|
@ -59,6 +65,9 @@ class PlayerViewModel @Inject constructor(
|
|||
layoutPreferenceDataStore = layoutPreferenceDataStore,
|
||||
watchedItemsPreferences = watchedItemsPreferences,
|
||||
trackPreferenceDataStore = trackPreferenceDataStore,
|
||||
tmdbService = tmdbService,
|
||||
tmdbMetadataService = tmdbMetadataService,
|
||||
tmdbSettingsDataStore = tmdbSettingsDataStore,
|
||||
savedStateHandle = savedStateHandle,
|
||||
scope = viewModelScope
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1246,10 +1246,13 @@
|
|||
<string name="cd_loading_logo">Ładowanie logo</string>
|
||||
<string name="cd_move_up">Przesuń w górę</string>
|
||||
<string name="cd_move_down">Przesuń w dół</string>
|
||||
<string name="cd_edit">Edytuj</string>
|
||||
<string name="cd_delete">Usuń</string>
|
||||
<string name="cd_qr_code">Kod QR</string>
|
||||
<string name="cd_add">Dodaj</string>
|
||||
<string name="cd_refresh">Odśwież</string>
|
||||
<string name="cd_remove">Usuń</string>
|
||||
<string name="cd_preview">Podgląd</string>
|
||||
<string name="cd_test">Test</string>
|
||||
<string name="cd_unlink">Odłącz</string>
|
||||
<string name="cd_nuvio_logo">NuvioTV</string>
|
||||
|
|
@ -1355,6 +1358,22 @@
|
|||
<string name="web_manage_collections_title">Zarządzaj kolekcjami</string>
|
||||
<string name="web_manage_collections_subtitle">Twórz i zarządzaj swoimi kolekcjami</string>
|
||||
<string name="web_status_msg_collections_updated">Twoje kolekcje zostały zaktualizowane na TV.</string>
|
||||
<string name="web_tab_addons">Dodatki</string>
|
||||
<string name="web_tab_home_layout">Układ ekranu głównego</string>
|
||||
<string name="web_tab_collections">Kolekcje</string>
|
||||
<string name="web_btn_enable_all">Włącz wszystko</string>
|
||||
<string name="web_btn_disable_all">Wyłącz wszystko</string>
|
||||
<string name="web_btn_show_all">Pokaż wszystko</string>
|
||||
<string name="web_btn_hide_all">Ukryj wszystko</string>
|
||||
<string name="web_btn_new_collection">+ Nowa kolekcja</string>
|
||||
<string name="web_btn_export">Eksportuj</string>
|
||||
<string name="web_btn_import">Importuj</string>
|
||||
<string name="web_no_collections">Brak kolekcji</string>
|
||||
<string name="web_badge_collection">Kolekcja</string>
|
||||
<string name="web_import_collections_title">Importuj kolekcje</string>
|
||||
<string name="web_import_tab_paste">Wklej</string>
|
||||
<string name="web_import_tab_file">Plik</string>
|
||||
<string name="web_import_tab_url">URL</string>
|
||||
<string name="web_error_failed_save">Nie udało się zapisać. Sprawdź połączenie z TV.</string>
|
||||
<string name="web_no_repos">Brak zainstalowanych repozytoriów</string>
|
||||
<string name="web_manage_repos_title">Zarządzaj repozytoriami</string>
|
||||
|
|
@ -1384,4 +1403,80 @@
|
|||
<string name="player_engine_switching_manual_message">Przełączanie na %1$s…</string>
|
||||
<string name="search_recent_title">Ostatnie wyszukiwania</string>
|
||||
<string name="search_recent_clear">Wyczyść historię</string>
|
||||
|
||||
<!-- Collection Management -->
|
||||
<string name="collections_header">Kolekcje</string>
|
||||
<string name="collections_empty">Brak kolekcji. Utwórz jedną, aby uporządkować katalogi.</string>
|
||||
<string name="collections_new">Nowa kolekcja</string>
|
||||
<string name="collections_import">Importuj</string>
|
||||
<string name="collections_export_file">Eksportuj plik</string>
|
||||
<string name="collections_saved_downloads">Zapisano w Pobrane</string>
|
||||
<string name="collections_export_failed">Eksport nie powiódł się</string>
|
||||
<string name="collections_import_header">Importuj kolekcje</string>
|
||||
<string name="collections_import_description">Importuj kolekcje z JSON. Katalogi z niezainstalowanych dodatków pokażą ostrzeżenie.</string>
|
||||
<string name="collections_cancel">Anuluj</string>
|
||||
<string name="collections_mode_paste">Wklej JSON</string>
|
||||
<string name="collections_mode_file">Z pliku</string>
|
||||
<string name="collections_mode_url">Z adresu URL</string>
|
||||
<string name="collections_paste_clipboard">Wklej ze schowka</string>
|
||||
<string name="collections_validate">Sprawdź</string>
|
||||
<string name="collections_paste_hint">Wklej JSON kolekcji tutaj…</string>
|
||||
<string name="collections_file_description">Odczytuje nuvio-collections.json z folderu Pobrane.</string>
|
||||
<string name="collections_load_file">Wczytaj plik</string>
|
||||
<string name="collections_file_loaded">Plik wczytany (%1$d znaków)</string>
|
||||
<string name="collections_fetch_url">Pobierz URL</string>
|
||||
<string name="collections_fetching">Pobieranie…</string>
|
||||
<string name="collections_valid_json">Poprawny JSON</string>
|
||||
<string name="collections_valid_summary">%1$d kolekcji, %2$d folderów</string>
|
||||
<string name="collections_confirm_import">Potwierdź import</string>
|
||||
<string name="collections_folder_count">%1$d folderów</string>
|
||||
|
||||
<!-- Collection Editor -->
|
||||
<string name="collections_editor_row_title">Tytuł wiersza</string>
|
||||
<string name="collections_editor_save">Zapisz</string>
|
||||
<string name="collections_editor_backdrop">Obraz tła</string>
|
||||
<string name="collections_editor_pin_above">Przypnij nad katalogami</string>
|
||||
<string name="collections_editor_pin_above_desc">Pokaż tę kolekcję nad wszystkimi zwykłymi katalogami. Wiele przypiętych kolekcji zachowuje kolejność tworzenia.</string>
|
||||
<string name="collections_editor_focus_glow">Poświata zaznaczonej karty</string>
|
||||
<string name="collections_editor_focus_glow_desc">Użyj natywnej poświaty TV na kartach tej kolekcji na ekranie głównym</string>
|
||||
<string name="collections_editor_view_mode">Tryb widoku</string>
|
||||
<string name="collections_editor_show_all_tab">Pokaż kartę \"Wszystko\"</string>
|
||||
<string name="collections_editor_show_all_tab_desc">Połącz wszystkie katalogi w jednej karcie</string>
|
||||
<string name="collections_editor_folders">Foldery</string>
|
||||
<string name="collections_editor_add_folder">Dodaj folder</string>
|
||||
<string name="collections_editor_edit_folder">Edytuj folder</string>
|
||||
<string name="collections_editor_folder_title">Tytuł folderu</string>
|
||||
<string name="collections_editor_cover">Okładka</string>
|
||||
<string name="collections_editor_cover_none">Brak</string>
|
||||
<string name="collections_editor_cover_emoji">Emoji</string>
|
||||
<string name="collections_editor_cover_image_url">URL obrazu</string>
|
||||
<string name="collections_editor_focus_gif">GIF fokusa</string>
|
||||
<string name="collections_editor_play_gif">Odtwarzaj GIF przy fokusie</string>
|
||||
<string name="collections_editor_tile_shape">Kształt kafelka</string>
|
||||
<string name="collections_editor_hide_title">Ukryj tytuł</string>
|
||||
<string name="collections_editor_hide_title_desc">Pokaż tylko obraz okładki</string>
|
||||
<string name="collections_editor_display">Wyświetlanie</string>
|
||||
<string name="collections_editor_catalogs">Katalogi</string>
|
||||
<string name="collections_editor_add_catalog">Dodaj katalog</string>
|
||||
<string name="collections_editor_select_catalogs">Wybierz katalogi</string>
|
||||
<string name="collections_editor_done">Gotowe</string>
|
||||
<string name="collections_editor_choose_emoji">Wybierz emoji</string>
|
||||
<string name="collections_editor_back">Wstecz</string>
|
||||
<string name="collections_editor_edit_collection">Edytuj kolekcję</string>
|
||||
<string name="collections_editor_catalog_count">%1$d katalogów</string>
|
||||
<string name="collections_editor_view_mode_tabs">Karty</string>
|
||||
<string name="collections_editor_view_mode_rows">Wiersze</string>
|
||||
<string name="collections_editor_view_mode_follow">Układ ekranu głównego</string>
|
||||
<string name="collections_editor_shape_poster">Plakat</string>
|
||||
<string name="collections_editor_shape_wide">Szeroki</string>
|
||||
<string name="collections_editor_shape_square">Kwadrat</string>
|
||||
<string name="collections_editor_addon_missing">Dodatek niezainstalowany: %1$s</string>
|
||||
<string name="collections_editor_placeholder_name">Nazwa kolekcji</string>
|
||||
<string name="collections_editor_placeholder_backdrop">URL obrazu tła (opcjonalnie)</string>
|
||||
<string name="collections_editor_placeholder_folder">Nazwa folderu</string>
|
||||
<string name="collections_editor_placeholder_gif">URL animowanego GIF (odtwarzany przy zaznaczeniu)</string>
|
||||
<string name="collections_card_title">Kolekcje</string>
|
||||
<string name="collections_card_subtitle">Grupuj katalogi w folderach na ekranie głównym</string>
|
||||
<string name="collections_tab_all">Wszystko</string>
|
||||
<string name="collections_tab_combined">Połączone</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -44,6 +44,22 @@
|
|||
<string name="web_manage_collections_title">Manage Collections</string>
|
||||
<string name="web_manage_collections_subtitle">Create and manage your custom collections</string>
|
||||
<string name="web_status_msg_collections_updated">Your collections have been updated on the TV.</string>
|
||||
<string name="web_tab_addons">Addons</string>
|
||||
<string name="web_tab_home_layout">Home Layout</string>
|
||||
<string name="web_tab_collections">Collections</string>
|
||||
<string name="web_btn_enable_all">Enable All</string>
|
||||
<string name="web_btn_disable_all">Disable All</string>
|
||||
<string name="web_btn_show_all">Show All</string>
|
||||
<string name="web_btn_hide_all">Hide All</string>
|
||||
<string name="web_btn_new_collection">+ New Collection</string>
|
||||
<string name="web_btn_export">Export</string>
|
||||
<string name="web_btn_import">Import</string>
|
||||
<string name="web_no_collections">No collections yet</string>
|
||||
<string name="web_badge_collection">Collection</string>
|
||||
<string name="web_import_collections_title">Import Collections</string>
|
||||
<string name="web_import_tab_paste">Paste</string>
|
||||
<string name="web_import_tab_file">File</string>
|
||||
<string name="web_import_tab_url">URL</string>
|
||||
|
||||
<!-- HomeScreen -->
|
||||
<string name="home_no_addons">No addons installed. Add one to get started.</string>
|
||||
|
|
@ -1372,10 +1388,13 @@
|
|||
<string name="cd_loading_logo">Loading logo</string>
|
||||
<string name="cd_move_up">Move up</string>
|
||||
<string name="cd_move_down">Move down</string>
|
||||
<string name="cd_edit">Edit</string>
|
||||
<string name="cd_delete">Delete</string>
|
||||
<string name="cd_qr_code">QR Code</string>
|
||||
<string name="cd_add">Add</string>
|
||||
<string name="cd_refresh">Refresh</string>
|
||||
<string name="cd_remove">Remove</string>
|
||||
<string name="cd_preview">Preview</string>
|
||||
<string name="cd_test">Test</string>
|
||||
<string name="cd_unlink">Unlink</string>
|
||||
<string name="cd_nuvio_logo">NuvioTV</string>
|
||||
|
|
@ -1396,4 +1415,80 @@
|
|||
<string name="debug_generate_description">Debug-generated %1$s item #%2$d</string>
|
||||
<string name="debug_generate_result_failed">Failed: %1$s</string>
|
||||
|
||||
<!-- Collection Management -->
|
||||
<string name="collections_header">Collections</string>
|
||||
<string name="collections_empty">No collections yet. Create one to organize your catalogs.</string>
|
||||
<string name="collections_new">New Collection</string>
|
||||
<string name="collections_import">Import</string>
|
||||
<string name="collections_export_file">Export File</string>
|
||||
<string name="collections_saved_downloads">Saved to Downloads</string>
|
||||
<string name="collections_export_failed">Export failed</string>
|
||||
<string name="collections_import_header">Import Collections</string>
|
||||
<string name="collections_import_description">Import collections from JSON. Catalogs from addons you don\'t have installed will show a warning.</string>
|
||||
<string name="collections_cancel">Cancel</string>
|
||||
<string name="collections_mode_paste">Paste JSON</string>
|
||||
<string name="collections_mode_file">From File</string>
|
||||
<string name="collections_mode_url">From URL</string>
|
||||
<string name="collections_paste_clipboard">Paste from Clipboard</string>
|
||||
<string name="collections_validate">Validate</string>
|
||||
<string name="collections_paste_hint">Paste collections JSON here…</string>
|
||||
<string name="collections_file_description">Reads nuvio-collections.json from the Downloads folder.</string>
|
||||
<string name="collections_load_file">Load File</string>
|
||||
<string name="collections_file_loaded">File loaded (%1$d characters)</string>
|
||||
<string name="collections_fetch_url">Fetch URL</string>
|
||||
<string name="collections_fetching">Fetching…</string>
|
||||
<string name="collections_valid_json">Valid JSON</string>
|
||||
<string name="collections_valid_summary">%1$d collection(s), %2$d folder(s)</string>
|
||||
<string name="collections_confirm_import">Confirm Import</string>
|
||||
<string name="collections_folder_count">%1$d folder(s)</string>
|
||||
|
||||
<!-- Collection Editor -->
|
||||
<string name="collections_editor_row_title">Row Title</string>
|
||||
<string name="collections_editor_save">Save</string>
|
||||
<string name="collections_editor_backdrop">Backdrop Image</string>
|
||||
<string name="collections_editor_pin_above">Pin Above Catalogs</string>
|
||||
<string name="collections_editor_pin_above_desc">Show this collection above all regular home catalogs. Multiple pinned collections follow collection creation order.</string>
|
||||
<string name="collections_editor_focus_glow">Focus Glow</string>
|
||||
<string name="collections_editor_focus_glow_desc">Use the native TV glow on this collection\'s home-screen custom catalog cards</string>
|
||||
<string name="collections_editor_view_mode">View Mode</string>
|
||||
<string name="collections_editor_show_all_tab">Show \"All\" Tab</string>
|
||||
<string name="collections_editor_show_all_tab_desc">Combine all catalogs into one tab</string>
|
||||
<string name="collections_editor_folders">Folders</string>
|
||||
<string name="collections_editor_add_folder">Add Folder</string>
|
||||
<string name="collections_editor_edit_folder">Edit Folder</string>
|
||||
<string name="collections_editor_folder_title">Folder Title</string>
|
||||
<string name="collections_editor_cover">Cover</string>
|
||||
<string name="collections_editor_cover_none">None</string>
|
||||
<string name="collections_editor_cover_emoji">Emoji</string>
|
||||
<string name="collections_editor_cover_image_url">Image URL</string>
|
||||
<string name="collections_editor_focus_gif">Focus GIF</string>
|
||||
<string name="collections_editor_play_gif">Play GIF on Focus</string>
|
||||
<string name="collections_editor_tile_shape">Tile Shape</string>
|
||||
<string name="collections_editor_hide_title">Hide Title</string>
|
||||
<string name="collections_editor_hide_title_desc">Only show the cover image</string>
|
||||
<string name="collections_editor_display">Display</string>
|
||||
<string name="collections_editor_catalogs">Catalogs</string>
|
||||
<string name="collections_editor_add_catalog">Add Catalog</string>
|
||||
<string name="collections_editor_select_catalogs">Select Catalogs</string>
|
||||
<string name="collections_editor_done">Done</string>
|
||||
<string name="collections_editor_choose_emoji">Choose Emoji</string>
|
||||
<string name="collections_editor_back">Back</string>
|
||||
<string name="collections_editor_edit_collection">Edit Collection</string>
|
||||
<string name="collections_editor_catalog_count">%1$d catalog(s)</string>
|
||||
<string name="collections_editor_view_mode_tabs">Tabs</string>
|
||||
<string name="collections_editor_view_mode_rows">Rows</string>
|
||||
<string name="collections_editor_view_mode_follow">Follow Home Layout</string>
|
||||
<string name="collections_editor_shape_poster">Poster</string>
|
||||
<string name="collections_editor_shape_wide">Wide</string>
|
||||
<string name="collections_editor_shape_square">Square</string>
|
||||
<string name="collections_editor_addon_missing">Addon not installed: %1$s</string>
|
||||
<string name="collections_editor_placeholder_name">Collection name</string>
|
||||
<string name="collections_editor_placeholder_backdrop">Backdrop image URL (optional)</string>
|
||||
<string name="collections_editor_placeholder_folder">Folder name</string>
|
||||
<string name="collections_editor_placeholder_gif">Animated GIF URL (plays only while focused)</string>
|
||||
<string name="collections_card_title">Collections</string>
|
||||
<string name="collections_card_subtitle">Group catalogs into folders on your home screen</string>
|
||||
<string name="collections_tab_all">All</string>
|
||||
<string name="collections_tab_combined">Combined</string>
|
||||
|
||||
</resources>
|
||||
|
|
|
|||
Loading…
Reference in a new issue