mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-10 10:06:15 -08:00
fix(collections): one confirmed Clear recently viewed action (#1865)
A playlist's own recently viewed page (/workspace/<provider>/<id>/recent, opened from the dashboard's recently viewed rails) showed two clear buttons: the page's "Clear recently viewed <type>", which confirms through createClearCollectionAction, and a header delete_sweep button that cleared every tab of the playlist at once without asking. Remove the header bulk action end to end, and the unreachable, unconfirmed clear button of the M3U channel list's recent view, so every clear goes through createClearCollectionAction. Drop the two i18n keys only those buttons used. E2E: a shared helper asserts one clear control per page and one confirmation per press; the M3U, Xtream and Stalker recent tests cancel first (rows stay), then clear once on the playlist's own recently viewed page. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
015ea203b7
commit
8fabb88106
39 files changed
+261
-327
No files matched your search
@@ -0,0 +1,8 @@
|
||||
---
|
||||
type: fix
|
||||
area: collections
|
||||
---
|
||||
|
||||
A playlist's recently viewed page now has a single "Clear recently viewed"
|
||||
button, and it always asks before clearing. The second button in the header,
|
||||
which cleared every tab at once without asking, is gone.
|
||||
@@ -32,34 +32,13 @@ import {
|
||||
RANGE_SERVER_ETAG,
|
||||
startDownload,
|
||||
} from './downloads.e2e-support';
|
||||
import { navigateWithinWorkspace } from './workspace-route.e2e-support';
|
||||
|
||||
async function openDownloadsPage(page: Page): Promise<void> {
|
||||
await page.getByRole('button', { name: 'Open downloads' }).click();
|
||||
await page.waitForURL(/\/workspace\/downloads(?:\?.*)?$/);
|
||||
}
|
||||
|
||||
async function navigateWithinWorkspace(
|
||||
page: Page,
|
||||
path: string
|
||||
): Promise<void> {
|
||||
const targetPathname = await page.evaluate((target) => {
|
||||
const targetUrl = new URL(window.location.href);
|
||||
const workspaceIndex = targetUrl.pathname.lastIndexOf('/workspace');
|
||||
const rendererPath =
|
||||
workspaceIndex >= 0
|
||||
? targetUrl.pathname.slice(0, workspaceIndex)
|
||||
: targetUrl.pathname.replace(/\/$/, '');
|
||||
|
||||
targetUrl.pathname = `${rendererPath}${target}`;
|
||||
targetUrl.search = '';
|
||||
targetUrl.hash = '';
|
||||
window.history.pushState(null, '', targetUrl);
|
||||
window.dispatchEvent(new PopStateEvent('popstate'));
|
||||
return targetUrl.pathname;
|
||||
}, path);
|
||||
await page.waitForURL((url) => url.pathname === targetPathname);
|
||||
}
|
||||
|
||||
async function getPlaylistId(page: Page, title: string): Promise<string> {
|
||||
const playlists = await page.evaluate(
|
||||
async () => (await window.electron?.dbGetAppPlaylistMetas?.()) ?? []
|
||||
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
routePlayableStreams,
|
||||
startAndConfirmPlayback,
|
||||
} from './playable-stream-fixture';
|
||||
import { clearRecentItems } from './recent-clear.e2e-support';
|
||||
|
||||
/**
|
||||
* Issue #1458, second report: "all channels disappear after clearing the
|
||||
@@ -160,13 +161,7 @@ test.describe('Live channel rail collapse (#1458)', () => {
|
||||
channelItemByTitle(app.mainWindow, 'Channel Alpha').first()
|
||||
).toBeVisible({ timeout: 20000 });
|
||||
|
||||
await app.mainWindow
|
||||
.getByRole('button', { name: 'Clear recently viewed Live TV' })
|
||||
.click();
|
||||
await app.mainWindow
|
||||
.locator('mat-dialog-container')
|
||||
.getByRole('button', { name: 'Clear', exact: true })
|
||||
.click();
|
||||
await clearRecentItems(app.mainWindow, 'Live TV');
|
||||
await expect(
|
||||
channelItemByTitle(app.mainWindow, 'Channel Alpha')
|
||||
).toHaveCount(0);
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
import type { Locator, Page } from '@playwright/test';
|
||||
import { expect } from './electron-test-fixtures';
|
||||
import { navigateWithinWorkspace } from './workspace-route.e2e-support';
|
||||
|
||||
/**
|
||||
* `/workspace/<provider>/<id>/recent` of the source the page is in: the
|
||||
* source's own recently viewed page, which the dashboard's recently viewed
|
||||
* rails open.
|
||||
*/
|
||||
export function sourceRecentPath(page: Page): string {
|
||||
const source = /\/workspace\/(?:xtreams|stalker|playlists)\/[^/]+/.exec(
|
||||
new URL(page.url()).pathname
|
||||
);
|
||||
if (!source) {
|
||||
throw new Error(`The page is not inside a source: ${page.url()}`);
|
||||
}
|
||||
return `${source[0]}/recent`;
|
||||
}
|
||||
|
||||
export async function openSourceRecent(
|
||||
page: Page,
|
||||
recentPath: string
|
||||
): Promise<void> {
|
||||
await navigateWithinWorkspace(page, recentPath);
|
||||
}
|
||||
|
||||
/** Every control on the page that clears recently viewed items. */
|
||||
function clearRecentControls(page: Page): Locator {
|
||||
return page.getByRole('button', { name: /^Clear recently viewed\b/ });
|
||||
}
|
||||
|
||||
/**
|
||||
* Presses the page's only "Clear recently viewed <type>" control and returns
|
||||
* the confirmation it opens: one control per page, one question per press.
|
||||
*/
|
||||
async function askToClearRecent(
|
||||
page: Page,
|
||||
typeLabel: string
|
||||
): Promise<Locator> {
|
||||
await expect(clearRecentControls(page)).toHaveCount(1);
|
||||
await page
|
||||
.getByRole('button', {
|
||||
name: `Clear recently viewed ${typeLabel}`,
|
||||
exact: true,
|
||||
})
|
||||
.click();
|
||||
|
||||
const dialogs = page.locator('mat-dialog-container');
|
||||
await expect(dialogs).toHaveCount(1);
|
||||
await expect(
|
||||
dialogs.getByRole('heading', {
|
||||
name: `Clear recently viewed ${typeLabel}?`,
|
||||
})
|
||||
).toBeVisible();
|
||||
return dialogs;
|
||||
}
|
||||
|
||||
export async function cancelClearRecentItems(
|
||||
page: Page,
|
||||
typeLabel: string
|
||||
): Promise<void> {
|
||||
const dialog = await askToClearRecent(page, typeLabel);
|
||||
await dialog.getByRole('button', { name: 'Cancel', exact: true }).click();
|
||||
await expect(page.locator('mat-dialog-container')).toHaveCount(0);
|
||||
}
|
||||
|
||||
export async function clearRecentItems(
|
||||
page: Page,
|
||||
typeLabel: string
|
||||
): Promise<void> {
|
||||
const dialog = await askToClearRecent(page, typeLabel);
|
||||
await dialog.getByRole('button', { name: 'Clear', exact: true }).click();
|
||||
await expect(page.locator('mat-dialog-container')).toHaveCount(0);
|
||||
}
|
||||
@@ -45,6 +45,12 @@ import {
|
||||
routePlayableStreams,
|
||||
startAndConfirmPlayback,
|
||||
} from './playable-stream-fixture';
|
||||
import {
|
||||
cancelClearRecentItems,
|
||||
clearRecentItems,
|
||||
openSourceRecent,
|
||||
sourceRecentPath,
|
||||
} from './recent-clear.e2e-support';
|
||||
|
||||
test.describe('Electron Recently Viewed', () => {
|
||||
test('keeps unified live detail open when re-clicking the active M3U recent item', async ({
|
||||
@@ -98,7 +104,7 @@ test.describe('Electron Recently Viewed', () => {
|
||||
}
|
||||
});
|
||||
|
||||
test('@persistence @m3u @electron tracks M3U recent channels in newest-first order, supports all-playlists scope, and persists favorites after restart', async ({
|
||||
test('@persistence @m3u @electron tracks M3U recent channels in newest-first order, supports all-playlists scope, persists favorites after restart, and clears only after one confirmation', async ({
|
||||
dataDir,
|
||||
}) => {
|
||||
const playlistTitle = 'm3u-recent-source.m3u';
|
||||
@@ -165,6 +171,7 @@ test.describe('Electron Recently Viewed', () => {
|
||||
.first()
|
||||
.click();
|
||||
await waitForM3uCatalog(app.mainWindow);
|
||||
const recentPath = sourceRecentPath(app.mainWindow);
|
||||
await openPlaylistRecent(app.mainWindow);
|
||||
await switchUnifiedCollectionScope(app.mainWindow, 'All playlists');
|
||||
await expect
|
||||
@@ -175,6 +182,19 @@ test.describe('Electron Recently Viewed', () => {
|
||||
await expect(
|
||||
channelItemByTitle(app.mainWindow, 'Recent Channel Two').first()
|
||||
).toBeVisible({ timeout: 20000 });
|
||||
|
||||
await openSourceRecent(app.mainWindow, recentPath);
|
||||
await expect
|
||||
.poll(() => visibleLiveTitles(app.mainWindow))
|
||||
.toEqual(['Recent Channel Two', 'Recent Channel One']);
|
||||
await cancelClearRecentItems(app.mainWindow, 'Live TV');
|
||||
await expect
|
||||
.poll(() => visibleLiveTitles(app.mainWindow))
|
||||
.toEqual(['Recent Channel Two', 'Recent Channel One']);
|
||||
await clearRecentItems(app.mainWindow, 'Live TV');
|
||||
await expect
|
||||
.poll(() => visibleLiveTitles(app.mainWindow))
|
||||
.toEqual([]);
|
||||
} finally {
|
||||
await closeElectronApp(app);
|
||||
}
|
||||
@@ -340,6 +360,7 @@ test.describe('Electron Recently Viewed', () => {
|
||||
await openSources(app.mainWindow);
|
||||
await sourceRowByTitle(app.mainWindow, portalTitle).first().click();
|
||||
await waitForXtreamWorkspaceReady(app.mainWindow);
|
||||
const recentPath = sourceRecentPath(app.mainWindow);
|
||||
await openPlaylistRecent(app.mainWindow);
|
||||
await switchUnifiedCollectionScope(app.mainWindow, 'All playlists');
|
||||
await switchUnifiedCollectionContent(app.mainWindow, 'Live TV');
|
||||
@@ -351,7 +372,12 @@ test.describe('Electron Recently Viewed', () => {
|
||||
await switchUnifiedCollectionContent(app.mainWindow, 'Series');
|
||||
await expectVisibleContentCardTitle(app.mainWindow, seriesTitle);
|
||||
|
||||
await openSourceRecent(app.mainWindow, recentPath);
|
||||
await switchUnifiedCollectionContent(app.mainWindow, 'Live TV');
|
||||
await cancelClearRecentItems(app.mainWindow, 'Live TV');
|
||||
await expect(
|
||||
channelItemByTitle(app.mainWindow, liveTitle)
|
||||
).toHaveCount(1);
|
||||
await clearRecentItems(app.mainWindow, 'Live TV');
|
||||
await expect(
|
||||
channelItemByTitle(app.mainWindow, liveTitle)
|
||||
@@ -496,7 +522,7 @@ test.describe('Electron Recently Viewed', () => {
|
||||
}
|
||||
});
|
||||
|
||||
test('@persistence @stalker @electron tracks Stalker live, movie, and series history across playlist and all-playlists scope, and preserves it after restart', async ({
|
||||
test('@persistence @stalker @electron tracks Stalker live, movie, and series history across playlist and all-playlists scope, preserves it after restart, and clears only after one confirmation', async ({
|
||||
dataDir,
|
||||
request,
|
||||
}) => {
|
||||
@@ -593,6 +619,7 @@ test.describe('Electron Recently Viewed', () => {
|
||||
await openSources(app.mainWindow);
|
||||
await sourceRowByTitle(app.mainWindow, portalTitle).first().click();
|
||||
await waitForStalkerCatalog(app.mainWindow);
|
||||
const recentPath = sourceRecentPath(app.mainWindow);
|
||||
await openPlaylistRecent(app.mainWindow);
|
||||
await switchUnifiedCollectionScope(app.mainWindow, 'All playlists');
|
||||
await switchUnifiedCollectionContent(app.mainWindow, 'Live TV');
|
||||
@@ -607,6 +634,18 @@ test.describe('Electron Recently Viewed', () => {
|
||||
await expect(
|
||||
contentCardByTitle(app.mainWindow, seriesTitle)
|
||||
).toHaveCount(1);
|
||||
|
||||
await openSourceRecent(app.mainWindow, recentPath);
|
||||
await switchUnifiedCollectionContent(app.mainWindow, 'Live TV');
|
||||
await cancelClearRecentItems(app.mainWindow, 'Live TV');
|
||||
await expect(
|
||||
channelItemByTitle(app.mainWindow, liveTitle)
|
||||
).toHaveCount(1);
|
||||
await clearRecentItems(app.mainWindow, 'Live TV');
|
||||
await expect(
|
||||
channelItemByTitle(app.mainWindow, liveTitle)
|
||||
).toHaveCount(0);
|
||||
await expectVisibleContentCardTitle(app.mainWindow, movieTitle);
|
||||
} finally {
|
||||
await closeElectronApp(app);
|
||||
}
|
||||
@@ -709,16 +748,6 @@ const xtreamCredentials = {
|
||||
password: defaultXtreamPassword,
|
||||
};
|
||||
|
||||
async function clearRecentItems(page: Page, typeLabel: string): Promise<void> {
|
||||
await page
|
||||
.getByRole('button', { name: `Clear recently viewed ${typeLabel}` })
|
||||
.click();
|
||||
await page
|
||||
.locator('mat-dialog-container')
|
||||
.getByRole('button', { name: 'Clear', exact: true })
|
||||
.click();
|
||||
}
|
||||
|
||||
async function expectUnifiedLiveDetailOpen(
|
||||
page: Page,
|
||||
title: string
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import type { Page } from '@playwright/test';
|
||||
|
||||
/**
|
||||
* Moves the renderer to a workspace route without reloading it, as a router
|
||||
* link would. The packaged renderer lives below its own path, so only the
|
||||
* part from `/workspace` on is replaced.
|
||||
*/
|
||||
export async function navigateWithinWorkspace(
|
||||
page: Page,
|
||||
path: string
|
||||
): Promise<void> {
|
||||
const targetPathname = await page.evaluate((target) => {
|
||||
const targetUrl = new URL(window.location.href);
|
||||
const workspaceIndex = targetUrl.pathname.lastIndexOf('/workspace');
|
||||
const rendererPath =
|
||||
workspaceIndex >= 0
|
||||
? targetUrl.pathname.slice(0, workspaceIndex)
|
||||
: targetUrl.pathname.replace(/\/$/, '');
|
||||
|
||||
targetUrl.pathname = `${rendererPath}${target}`;
|
||||
targetUrl.search = '';
|
||||
targetUrl.hash = '';
|
||||
window.history.pushState(null, '', targetUrl);
|
||||
window.dispatchEvent(new PopStateEvent('popstate'));
|
||||
return targetUrl.pathname;
|
||||
}, path);
|
||||
await page.waitForURL((url) => url.pathname === targetPathname);
|
||||
}
|
||||
@@ -1701,8 +1701,6 @@
|
||||
"CLEAR_COMPLETED_DOWNLOADS_ALL_ARIA": "مسح التنزيلات المكتملة لجميع قوائم التشغيل",
|
||||
"CLEAR_COMPLETED_DOWNLOADS_THIS_PLAYLIST": "مسح التنزيلات المكتملة (قائمة التشغيل هذه)",
|
||||
"CLEAR_COMPLETED_DOWNLOADS_THIS_PLAYLIST_ARIA": "مسح التنزيلات المكتملة لقائمة التشغيل هذه",
|
||||
"CLEAR_RECENTLY_VIEWED_SECTION": "مسح ما تمت مشاهدته مؤخرًا",
|
||||
"CLEAR_RECENTLY_VIEWED_SECTION_ARIA": "مسح العناصر المشاهدة مؤخرًا في هذا القسم",
|
||||
"CLEAR_RECENTLY_VIEWED_TYPE": "مسح ما تمت مشاهدته مؤخرًا من {{type}}",
|
||||
"CLEAR_RECENTLY_VIEWED_DIALOG_TITLE": "مسح ما تمت مشاهدته مؤخرًا من {{type}}؟",
|
||||
"CLEAR_RECENTLY_VIEWED_DIALOG_MESSAGE_PLAYLIST": "ستتم إزالة جميع عناصر {{type}} من قائمة التشغيل هذه من سجل ما تمت مشاهدته مؤخرًا. لا يمكن التراجع عن هذا الإجراء.",
|
||||
|
||||
@@ -1701,8 +1701,6 @@
|
||||
"CLEAR_COMPLETED_DOWNLOADS_ALL_ARIA": "مسح التحميلات المكتملة لجميع قوائم التشغيل",
|
||||
"CLEAR_COMPLETED_DOWNLOADS_THIS_PLAYLIST": "مسح التحميلات المكتملة (هاد قائمة التشغيل)",
|
||||
"CLEAR_COMPLETED_DOWNLOADS_THIS_PLAYLIST_ARIA": "مسح التحميلات المكتملة لهاد قائمة التشغيل",
|
||||
"CLEAR_RECENTLY_VIEWED_SECTION": "مسح المتشافين مؤخراً",
|
||||
"CLEAR_RECENTLY_VIEWED_SECTION_ARIA": "مسح المتشافين مؤخراً لهاد القسم",
|
||||
"CLEAR_RECENTLY_VIEWED_TYPE": "مسح {{type}} المتشافين مؤخراً",
|
||||
"CLEAR_RECENTLY_VIEWED_DIALOG_TITLE": "مسح {{type}} المتشافين مؤخراً؟",
|
||||
"CLEAR_RECENTLY_VIEWED_DIALOG_MESSAGE_PLAYLIST": "جميع عناصر {{type}} من هاد قائمة التشغيل غادي يتحيدو من سجل المتشافين مؤخراً ديالك. هاد العملية ما تقدرش ترجع.",
|
||||
|
||||
@@ -1701,8 +1701,6 @@
|
||||
"CLEAR_COMPLETED_DOWNLOADS_ALL_ARIA": "Ачысціць завершаныя спампаванні для ўсіх плэйлістоў",
|
||||
"CLEAR_COMPLETED_DOWNLOADS_THIS_PLAYLIST": "Ачысціць завершаныя спампаванні (гэты плэйліст)",
|
||||
"CLEAR_COMPLETED_DOWNLOADS_THIS_PLAYLIST_ARIA": "Ачысціць завершаныя спампаванні для гэтага плэйліста",
|
||||
"CLEAR_RECENTLY_VIEWED_SECTION": "Ачысціць нядаўна праглянутае",
|
||||
"CLEAR_RECENTLY_VIEWED_SECTION_ARIA": "Ачысціць нядаўна прагледжанае ў гэтым раздзеле",
|
||||
"CLEAR_RECENTLY_VIEWED_TYPE": "Ачысціць нядаўна праглянутае: {{type}}",
|
||||
"CLEAR_RECENTLY_VIEWED_DIALOG_TITLE": "Ачысціць нядаўна праглянутае: {{type}}?",
|
||||
"CLEAR_RECENTLY_VIEWED_DIALOG_MESSAGE_PLAYLIST": "Усе элементы тыпу {{type}} з гэтага плэйліста будуць выдалены з гісторыі нядаўна праглянутага. Гэта дзеянне нельга адмяніць.",
|
||||
|
||||
@@ -1701,8 +1701,6 @@
|
||||
"CLEAR_COMPLETED_DOWNLOADS_ALL_ARIA": "Abgeschlossene Downloads für alle Playlists löschen",
|
||||
"CLEAR_COMPLETED_DOWNLOADS_THIS_PLAYLIST": "Abgeschlossene Downloads löschen (diese Playlist)",
|
||||
"CLEAR_COMPLETED_DOWNLOADS_THIS_PLAYLIST_ARIA": "Abgeschlossene Downloads für diese Playlist löschen",
|
||||
"CLEAR_RECENTLY_VIEWED_SECTION": "Zuletzt angesehen löschen",
|
||||
"CLEAR_RECENTLY_VIEWED_SECTION_ARIA": "Kürzlich angesehene Elemente für diesen Abschnitt löschen",
|
||||
"CLEAR_RECENTLY_VIEWED_TYPE": "Zuletzt angesehen ({{type}}) löschen",
|
||||
"CLEAR_RECENTLY_VIEWED_DIALOG_TITLE": "Zuletzt angesehen ({{type}}) löschen?",
|
||||
"CLEAR_RECENTLY_VIEWED_DIALOG_MESSAGE_PLAYLIST": "Alle {{type}}-Einträge aus dieser Playlist werden aus dem Verlauf entfernt. Diese Aktion kann nicht rückgängig gemacht werden.",
|
||||
|
||||
@@ -1701,8 +1701,6 @@
|
||||
"CLEAR_COMPLETED_DOWNLOADS_ALL_ARIA": "Εκκαθάριση ολοκληρωμένων λήψεων για όλες τις λίστες αναπαραγωγής",
|
||||
"CLEAR_COMPLETED_DOWNLOADS_THIS_PLAYLIST": "Εκκαθάριση ολοκληρωμένων λήψεων (αυτή η λίστα αναπαραγωγής)",
|
||||
"CLEAR_COMPLETED_DOWNLOADS_THIS_PLAYLIST_ARIA": "Εκκαθάριση ολοκληρωμένων λήψεων για αυτή τη λίστα αναπαραγωγής",
|
||||
"CLEAR_RECENTLY_VIEWED_SECTION": "Εκκαθάριση πρόσφατα προβληθέντων",
|
||||
"CLEAR_RECENTLY_VIEWED_SECTION_ARIA": "Εκκαθάριση πρόσφατων προβολών για αυτή την ενότητα",
|
||||
"CLEAR_RECENTLY_VIEWED_TYPE": "Εκκαθάριση πρόσφατα προβληθέντων {{type}}",
|
||||
"CLEAR_RECENTLY_VIEWED_DIALOG_TITLE": "Εκκαθάριση πρόσφατα προβληθέντων {{type}};",
|
||||
"CLEAR_RECENTLY_VIEWED_DIALOG_MESSAGE_PLAYLIST": "Όλα τα στοιχεία {{type}} από αυτή τη λίστα αναπαραγωγής θα αφαιρεθούν από το ιστορικό πρόσφατα προβληθέντων. Αυτή η ενέργεια δεν μπορεί να αναιρεθεί.",
|
||||
|
||||
@@ -1701,8 +1701,6 @@
|
||||
"CLEAR_COMPLETED_DOWNLOADS_ALL_ARIA": "Clear completed downloads for all playlists",
|
||||
"CLEAR_COMPLETED_DOWNLOADS_THIS_PLAYLIST": "Clear completed downloads (this playlist)",
|
||||
"CLEAR_COMPLETED_DOWNLOADS_THIS_PLAYLIST_ARIA": "Clear completed downloads for this playlist",
|
||||
"CLEAR_RECENTLY_VIEWED_SECTION": "Clear recently viewed",
|
||||
"CLEAR_RECENTLY_VIEWED_SECTION_ARIA": "Clear recently viewed for this section",
|
||||
"CLEAR_RECENTLY_VIEWED_TYPE": "Clear recently viewed {{type}}",
|
||||
"CLEAR_RECENTLY_VIEWED_DIALOG_TITLE": "Clear recently viewed {{type}}?",
|
||||
"CLEAR_RECENTLY_VIEWED_DIALOG_MESSAGE_PLAYLIST": "All {{type}} items from this playlist will be removed from your recently viewed history. This action cannot be undone.",
|
||||
|
||||
@@ -1701,8 +1701,6 @@
|
||||
"CLEAR_COMPLETED_DOWNLOADS_ALL_ARIA": "Borrar descargas completadas de todas las listas",
|
||||
"CLEAR_COMPLETED_DOWNLOADS_THIS_PLAYLIST": "Borrar descargas completadas (esta lista)",
|
||||
"CLEAR_COMPLETED_DOWNLOADS_THIS_PLAYLIST_ARIA": "Borrar descargas completadas de esta lista",
|
||||
"CLEAR_RECENTLY_VIEWED_SECTION": "Borrar vistos recientemente",
|
||||
"CLEAR_RECENTLY_VIEWED_SECTION_ARIA": "Borrar vistos recientemente de esta sección",
|
||||
"CLEAR_RECENTLY_VIEWED_TYPE": "Borrar vistos recientemente {{type}}",
|
||||
"CLEAR_RECENTLY_VIEWED_DIALOG_TITLE": "¿Borrar vistos recientemente {{type}}?",
|
||||
"CLEAR_RECENTLY_VIEWED_DIALOG_MESSAGE_PLAYLIST": "Todos los elementos {{type}} de esta lista se eliminarán de tu historial de vistos recientemente. Esta acción no se puede deshacer.",
|
||||
|
||||
@@ -1701,8 +1701,6 @@
|
||||
"CLEAR_COMPLETED_DOWNLOADS_ALL_ARIA": "Effacer les téléchargements terminés pour toutes les listes de lecture",
|
||||
"CLEAR_COMPLETED_DOWNLOADS_THIS_PLAYLIST": "Effacer les téléchargements terminés (cette liste)",
|
||||
"CLEAR_COMPLETED_DOWNLOADS_THIS_PLAYLIST_ARIA": "Effacer les téléchargements terminés pour cette liste de lecture",
|
||||
"CLEAR_RECENTLY_VIEWED_SECTION": "Effacer les éléments récemment consultés",
|
||||
"CLEAR_RECENTLY_VIEWED_SECTION_ARIA": "Effacer les éléments récemment consultés de cette section",
|
||||
"CLEAR_RECENTLY_VIEWED_TYPE": "Effacer les {{type}} récemment consultés",
|
||||
"CLEAR_RECENTLY_VIEWED_DIALOG_TITLE": "Effacer les {{type}} récemment consultés ?",
|
||||
"CLEAR_RECENTLY_VIEWED_DIALOG_MESSAGE_PLAYLIST": "Tous les éléments {{type}} de cette liste de lecture seront retirés de votre historique récemment consulté. Cette action est irréversible.",
|
||||
|
||||
@@ -1701,8 +1701,6 @@
|
||||
"CLEAR_COMPLETED_DOWNLOADS_ALL_ARIA": "Az összes lejátszási listához tartozó befejezett letöltési bejegyzések eltávolítása",
|
||||
"CLEAR_COMPLETED_DOWNLOADS_THIS_PLAYLIST": "Befejezett letöltési bejegyzések eltávolítása – jelenlegi lejátszási lista",
|
||||
"CLEAR_COMPLETED_DOWNLOADS_THIS_PLAYLIST_ARIA": "A jelenlegi lejátszási listához tartozó befejezett letöltési bejegyzések eltávolítása",
|
||||
"CLEAR_RECENTLY_VIEWED_SECTION": "Megtekintési előzmények törlése",
|
||||
"CLEAR_RECENTLY_VIEWED_SECTION_ARIA": "A szakasz legutóbbi megtekintéseinek törlése",
|
||||
"CLEAR_RECENTLY_VIEWED_TYPE": "Megtekintési előzmények törlése – {{type}}",
|
||||
"CLEAR_RECENTLY_VIEWED_DIALOG_TITLE": "Törli ezeket a megtekintési előzményeket: {{type}}?",
|
||||
"CLEAR_RECENTLY_VIEWED_DIALOG_MESSAGE_PLAYLIST": "A lejátszási lista következő tartalomtípusba tartozó összes eleme törlődik a megtekintési előzményekből: {{type}}. A művelet nem vonható vissza.",
|
||||
|
||||
@@ -1701,8 +1701,6 @@
|
||||
"CLEAR_COMPLETED_DOWNLOADS_ALL_ARIA": "Cancella i download completati per tutte le playlist",
|
||||
"CLEAR_COMPLETED_DOWNLOADS_THIS_PLAYLIST": "Cancella download completati (questa playlist)",
|
||||
"CLEAR_COMPLETED_DOWNLOADS_THIS_PLAYLIST_ARIA": "Cancella i download completati per questa playlist",
|
||||
"CLEAR_RECENTLY_VIEWED_SECTION": "Cancella visti di recente",
|
||||
"CLEAR_RECENTLY_VIEWED_SECTION_ARIA": "Cancella visti di recente per questa sezione",
|
||||
"CLEAR_RECENTLY_VIEWED_TYPE": "Cancella visti di recente: {{type}}",
|
||||
"CLEAR_RECENTLY_VIEWED_DIALOG_TITLE": "Cancellare visti di recente: {{type}}?",
|
||||
"CLEAR_RECENTLY_VIEWED_DIALOG_MESSAGE_PLAYLIST": "Tutti gli elementi {{type}} di questa playlist verranno rimossi dalla cronologia dei visti di recente. L'azione non può essere annullata.",
|
||||
|
||||
@@ -1701,8 +1701,6 @@
|
||||
"CLEAR_COMPLETED_DOWNLOADS_ALL_ARIA": "すべてのプレイリストの完了済みダウンロードをクリア",
|
||||
"CLEAR_COMPLETED_DOWNLOADS_THIS_PLAYLIST": "完了済みダウンロードをクリア(このプレイリスト)",
|
||||
"CLEAR_COMPLETED_DOWNLOADS_THIS_PLAYLIST_ARIA": "このプレイリストの完了済みダウンロードをクリア",
|
||||
"CLEAR_RECENTLY_VIEWED_SECTION": "最近表示した項目をクリア",
|
||||
"CLEAR_RECENTLY_VIEWED_SECTION_ARIA": "このセクションの最近視聴した項目をクリア",
|
||||
"CLEAR_RECENTLY_VIEWED_TYPE": "最近表示した{{type}}をクリア",
|
||||
"CLEAR_RECENTLY_VIEWED_DIALOG_TITLE": "最近表示した{{type}}をクリアしますか?",
|
||||
"CLEAR_RECENTLY_VIEWED_DIALOG_MESSAGE_PLAYLIST": "このプレイリストのすべての{{type}}項目が、最近表示した履歴から削除されます。この操作は取り消せません。",
|
||||
|
||||
@@ -1701,8 +1701,6 @@
|
||||
"CLEAR_COMPLETED_DOWNLOADS_ALL_ARIA": "모든 재생목록의 완료된 다운로드 지우기",
|
||||
"CLEAR_COMPLETED_DOWNLOADS_THIS_PLAYLIST": "완료된 다운로드 지우기 (이 재생목록)",
|
||||
"CLEAR_COMPLETED_DOWNLOADS_THIS_PLAYLIST_ARIA": "이 재생목록의 완료된 다운로드 지우기",
|
||||
"CLEAR_RECENTLY_VIEWED_SECTION": "최근 본 항목 지우기",
|
||||
"CLEAR_RECENTLY_VIEWED_SECTION_ARIA": "이 섹션의 최근 시청 항목 지우기",
|
||||
"CLEAR_RECENTLY_VIEWED_TYPE": "최근 본 {{type}} 지우기",
|
||||
"CLEAR_RECENTLY_VIEWED_DIALOG_TITLE": "최근 본 {{type}}을(를) 지우시겠습니까?",
|
||||
"CLEAR_RECENTLY_VIEWED_DIALOG_MESSAGE_PLAYLIST": "이 재생목록의 모든 {{type}} 항목이 최근 본 기록에서 제거됩니다. 이 작업은 되돌릴 수 없습니다.",
|
||||
|
||||
@@ -1701,8 +1701,6 @@
|
||||
"CLEAR_COMPLETED_DOWNLOADS_ALL_ARIA": "Voltooide downloads wissen voor alle afspeellijsten",
|
||||
"CLEAR_COMPLETED_DOWNLOADS_THIS_PLAYLIST": "Voltooide downloads wissen (deze afspeellijst)",
|
||||
"CLEAR_COMPLETED_DOWNLOADS_THIS_PLAYLIST_ARIA": "Voltooide downloads wissen voor deze afspeellijst",
|
||||
"CLEAR_RECENTLY_VIEWED_SECTION": "Recent bekeken wissen",
|
||||
"CLEAR_RECENTLY_VIEWED_SECTION_ARIA": "Recent bekeken voor deze sectie wissen",
|
||||
"CLEAR_RECENTLY_VIEWED_TYPE": "Recent bekeken {{type}} wissen",
|
||||
"CLEAR_RECENTLY_VIEWED_DIALOG_TITLE": "Recent bekeken {{type}} wissen?",
|
||||
"CLEAR_RECENTLY_VIEWED_DIALOG_MESSAGE_PLAYLIST": "Alle {{type}}-items uit deze afspeellijst worden verwijderd uit je recent bekeken-geschiedenis. Deze actie kan niet ongedaan worden gemaakt.",
|
||||
|
||||
@@ -1701,8 +1701,6 @@
|
||||
"CLEAR_COMPLETED_DOWNLOADS_ALL_ARIA": "Wyczyść ukończone pobrania dla wszystkich list odtwarzania",
|
||||
"CLEAR_COMPLETED_DOWNLOADS_THIS_PLAYLIST": "Wyczyść ukończone pobrania (ta lista odtwarzania)",
|
||||
"CLEAR_COMPLETED_DOWNLOADS_THIS_PLAYLIST_ARIA": "Wyczyść ukończone pobrania dla tej listy odtwarzania",
|
||||
"CLEAR_RECENTLY_VIEWED_SECTION": "Wyczyść ostatnio oglądane",
|
||||
"CLEAR_RECENTLY_VIEWED_SECTION_ARIA": "Wyczyść ostatnio oglądane dla tej sekcji",
|
||||
"CLEAR_RECENTLY_VIEWED_TYPE": "Wyczyść ostatnio oglądane {{type}}",
|
||||
"CLEAR_RECENTLY_VIEWED_DIALOG_TITLE": "Wyczyścić ostatnio oglądane {{type}}?",
|
||||
"CLEAR_RECENTLY_VIEWED_DIALOG_MESSAGE_PLAYLIST": "Wszystkie elementy {{type}} z tej listy odtwarzania zostaną usunięte z historii ostatnio oglądanych. Tej akcji nie można cofnąć.",
|
||||
|
||||
@@ -1701,8 +1701,6 @@
|
||||
"CLEAR_COMPLETED_DOWNLOADS_ALL_ARIA": "Limpar downloads concluídos para todas as playlists",
|
||||
"CLEAR_COMPLETED_DOWNLOADS_THIS_PLAYLIST": "Limpar downloads concluídos (esta playlist)",
|
||||
"CLEAR_COMPLETED_DOWNLOADS_THIS_PLAYLIST_ARIA": "Limpar downloads concluídos para esta playlist",
|
||||
"CLEAR_RECENTLY_VIEWED_SECTION": "Limpar visualizados recentemente",
|
||||
"CLEAR_RECENTLY_VIEWED_SECTION_ARIA": "Limpar assistidos recentemente nesta seção",
|
||||
"CLEAR_RECENTLY_VIEWED_TYPE": "Limpar visualizados recentemente {{type}}",
|
||||
"CLEAR_RECENTLY_VIEWED_DIALOG_TITLE": "Limpar visualizados recentemente {{type}}?",
|
||||
"CLEAR_RECENTLY_VIEWED_DIALOG_MESSAGE_PLAYLIST": "Todos os itens de {{type}} desta playlist serão removidos do seu histórico de visualizados recentemente. Esta ação não pode ser desfeita.",
|
||||
|
||||
@@ -1701,8 +1701,6 @@
|
||||
"CLEAR_COMPLETED_DOWNLOADS_ALL_ARIA": "Очистить завершенные загрузки для всех плейлистов",
|
||||
"CLEAR_COMPLETED_DOWNLOADS_THIS_PLAYLIST": "Очистить завершенные загрузки (этот плейлист)",
|
||||
"CLEAR_COMPLETED_DOWNLOADS_THIS_PLAYLIST_ARIA": "Очистить завершенные загрузки для этого плейлиста",
|
||||
"CLEAR_RECENTLY_VIEWED_SECTION": "Очистить недавно просмотренное",
|
||||
"CLEAR_RECENTLY_VIEWED_SECTION_ARIA": "Очистить недавно просмотренное для этого раздела",
|
||||
"CLEAR_RECENTLY_VIEWED_TYPE": "Очистить недавно просмотренное ({{type}})",
|
||||
"CLEAR_RECENTLY_VIEWED_DIALOG_TITLE": "Очистить недавно просмотренное ({{type}})?",
|
||||
"CLEAR_RECENTLY_VIEWED_DIALOG_MESSAGE_PLAYLIST": "Все элементы «{{type}}» из этого плейлиста будут удалены из истории просмотров. Это действие нельзя отменить.",
|
||||
|
||||
@@ -1701,8 +1701,6 @@
|
||||
"CLEAR_COMPLETED_DOWNLOADS_ALL_ARIA": "Tüm oynatma listeleri için tamamlanan indirmeleri temizle",
|
||||
"CLEAR_COMPLETED_DOWNLOADS_THIS_PLAYLIST": "Tamamlanan indirmeleri temizle (bu oynatma listesi)",
|
||||
"CLEAR_COMPLETED_DOWNLOADS_THIS_PLAYLIST_ARIA": "Bu oynatma listesi için tamamlanan indirmeleri temizle",
|
||||
"CLEAR_RECENTLY_VIEWED_SECTION": "Son izlenenleri temizle",
|
||||
"CLEAR_RECENTLY_VIEWED_SECTION_ARIA": "Bu bölüm için son görüntülenenleri temizle",
|
||||
"CLEAR_RECENTLY_VIEWED_TYPE": "Son izlenen {{type}} öğelerini temizle",
|
||||
"CLEAR_RECENTLY_VIEWED_DIALOG_TITLE": "Son izlenen {{type}} öğeleri temizlensin mi?",
|
||||
"CLEAR_RECENTLY_VIEWED_DIALOG_MESSAGE_PLAYLIST": "Bu oynatma listesindeki tüm {{type}} öğeleri son izlenenler geçmişinizden kaldırılacaktır. Bu işlem geri alınamaz.",
|
||||
|
||||
@@ -1701,8 +1701,6 @@
|
||||
"CLEAR_COMPLETED_DOWNLOADS_ALL_ARIA": "清除所有播放列表中已完成的下载",
|
||||
"CLEAR_COMPLETED_DOWNLOADS_THIS_PLAYLIST": "清除已完成的下载(本播放列表)",
|
||||
"CLEAR_COMPLETED_DOWNLOADS_THIS_PLAYLIST_ARIA": "清除本播放列表中已完成的下载",
|
||||
"CLEAR_RECENTLY_VIEWED_SECTION": "清除最近观看",
|
||||
"CLEAR_RECENTLY_VIEWED_SECTION_ARIA": "清除当前区域的最近观看",
|
||||
"CLEAR_RECENTLY_VIEWED_TYPE": "清除最近观看的{{type}}",
|
||||
"CLEAR_RECENTLY_VIEWED_DIALOG_TITLE": "清除最近观看的{{type}}?",
|
||||
"CLEAR_RECENTLY_VIEWED_DIALOG_MESSAGE_PLAYLIST": "本播放列表中所有的{{type}}项目将从您的最近观看历史中移除。此操作无法撤销。",
|
||||
|
||||
@@ -1701,8 +1701,6 @@
|
||||
"CLEAR_COMPLETED_DOWNLOADS_ALL_ARIA": "清除所有播放清單中已完成的下載",
|
||||
"CLEAR_COMPLETED_DOWNLOADS_THIS_PLAYLIST": "清除已完成的下載(此播放清單)",
|
||||
"CLEAR_COMPLETED_DOWNLOADS_THIS_PLAYLIST_ARIA": "清除此播放清單中已完成的下載",
|
||||
"CLEAR_RECENTLY_VIEWED_SECTION": "清除最近觀看",
|
||||
"CLEAR_RECENTLY_VIEWED_SECTION_ARIA": "清除此區段的最近觀看記錄",
|
||||
"CLEAR_RECENTLY_VIEWED_TYPE": "清除最近觀看的{{type}}",
|
||||
"CLEAR_RECENTLY_VIEWED_DIALOG_TITLE": "清除最近觀看的{{type}}?",
|
||||
"CLEAR_RECENTLY_VIEWED_DIALOG_MESSAGE_PLAYLIST": "此播放清單中所有的{{type}}項目都將從您的最近觀看記錄中移除。此操作無法復原。",
|
||||
|
||||
@@ -842,6 +842,13 @@ per-theme tokens in `apps/web/src/_settings-theme.scss`.
|
||||
is "Cancel …", pass `cancelLabel` "Close" so the two buttons do not read
|
||||
alike. `theme-tokens.e2e.ts` checks the label and the error fill in both
|
||||
themes.
|
||||
- **One control per action.** A page offers one control for a destructive
|
||||
action. Clearing a favorites or recently viewed tab goes through
|
||||
`createClearCollectionAction` (`unified-collection-clear-action.ts` in
|
||||
`portal/shared/ui`), from the collection page's button and from the command
|
||||
palette alike; the shell
|
||||
header does not repeat it. `recent.e2e.ts` checks the single control and
|
||||
the single confirmation on M3U, Xtream and Stalker recently viewed pages.
|
||||
|
||||
## Forms
|
||||
|
||||
|
||||
@@ -139,7 +139,8 @@ services:
|
||||
3. `WorkspaceShellSearchSyncService` owns the search query signals, debounced
|
||||
application, provider-store synchronization, and query-param sync.
|
||||
4. `WorkspaceShellHeaderService` owns playlist title/subtitle, account/info
|
||||
actions, refresh action state, and recent-items bulk cleanup.
|
||||
actions, and refresh action state. It has no clear action: clearing
|
||||
recently viewed or favorites belongs to the collection page.
|
||||
5. `WorkspaceShellCommandPaletteService` owns command-palette dialog lifecycle
|
||||
and recent-command recording.
|
||||
6. `WorkspaceShellXtreamImportService` owns Xtream import/refresh overlay
|
||||
|
||||
+92
@@ -0,0 +1,92 @@
|
||||
import { signal } from '@angular/core';
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import {
|
||||
CollectionMode,
|
||||
UnifiedCollectionDataService,
|
||||
} from '@iptvnator/portal/shared/data-access';
|
||||
import { UnifiedCollectionItem } from '@iptvnator/portal/shared/util';
|
||||
import { ConfirmDialogData, DialogService } from '@iptvnator/ui/components';
|
||||
import { createClearCollectionAction } from './unified-collection-clear-action';
|
||||
|
||||
const recentItem: UnifiedCollectionItem = {
|
||||
uid: 'm3u::playlist-1::one',
|
||||
name: 'Channel One',
|
||||
contentType: 'live',
|
||||
sourceType: 'm3u',
|
||||
playlistId: 'playlist-1',
|
||||
playlistName: 'Playlist One',
|
||||
streamUrl: 'https://example.com/one.m3u8',
|
||||
};
|
||||
|
||||
describe('createClearCollectionAction', () => {
|
||||
const openConfirmDialog = jest.fn();
|
||||
const data = {
|
||||
clearFavorites: jest.fn().mockResolvedValue(undefined),
|
||||
removeRecentItemsBatch: jest.fn(),
|
||||
};
|
||||
const dropCurrentType = jest.fn();
|
||||
|
||||
function createAction(
|
||||
mode: CollectionMode,
|
||||
items: UnifiedCollectionItem[] = [recentItem]
|
||||
) {
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
{ provide: DialogService, useValue: { openConfirmDialog } },
|
||||
{
|
||||
provide: TranslateService,
|
||||
useValue: { instant: (key: string) => key },
|
||||
},
|
||||
],
|
||||
});
|
||||
return TestBed.runInInjectionContext(() =>
|
||||
createClearCollectionAction({
|
||||
mode: signal(mode),
|
||||
items: signal(items),
|
||||
typeLabelKey: signal('PORTALS.LIVE_TV'),
|
||||
isPlaylistScope: () => true,
|
||||
data: data as unknown as UnifiedCollectionDataService,
|
||||
dropCurrentType,
|
||||
reload: jest.fn().mockResolvedValue(undefined),
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
function lastDialog(): ConfirmDialogData {
|
||||
return openConfirmDialog.mock.calls.at(-1)?.[0];
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('asks once, as a destructive Clear, before removing recently viewed rows', () => {
|
||||
createAction('recent').run();
|
||||
|
||||
expect(openConfirmDialog).toHaveBeenCalledTimes(1);
|
||||
expect(lastDialog()).toMatchObject({
|
||||
title: 'WORKSPACE.SHELL.CLEAR_RECENTLY_VIEWED_DIALOG_TITLE',
|
||||
message:
|
||||
'WORKSPACE.SHELL.CLEAR_RECENTLY_VIEWED_DIALOG_MESSAGE_PLAYLIST',
|
||||
confirmLabel: 'CLEAR',
|
||||
tone: 'destructive',
|
||||
});
|
||||
expect(dropCurrentType).not.toHaveBeenCalled();
|
||||
expect(data.removeRecentItemsBatch).not.toHaveBeenCalled();
|
||||
|
||||
lastDialog().onConfirm();
|
||||
|
||||
expect(dropCurrentType).toHaveBeenCalledTimes(1);
|
||||
expect(data.removeRecentItemsBatch).toHaveBeenCalledTimes(1);
|
||||
expect(data.removeRecentItemsBatch).toHaveBeenCalledWith([recentItem]);
|
||||
expect(data.clearFavorites).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('asks nothing when the tab on screen has no rows', () => {
|
||||
createAction('recent', []).run();
|
||||
|
||||
expect(openConfirmDialog).not.toHaveBeenCalled();
|
||||
expect(data.removeRecentItemsBatch).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
-10
@@ -13,16 +13,6 @@
|
||||
<header class="context-header">
|
||||
<div class="context-header__top">
|
||||
<h2>{{ viewTitle() | translate }}</h2>
|
||||
@if (
|
||||
activeView() === 'recent' && recentChannelItems().length > 0
|
||||
) {
|
||||
<button mat-button (click)="clearRecentChannels()">
|
||||
{{
|
||||
'WORKSPACE.SHELL.CLEAR_RECENTLY_VIEWED_SECTION'
|
||||
| translate
|
||||
}}
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</header>
|
||||
<div class="context-divider"></div>
|
||||
|
||||
@@ -14,7 +14,6 @@ import {
|
||||
untracked,
|
||||
} from '@angular/core';
|
||||
import { toObservable, toSignal } from '@angular/core/rxjs-interop';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { ActivatedRoute, NavigationEnd, Router } from '@angular/router';
|
||||
import { Store } from '@ngrx/store';
|
||||
@@ -111,7 +110,6 @@ const EPG_AVAILABILITY_REFRESH_DEBOUNCE_MS = 2000;
|
||||
CommonModule,
|
||||
FavoritesViewComponent,
|
||||
GroupsViewComponent,
|
||||
MatButtonModule,
|
||||
MatIconModule,
|
||||
RecentViewComponent,
|
||||
TranslatePipe,
|
||||
@@ -720,26 +718,6 @@ export class ChannelListContainerComponent implements OnInit, OnDestroy {
|
||||
);
|
||||
}
|
||||
|
||||
async clearRecentChannels(): Promise<void> {
|
||||
const playlistId = this.resolvedPlaylistId();
|
||||
if (!playlistId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const updatedPlaylist = await firstValueFrom(
|
||||
this.playlistsService.clearM3uRecentlyViewed(playlistId)
|
||||
);
|
||||
|
||||
this.store.dispatch(
|
||||
PlaylistActions.updatePlaylistMeta({
|
||||
playlist: {
|
||||
_id: playlistId,
|
||||
recentlyViewed: updatedPlaylist?.recentlyViewed ?? [],
|
||||
} as PlaylistMeta,
|
||||
}) as any
|
||||
);
|
||||
}
|
||||
|
||||
onSidebarWidthRequested(width: number): void {
|
||||
this.sidebarWidthRequested.emit(width);
|
||||
}
|
||||
|
||||
+1
-14
@@ -121,7 +121,7 @@
|
||||
playlist is a global action, not playlist-scoped, so it must
|
||||
remain reachable (e.g. a fresh user configuring the app before
|
||||
importing their first source). Only the per-playlist shortcut
|
||||
and bulk actions are hidden on Settings. -->
|
||||
is hidden on Settings. -->
|
||||
<div class="header-actions header-actions--leading">
|
||||
<button
|
||||
type="button"
|
||||
@@ -201,19 +201,6 @@
|
||||
<mat-icon>{{ action.icon }}</mat-icon>
|
||||
</button>
|
||||
}
|
||||
@if (headerBulkAction(); as bulk) {
|
||||
<button
|
||||
type="button"
|
||||
class="header-shortcut header-bulk-action"
|
||||
mat-icon-button
|
||||
[disabled]="bulk.disabled"
|
||||
(click)="onHeaderBulkActionRequested()"
|
||||
[matTooltip]="bulk.tooltip"
|
||||
[attr.aria-label]="bulk.ariaLabel"
|
||||
>
|
||||
<mat-icon>{{ bulk.icon }}</mat-icon>
|
||||
</button>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
|
||||
|
||||
+2
-11
@@ -17,10 +17,7 @@ import {
|
||||
WorkspaceHeaderAction,
|
||||
} from '@iptvnator/portal/shared/util';
|
||||
import { PlaylistMeta } from '@iptvnator/shared/interfaces';
|
||||
import {
|
||||
WorkspaceHeaderBulkAction,
|
||||
WorkspaceHeaderSidebarToggle,
|
||||
} from '../../services/helpers/workspace-shell-constants';
|
||||
import { WorkspaceHeaderSidebarToggle } from '../../services/helpers/workspace-shell-constants';
|
||||
|
||||
@Component({
|
||||
selector: 'app-workspace-shell-header',
|
||||
@@ -61,7 +58,6 @@ export class WorkspaceShellHeaderComponent {
|
||||
readonly searchScopeLabel = input('');
|
||||
readonly searchStatusLabel = input('');
|
||||
readonly headerShortcut = input<WorkspaceHeaderAction | null>(null);
|
||||
readonly headerBulkAction = input<WorkspaceHeaderBulkAction | null>(null);
|
||||
readonly headerSidebarToggle = input<WorkspaceHeaderSidebarToggle | null>(
|
||||
null
|
||||
);
|
||||
@@ -76,7 +72,7 @@ export class WorkspaceShellHeaderComponent {
|
||||
);
|
||||
/**
|
||||
* When true the playlist switcher + the "+ Add source" / refresh /
|
||||
* bulk-action buttons are hidden — those controls scope to a
|
||||
* shortcut buttons are hidden — those controls scope to a
|
||||
* playlist, but Settings is a global page, so leaving them visible
|
||||
* implies (falsely) that switching the playlist changes which
|
||||
* settings you're editing. Driven from the shell facade's existing
|
||||
@@ -131,7 +127,6 @@ export class WorkspaceShellHeaderComponent {
|
||||
readonly shortcutsRequested = output<void>();
|
||||
readonly addPlaylistRequested = output<void>();
|
||||
readonly headerShortcutRequested = output<void>();
|
||||
readonly headerBulkActionRequested = output<void>();
|
||||
readonly headerSidebarToggleRequested = output<void>();
|
||||
readonly parentalLockToggleRequested = output<void>();
|
||||
readonly refreshPlaylistRequested = output<void>();
|
||||
@@ -217,10 +212,6 @@ export class WorkspaceShellHeaderComponent {
|
||||
this.headerShortcutRequested.emit();
|
||||
}
|
||||
|
||||
onHeaderBulkActionRequested(): void {
|
||||
this.headerBulkActionRequested.emit();
|
||||
}
|
||||
|
||||
onHeaderSidebarToggleRequested(): void {
|
||||
this.headerSidebarToggleRequested.emit();
|
||||
}
|
||||
|
||||
-12
@@ -15,11 +15,6 @@ export const SEARCH_SETTINGS_PLACEHOLDER =
|
||||
export const SEARCH_LOADED_ONLY_STATUS =
|
||||
'WORKSPACE.SHELL.SEARCH_STATUS_LOADED_ONLY';
|
||||
|
||||
export const CLEAR_RECENTLY_VIEWED_TOOLTIP =
|
||||
'WORKSPACE.SHELL.CLEAR_RECENTLY_VIEWED_SECTION';
|
||||
export const CLEAR_RECENTLY_VIEWED_ARIA =
|
||||
'WORKSPACE.SHELL.CLEAR_RECENTLY_VIEWED_SECTION_ARIA';
|
||||
|
||||
export const RAIL_TOOLTIP_KEYS: Readonly<
|
||||
Partial<Record<PortalRailSection, string>>
|
||||
> = {
|
||||
@@ -39,13 +34,6 @@ export const RAIL_TOOLTIP_KEYS: Readonly<
|
||||
|
||||
export type XtreamImportPhaseTone = 'remote' | 'local' | null;
|
||||
|
||||
export interface WorkspaceHeaderBulkAction {
|
||||
icon: string;
|
||||
tooltip: string;
|
||||
ariaLabel: string;
|
||||
disabled: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Header toggle for the live-channel rail of the current route. It exists in
|
||||
* both rail states so the control never moves: inside the rail the chevron
|
||||
|
||||
-12
@@ -65,18 +65,6 @@ export function syncSearchQueryParam(
|
||||
return true;
|
||||
}
|
||||
|
||||
export function bumpRefreshQueryParam(router: Router, currentUrl: string): void {
|
||||
const routePath = getRoutePath(currentUrl);
|
||||
const queryParams = {
|
||||
...router.parseUrl(currentUrl).queryParams,
|
||||
refresh: Date.now().toString(),
|
||||
};
|
||||
|
||||
const queryString = toQueryString(queryParams);
|
||||
const nextUrl = queryString ? `${routePath}?${queryString}` : routePath;
|
||||
void router.navigateByUrl(nextUrl, { replaceUrl: true });
|
||||
}
|
||||
|
||||
export function getProviderFromPlaylist(playlist: {
|
||||
serverUrl?: string;
|
||||
macAddress?: string;
|
||||
|
||||
+2
-112
@@ -1,10 +1,9 @@
|
||||
import { computed, inject, Injectable } from '@angular/core';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { Router } from '@angular/router';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { toSignal } from '@angular/core/rxjs-interop';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { firstValueFrom, startWith } from 'rxjs';
|
||||
import { startWith } from 'rxjs';
|
||||
import {
|
||||
PlaylistInfoComponent,
|
||||
PlaylistRefreshActionService,
|
||||
@@ -16,8 +15,6 @@ import {
|
||||
WorkspaceHeaderContextService,
|
||||
} from '@iptvnator/portal/shared/util';
|
||||
import { XtreamStore } from '@iptvnator/portal/xtream/data-access';
|
||||
import { PlaylistsService } from '@iptvnator/services';
|
||||
import { PlaylistActions } from '@iptvnator/m3u-state';
|
||||
import {
|
||||
isStalkerAccountPlaylist,
|
||||
isXtreamAccountPlaylist,
|
||||
@@ -25,24 +22,15 @@ import {
|
||||
} from '@iptvnator/shared/interfaces';
|
||||
import {
|
||||
WorkspaceAccountInfoData,
|
||||
WorkspacePortalContext,
|
||||
WORKSPACE_SHELL_ACTIONS,
|
||||
} from '@iptvnator/workspace/shell/util';
|
||||
import {
|
||||
CLEAR_RECENTLY_VIEWED_ARIA,
|
||||
CLEAR_RECENTLY_VIEWED_TOOLTIP,
|
||||
WorkspaceHeaderBulkAction,
|
||||
WorkspaceHeaderSidebarToggle,
|
||||
} from './helpers/workspace-shell-constants';
|
||||
import { bumpRefreshQueryParam } from './helpers/workspace-shell-route-utils';
|
||||
import { WorkspaceHeaderSidebarToggle } from './helpers/workspace-shell-constants';
|
||||
import { WorkspaceShellRouteStateService } from './workspace-shell-route-state.service';
|
||||
|
||||
@Injectable()
|
||||
export class WorkspaceShellHeaderService {
|
||||
private readonly router = inject(Router);
|
||||
private readonly store = inject(Store);
|
||||
private readonly xtreamStore = inject(XtreamStore);
|
||||
private readonly playlistsService = inject(PlaylistsService);
|
||||
private readonly workspaceActions = inject(WORKSPACE_SHELL_ACTIONS);
|
||||
private readonly translate = inject(TranslateService);
|
||||
private readonly dialog = inject(MatDialog);
|
||||
@@ -128,33 +116,6 @@ export class WorkspaceShellHeaderService {
|
||||
)
|
||||
);
|
||||
readonly isRefreshingPlaylist = this.playlistRefreshAction.isRefreshing;
|
||||
readonly headerBulkAction = computed<WorkspaceHeaderBulkAction | null>(
|
||||
() => {
|
||||
this.languageTick();
|
||||
|
||||
const context = this.routeState.currentContext();
|
||||
const section = this.routeState.currentSection();
|
||||
|
||||
if (!context || section !== 'recent') {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (
|
||||
context.provider !== 'xtreams' &&
|
||||
context.provider !== 'stalker' &&
|
||||
context.provider !== 'playlists'
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
icon: 'delete_sweep',
|
||||
tooltip: this.translateText(CLEAR_RECENTLY_VIEWED_TOOLTIP),
|
||||
ariaLabel: this.translateText(CLEAR_RECENTLY_VIEWED_ARIA),
|
||||
disabled: this.isRecentCleanupDisabled(context.provider),
|
||||
};
|
||||
}
|
||||
);
|
||||
readonly playlistSubtitle = computed(() => {
|
||||
this.languageTick();
|
||||
|
||||
@@ -200,61 +161,6 @@ export class WorkspaceShellHeaderService {
|
||||
this.workspaceActions.openGlobalRecent();
|
||||
}
|
||||
|
||||
async runHeaderBulkAction(): Promise<void> {
|
||||
const context = this.routeState.currentContext();
|
||||
const section = this.routeState.currentSection();
|
||||
|
||||
if (!context || section !== 'recent') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (context.provider === 'xtreams') {
|
||||
this.xtreamStore.clearRecentItems({ id: context.playlistId });
|
||||
return;
|
||||
}
|
||||
|
||||
if (context.provider === 'stalker') {
|
||||
const updatedPlaylist = await firstValueFrom(
|
||||
this.playlistsService.clearPortalRecentlyViewed(
|
||||
context.playlistId
|
||||
)
|
||||
);
|
||||
this.store.dispatch(
|
||||
PlaylistActions.updatePlaylistMeta({
|
||||
playlist: {
|
||||
_id: context.playlistId,
|
||||
recentlyViewed: updatedPlaylist?.recentlyViewed ?? [],
|
||||
} as PlaylistMeta,
|
||||
})
|
||||
);
|
||||
bumpRefreshQueryParam(
|
||||
this.router,
|
||||
this.routeState.currentUrl()
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (context.provider === 'playlists') {
|
||||
const updatedPlaylist = await firstValueFrom(
|
||||
this.playlistsService.clearM3uRecentlyViewed(
|
||||
context.playlistId
|
||||
)
|
||||
);
|
||||
this.store.dispatch(
|
||||
PlaylistActions.updatePlaylistMeta({
|
||||
playlist: {
|
||||
_id: context.playlistId,
|
||||
recentlyViewed: updatedPlaylist?.recentlyViewed ?? [],
|
||||
} as PlaylistMeta,
|
||||
})
|
||||
);
|
||||
bumpRefreshQueryParam(
|
||||
this.router,
|
||||
this.routeState.currentUrl()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
toggleLiveSidebar(): void {
|
||||
const surface = this.liveSidebarSurface();
|
||||
if (surface) {
|
||||
@@ -343,22 +249,6 @@ export class WorkspaceShellHeaderService {
|
||||
this.playlistRefreshAction.refresh(playlist);
|
||||
}
|
||||
|
||||
private isRecentCleanupDisabled(
|
||||
provider: WorkspacePortalContext['provider']
|
||||
): boolean {
|
||||
if (provider === 'xtreams') {
|
||||
return this.xtreamStore.recentItems().length === 0;
|
||||
}
|
||||
|
||||
if (provider === 'playlists') {
|
||||
return (
|
||||
this.routeState.activePlaylist()?.recentlyViewed?.length ?? 0
|
||||
) === 0;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private translateText(
|
||||
key: string,
|
||||
params?: Record<string, string | number>
|
||||
|
||||
-37
@@ -19,7 +19,6 @@ import { StalkerStore } from '@iptvnator/portal/stalker/data-access';
|
||||
import { XtreamStore } from '@iptvnator/portal/xtream/data-access';
|
||||
import {
|
||||
DownloadsService,
|
||||
PlaylistsService,
|
||||
RuntimeCapabilitiesService,
|
||||
SettingsStore,
|
||||
} from '@iptvnator/services';
|
||||
@@ -138,10 +137,6 @@ describe('WorkspaceShellFacade', () => {
|
||||
isActive: jest.Mock;
|
||||
lastSuccessfulNavigation: () => { trigger: string };
|
||||
};
|
||||
let playlistsService: {
|
||||
clearPortalRecentlyViewed: jest.Mock;
|
||||
clearM3uRecentlyViewed: jest.Mock;
|
||||
};
|
||||
let workspaceActions: {
|
||||
openAddPlaylistDialog: jest.Mock;
|
||||
openGlobalSearch: jest.Mock;
|
||||
@@ -225,14 +220,6 @@ describe('WorkspaceShellFacade', () => {
|
||||
isActive: jest.fn(),
|
||||
lastSuccessfulNavigation: () => ({ trigger: navigationTrigger }),
|
||||
};
|
||||
playlistsService = {
|
||||
clearPortalRecentlyViewed: jest
|
||||
.fn()
|
||||
.mockReturnValue(of({ recentlyViewed: [] })),
|
||||
clearM3uRecentlyViewed: jest
|
||||
.fn()
|
||||
.mockReturnValue(of({ recentlyViewed: [] })),
|
||||
};
|
||||
workspaceActions = {
|
||||
openAddPlaylistDialog: jest.fn(),
|
||||
openGlobalSearch: jest.fn(),
|
||||
@@ -328,10 +315,6 @@ describe('WorkspaceShellFacade', () => {
|
||||
provide: RuntimeCapabilitiesService,
|
||||
useValue: runtime,
|
||||
},
|
||||
{
|
||||
provide: PlaylistsService,
|
||||
useValue: playlistsService,
|
||||
},
|
||||
{
|
||||
provide: DownloadsService,
|
||||
useValue: {
|
||||
@@ -647,26 +630,6 @@ describe('WorkspaceShellFacade', () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it('clears stalker recent items and refreshes the route', async () => {
|
||||
facade.currentUrl.set('/workspace/stalker/pl-1/recent');
|
||||
router.navigateByUrl.mockClear();
|
||||
|
||||
await facade.runHeaderBulkAction();
|
||||
|
||||
expect(playlistsService.clearPortalRecentlyViewed).toHaveBeenCalledWith(
|
||||
'pl-1'
|
||||
);
|
||||
expect(storeDispatch).toHaveBeenCalled();
|
||||
expect(router.navigateByUrl).toHaveBeenCalledWith(
|
||||
expect.stringMatching(
|
||||
/^\/workspace\/stalker\/pl-1\/recent\?refresh=/
|
||||
),
|
||||
{
|
||||
replaceUrl: true,
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('exposes loaded-only status for stalker itv searches', () => {
|
||||
facade.currentUrl.set('/workspace/stalker/pl-1/itv?q=cnn');
|
||||
searchSync.syncSearchFromRoute();
|
||||
|
||||
-7
@@ -28,8 +28,6 @@ import { WorkspaceShellSearchService } from './workspace-shell-search.service';
|
||||
import { WorkspaceShellXtreamImportService } from './workspace-shell-xtream-import.service';
|
||||
import { WorkspaceShellContextDrawerService } from '@iptvnator/workspace/shell/util';
|
||||
|
||||
export type { WorkspaceHeaderBulkAction } from './helpers/workspace-shell-constants';
|
||||
|
||||
@Injectable()
|
||||
export class WorkspaceShellFacade {
|
||||
private readonly router = inject(Router);
|
||||
@@ -134,7 +132,6 @@ export class WorkspaceShellFacade {
|
||||
readonly canOpenAccountInfo = this.header.canOpenAccountInfo;
|
||||
readonly canRefreshPlaylist = this.header.canRefreshPlaylist;
|
||||
readonly isRefreshingPlaylist = this.header.isRefreshingPlaylist;
|
||||
readonly headerBulkAction = this.header.headerBulkAction;
|
||||
readonly headerSidebarToggle = this.header.headerSidebarToggle;
|
||||
readonly playlistSubtitle = this.header.playlistSubtitle;
|
||||
readonly activeDownloadsCount = computed(() =>
|
||||
@@ -210,10 +207,6 @@ export class WorkspaceShellFacade {
|
||||
);
|
||||
}
|
||||
|
||||
runHeaderBulkAction(): Promise<void> {
|
||||
return this.header.runHeaderBulkAction();
|
||||
}
|
||||
|
||||
toggleLiveSidebar(): void {
|
||||
this.header.toggleLiveSidebar();
|
||||
}
|
||||
|
||||
@@ -41,7 +41,6 @@
|
||||
[searchScopeLabel]="facade.searchScopeLabel()"
|
||||
[searchStatusLabel]="facade.searchStatusLabel()"
|
||||
[headerShortcut]="facade.headerShortcut()"
|
||||
[headerBulkAction]="facade.headerBulkAction()"
|
||||
[headerSidebarToggle]="facade.headerSidebarToggle()"
|
||||
[canRefreshPlaylist]="facade.canRefreshPlaylist()"
|
||||
[isRefreshingPlaylist]="facade.isRefreshingPlaylist()"
|
||||
@@ -64,7 +63,6 @@
|
||||
(shortcutsRequested)="keyboardShortcuts.openShortcutsDialog()"
|
||||
(addPlaylistRequested)="facade.openAddPlaylistDialog()"
|
||||
(headerShortcutRequested)="facade.runHeaderShortcut()"
|
||||
(headerBulkActionRequested)="facade.runHeaderBulkAction()"
|
||||
(headerSidebarToggleRequested)="facade.toggleLiveSidebar()"
|
||||
(parentalLockToggleRequested)="facade.toggleParentalLock()"
|
||||
(downloadsRequested)="facade.openDownloadsShortcut()"
|
||||
|
||||
+1
-8
@@ -20,10 +20,7 @@ import {
|
||||
} from '@iptvnator/workspace/shell/util';
|
||||
import { TrafficLightsClearanceDirective } from './traffic-lights-clearance.directive';
|
||||
import { WorkspaceShellComponent } from './workspace-shell.component';
|
||||
import {
|
||||
WorkspaceHeaderBulkAction,
|
||||
WorkspaceShellFacade,
|
||||
} from './services/workspace-shell.facade';
|
||||
import { WorkspaceShellFacade } from './services/workspace-shell.facade';
|
||||
import { WorkspaceKeyboardShortcutsService } from '../workspace-keyboard-shortcuts/workspace-keyboard-shortcuts.service';
|
||||
|
||||
@Component({
|
||||
@@ -71,7 +68,6 @@ class MockWorkspaceShellHeaderComponent {
|
||||
readonly isContextDrawerOpen = input(false);
|
||||
readonly contextDrawerToggleAriaKey = input('');
|
||||
readonly contextDrawerTooltipKey = input('');
|
||||
readonly headerBulkAction = input<WorkspaceHeaderBulkAction | null>(null);
|
||||
readonly headerSidebarToggle = input<unknown>(null);
|
||||
readonly parentalLockState = input<'off' | 'locked' | 'unlocked'>('off');
|
||||
readonly backRequested = output<void>();
|
||||
@@ -83,7 +79,6 @@ class MockWorkspaceShellHeaderComponent {
|
||||
readonly headerShortcutRequested = output<void>();
|
||||
readonly refreshPlaylistRequested = output<void>();
|
||||
readonly downloadsRequested = output<void>();
|
||||
readonly headerBulkActionRequested = output<void>();
|
||||
readonly headerSidebarToggleRequested = output<void>();
|
||||
readonly parentalLockToggleRequested = output<void>();
|
||||
readonly playlistInfoRequested = output<void>();
|
||||
@@ -177,7 +172,6 @@ class MockWorkspaceShellFacade {
|
||||
readonly hasNoPlaylists = signal(false);
|
||||
readonly isDownloadsView = signal(false);
|
||||
readonly activeDownloadsCount = signal(3);
|
||||
readonly headerBulkAction = signal<WorkspaceHeaderBulkAction | null>(null);
|
||||
readonly headerSidebarToggle = signal(null);
|
||||
toggleLiveSidebar = jest.fn();
|
||||
readonly parentalLockState = signal<'off' | 'locked' | 'unlocked'>('off');
|
||||
@@ -226,7 +220,6 @@ class MockWorkspaceShellFacade {
|
||||
runHeaderShortcut = jest.fn();
|
||||
refreshCurrentPlaylist = jest.fn();
|
||||
openDownloadsShortcut = jest.fn();
|
||||
runHeaderBulkAction = jest.fn();
|
||||
openPlaylistInfo = jest.fn();
|
||||
openAccountInfo = jest.fn();
|
||||
openAccountInfoFor = jest.fn();
|
||||
|
||||
Reference in new issue
Block a user