mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-11 19:46:16 -08:00
feat(settings): one lock entry point per rail plus a right-click Lock/Unlock
- Stalker's dedicated lock button becomes the same "Manage categories" (tune) button the Xtream rail has; it opens the lock-only dialog, so every portal type shares one entry point and the rail header keeps three actions. - Right-clicking a category (Xtream, Stalker) or an M3U group offers a single-row Lock / Unlock through the shared CategoryLockMenuComponent, behind the same PIN gate and lock store as the dialog. - The settings hint explains where locks are set; group lock strings added to all locales (ru/de translated). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
1 parent
1d945e91b1
commit
225e8de78e
34 files changed
+677
-81
No files matched your search
@@ -567,7 +567,7 @@
|
||||
"CHANGE_PIN": "Change PIN",
|
||||
"CHANGE_PIN_DESCRIPTION": "Asks for the current PIN first.",
|
||||
"HOW_TO_LOCK": "Choosing what to lock",
|
||||
"HOW_TO_LOCK_DESCRIPTION": "Open a source, then use the lock icons in the category list's manage dialog (Xtream, M3U groups) or the lock button above Stalker categories.",
|
||||
"HOW_TO_LOCK_DESCRIPTION": "Open a source, then use “Manage categories” (the tune icon above the category list; “Manage groups” in an M3U playlist): every row has a lock toggle, and Stalker offers “Lock adult (18+)” for genres the portal flags. Right-click a category or group for a quick Lock / Unlock.",
|
||||
"DISCLAIMER": "This is a child lock, not a security feature: the app's data files are readable by anyone with access to this computer.",
|
||||
"ENABLED_TOAST": "Parental lock enabled",
|
||||
"DISABLED_TOAST": "Parental lock disabled",
|
||||
@@ -1918,6 +1918,8 @@
|
||||
"LOCKED_COUNT": "Locked",
|
||||
"LOCK_CATEGORY": "Lock category",
|
||||
"UNLOCK_CATEGORY": "Unlock category",
|
||||
"LOCK_GROUP": "Lock group",
|
||||
"UNLOCK_GROUP": "Unlock group",
|
||||
"MANAGE_LOCKS": "Lock categories",
|
||||
"MANAGE_LOCKS_DESCRIPTION": "Locked categories and their channels disappear from the whole app until the parental PIN is entered.",
|
||||
"LOCK_CENSORED": "Lock adult (18+)",
|
||||
|
||||
@@ -567,7 +567,7 @@
|
||||
"CHANGE_PIN": "Change PIN",
|
||||
"CHANGE_PIN_DESCRIPTION": "Asks for the current PIN first.",
|
||||
"HOW_TO_LOCK": "Choosing what to lock",
|
||||
"HOW_TO_LOCK_DESCRIPTION": "Open a source, then use the lock icons in the category list's manage dialog (Xtream, M3U groups) or the lock button above Stalker categories.",
|
||||
"HOW_TO_LOCK_DESCRIPTION": "Open a source, then use “Manage categories” (the tune icon above the category list; “Manage groups” in an M3U playlist): every row has a lock toggle, and Stalker offers “Lock adult (18+)” for genres the portal flags. Right-click a category or group for a quick Lock / Unlock.",
|
||||
"DISCLAIMER": "This is a child lock, not a security feature: the app's data files are readable by anyone with access to this computer.",
|
||||
"ENABLED_TOAST": "Parental lock enabled",
|
||||
"DISABLED_TOAST": "Parental lock disabled",
|
||||
@@ -1918,6 +1918,8 @@
|
||||
"LOCKED_COUNT": "Locked",
|
||||
"LOCK_CATEGORY": "Lock category",
|
||||
"UNLOCK_CATEGORY": "Unlock category",
|
||||
"LOCK_GROUP": "Lock group",
|
||||
"UNLOCK_GROUP": "Unlock group",
|
||||
"MANAGE_LOCKS": "Lock categories",
|
||||
"MANAGE_LOCKS_DESCRIPTION": "Locked categories and their channels disappear from the whole app until the parental PIN is entered.",
|
||||
"LOCK_CENSORED": "Lock adult (18+)",
|
||||
|
||||
@@ -567,7 +567,7 @@
|
||||
"CHANGE_PIN": "Change PIN",
|
||||
"CHANGE_PIN_DESCRIPTION": "Asks for the current PIN first.",
|
||||
"HOW_TO_LOCK": "Choosing what to lock",
|
||||
"HOW_TO_LOCK_DESCRIPTION": "Open a source, then use the lock icons in the category list's manage dialog (Xtream, M3U groups) or the lock button above Stalker categories.",
|
||||
"HOW_TO_LOCK_DESCRIPTION": "Open a source, then use “Manage categories” (the tune icon above the category list; “Manage groups” in an M3U playlist): every row has a lock toggle, and Stalker offers “Lock adult (18+)” for genres the portal flags. Right-click a category or group for a quick Lock / Unlock.",
|
||||
"DISCLAIMER": "This is a child lock, not a security feature: the app's data files are readable by anyone with access to this computer.",
|
||||
"ENABLED_TOAST": "Parental lock enabled",
|
||||
"DISABLED_TOAST": "Parental lock disabled",
|
||||
@@ -1918,6 +1918,8 @@
|
||||
"LOCKED_COUNT": "Locked",
|
||||
"LOCK_CATEGORY": "Lock category",
|
||||
"UNLOCK_CATEGORY": "Unlock category",
|
||||
"LOCK_GROUP": "Lock group",
|
||||
"UNLOCK_GROUP": "Unlock group",
|
||||
"MANAGE_LOCKS": "Lock categories",
|
||||
"MANAGE_LOCKS_DESCRIPTION": "Locked categories and their channels disappear from the whole app until the parental PIN is entered.",
|
||||
"LOCK_CENSORED": "Lock adult (18+)",
|
||||
|
||||
@@ -567,7 +567,7 @@
|
||||
"CHANGE_PIN": "PIN ändern",
|
||||
"CHANGE_PIN_DESCRIPTION": "Fragt zuerst die aktuelle PIN ab.",
|
||||
"HOW_TO_LOCK": "Was gesperrt wird",
|
||||
"HOW_TO_LOCK_DESCRIPTION": "Öffne eine Quelle und nutze die Schloss-Symbole im Verwaltungsdialog der Kategorienliste (Xtream, M3U-Gruppen) oder die Schloss-Schaltfläche über den Stalker-Kategorien.",
|
||||
"HOW_TO_LOCK_DESCRIPTION": "Öffne eine Quelle und wähle „Kategorien verwalten“ (das Einstellungssymbol über der Kategorienliste; in einer M3U-Playlist „Gruppen verwalten“): jede Zeile hat einen Schloss-Schalter, und Stalker bietet „Erwachsene (18+) sperren“ für vom Portal markierte Genres. Rechtsklick auf eine Kategorie oder Gruppe für schnelles Sperren / Entsperren.",
|
||||
"DISCLAIMER": "Das ist eine Kindersicherung, kein Sicherheitsmerkmal: Die Datendateien der App sind für jeden mit Zugriff auf diesen Computer lesbar.",
|
||||
"ENABLED_TOAST": "Kindersicherung aktiviert",
|
||||
"DISABLED_TOAST": "Kindersicherung deaktiviert",
|
||||
@@ -1918,6 +1918,8 @@
|
||||
"LOCKED_COUNT": "Gesperrt",
|
||||
"LOCK_CATEGORY": "Kategorie sperren",
|
||||
"UNLOCK_CATEGORY": "Kategorie entsperren",
|
||||
"LOCK_GROUP": "Gruppe sperren",
|
||||
"UNLOCK_GROUP": "Gruppe entsperren",
|
||||
"MANAGE_LOCKS": "Kategorien sperren",
|
||||
"MANAGE_LOCKS_DESCRIPTION": "Gesperrte Kategorien und ihre Sender verschwinden aus der gesamten App, bis die Kindersicherungs-PIN eingegeben wird.",
|
||||
"LOCK_CENSORED": "18+ sperren",
|
||||
|
||||
@@ -567,7 +567,7 @@
|
||||
"CHANGE_PIN": "Change PIN",
|
||||
"CHANGE_PIN_DESCRIPTION": "Asks for the current PIN first.",
|
||||
"HOW_TO_LOCK": "Choosing what to lock",
|
||||
"HOW_TO_LOCK_DESCRIPTION": "Open a source, then use the lock icons in the category list's manage dialog (Xtream, M3U groups) or the lock button above Stalker categories.",
|
||||
"HOW_TO_LOCK_DESCRIPTION": "Open a source, then use “Manage categories” (the tune icon above the category list; “Manage groups” in an M3U playlist): every row has a lock toggle, and Stalker offers “Lock adult (18+)” for genres the portal flags. Right-click a category or group for a quick Lock / Unlock.",
|
||||
"DISCLAIMER": "This is a child lock, not a security feature: the app's data files are readable by anyone with access to this computer.",
|
||||
"ENABLED_TOAST": "Parental lock enabled",
|
||||
"DISABLED_TOAST": "Parental lock disabled",
|
||||
@@ -1918,6 +1918,8 @@
|
||||
"LOCKED_COUNT": "Locked",
|
||||
"LOCK_CATEGORY": "Lock category",
|
||||
"UNLOCK_CATEGORY": "Unlock category",
|
||||
"LOCK_GROUP": "Lock group",
|
||||
"UNLOCK_GROUP": "Unlock group",
|
||||
"MANAGE_LOCKS": "Lock categories",
|
||||
"MANAGE_LOCKS_DESCRIPTION": "Locked categories and their channels disappear from the whole app until the parental PIN is entered.",
|
||||
"LOCK_CENSORED": "Lock adult (18+)",
|
||||
|
||||
@@ -567,7 +567,7 @@
|
||||
"CHANGE_PIN": "Change PIN",
|
||||
"CHANGE_PIN_DESCRIPTION": "Asks for the current PIN first.",
|
||||
"HOW_TO_LOCK": "Choosing what to lock",
|
||||
"HOW_TO_LOCK_DESCRIPTION": "Open a source, then use the lock icons in the category list's manage dialog (Xtream, M3U groups) or the lock button above Stalker categories.",
|
||||
"HOW_TO_LOCK_DESCRIPTION": "Open a source, then use “Manage categories” (the tune icon above the category list; “Manage groups” in an M3U playlist): every row has a lock toggle, and Stalker offers “Lock adult (18+)” for genres the portal flags. Right-click a category or group for a quick Lock / Unlock.",
|
||||
"DISCLAIMER": "This is a child lock, not a security feature: the app's data files are readable by anyone with access to this computer.",
|
||||
"ENABLED_TOAST": "Parental lock enabled",
|
||||
"DISABLED_TOAST": "Parental lock disabled",
|
||||
@@ -1918,6 +1918,8 @@
|
||||
"LOCKED_COUNT": "Locked",
|
||||
"LOCK_CATEGORY": "Lock category",
|
||||
"UNLOCK_CATEGORY": "Unlock category",
|
||||
"LOCK_GROUP": "Lock group",
|
||||
"UNLOCK_GROUP": "Unlock group",
|
||||
"MANAGE_LOCKS": "Lock categories",
|
||||
"MANAGE_LOCKS_DESCRIPTION": "Locked categories and their channels disappear from the whole app until the parental PIN is entered.",
|
||||
"LOCK_CENSORED": "Lock adult (18+)",
|
||||
|
||||
@@ -567,7 +567,7 @@
|
||||
"CHANGE_PIN": "Change PIN",
|
||||
"CHANGE_PIN_DESCRIPTION": "Asks for the current PIN first.",
|
||||
"HOW_TO_LOCK": "Choosing what to lock",
|
||||
"HOW_TO_LOCK_DESCRIPTION": "Open a source, then use the lock icons in the category list's manage dialog (Xtream, M3U groups) or the lock button above Stalker categories.",
|
||||
"HOW_TO_LOCK_DESCRIPTION": "Open a source, then use “Manage categories” (the tune icon above the category list; “Manage groups” in an M3U playlist): every row has a lock toggle, and Stalker offers “Lock adult (18+)” for genres the portal flags. Right-click a category or group for a quick Lock / Unlock.",
|
||||
"DISCLAIMER": "This is a child lock, not a security feature: the app's data files are readable by anyone with access to this computer.",
|
||||
"ENABLED_TOAST": "Parental lock enabled",
|
||||
"DISABLED_TOAST": "Parental lock disabled",
|
||||
@@ -1918,6 +1918,8 @@
|
||||
"LOCKED_COUNT": "Locked",
|
||||
"LOCK_CATEGORY": "Lock category",
|
||||
"UNLOCK_CATEGORY": "Unlock category",
|
||||
"LOCK_GROUP": "Lock group",
|
||||
"UNLOCK_GROUP": "Unlock group",
|
||||
"MANAGE_LOCKS": "Lock categories",
|
||||
"MANAGE_LOCKS_DESCRIPTION": "Locked categories and their channels disappear from the whole app until the parental PIN is entered.",
|
||||
"LOCK_CENSORED": "Lock adult (18+)",
|
||||
|
||||
@@ -567,7 +567,7 @@
|
||||
"CHANGE_PIN": "Change PIN",
|
||||
"CHANGE_PIN_DESCRIPTION": "Asks for the current PIN first.",
|
||||
"HOW_TO_LOCK": "Choosing what to lock",
|
||||
"HOW_TO_LOCK_DESCRIPTION": "Open a source, then use the lock icons in the category list's manage dialog (Xtream, M3U groups) or the lock button above Stalker categories.",
|
||||
"HOW_TO_LOCK_DESCRIPTION": "Open a source, then use “Manage categories” (the tune icon above the category list; “Manage groups” in an M3U playlist): every row has a lock toggle, and Stalker offers “Lock adult (18+)” for genres the portal flags. Right-click a category or group for a quick Lock / Unlock.",
|
||||
"DISCLAIMER": "This is a child lock, not a security feature: the app's data files are readable by anyone with access to this computer.",
|
||||
"ENABLED_TOAST": "Parental lock enabled",
|
||||
"DISABLED_TOAST": "Parental lock disabled",
|
||||
@@ -1918,6 +1918,8 @@
|
||||
"LOCKED_COUNT": "Locked",
|
||||
"LOCK_CATEGORY": "Lock category",
|
||||
"UNLOCK_CATEGORY": "Unlock category",
|
||||
"LOCK_GROUP": "Lock group",
|
||||
"UNLOCK_GROUP": "Unlock group",
|
||||
"MANAGE_LOCKS": "Lock categories",
|
||||
"MANAGE_LOCKS_DESCRIPTION": "Locked categories and their channels disappear from the whole app until the parental PIN is entered.",
|
||||
"LOCK_CENSORED": "Lock adult (18+)",
|
||||
|
||||
@@ -567,7 +567,7 @@
|
||||
"CHANGE_PIN": "Change PIN",
|
||||
"CHANGE_PIN_DESCRIPTION": "Asks for the current PIN first.",
|
||||
"HOW_TO_LOCK": "Choosing what to lock",
|
||||
"HOW_TO_LOCK_DESCRIPTION": "Open a source, then use the lock icons in the category list's manage dialog (Xtream, M3U groups) or the lock button above Stalker categories.",
|
||||
"HOW_TO_LOCK_DESCRIPTION": "Open a source, then use “Manage categories” (the tune icon above the category list; “Manage groups” in an M3U playlist): every row has a lock toggle, and Stalker offers “Lock adult (18+)” for genres the portal flags. Right-click a category or group for a quick Lock / Unlock.",
|
||||
"DISCLAIMER": "This is a child lock, not a security feature: the app's data files are readable by anyone with access to this computer.",
|
||||
"ENABLED_TOAST": "Parental lock enabled",
|
||||
"DISABLED_TOAST": "Parental lock disabled",
|
||||
@@ -1918,6 +1918,8 @@
|
||||
"LOCKED_COUNT": "Locked",
|
||||
"LOCK_CATEGORY": "Lock category",
|
||||
"UNLOCK_CATEGORY": "Unlock category",
|
||||
"LOCK_GROUP": "Lock group",
|
||||
"UNLOCK_GROUP": "Unlock group",
|
||||
"MANAGE_LOCKS": "Lock categories",
|
||||
"MANAGE_LOCKS_DESCRIPTION": "Locked categories and their channels disappear from the whole app until the parental PIN is entered.",
|
||||
"LOCK_CENSORED": "Lock adult (18+)",
|
||||
|
||||
@@ -567,7 +567,7 @@
|
||||
"CHANGE_PIN": "Change PIN",
|
||||
"CHANGE_PIN_DESCRIPTION": "Asks for the current PIN first.",
|
||||
"HOW_TO_LOCK": "Choosing what to lock",
|
||||
"HOW_TO_LOCK_DESCRIPTION": "Open a source, then use the lock icons in the category list's manage dialog (Xtream, M3U groups) or the lock button above Stalker categories.",
|
||||
"HOW_TO_LOCK_DESCRIPTION": "Open a source, then use “Manage categories” (the tune icon above the category list; “Manage groups” in an M3U playlist): every row has a lock toggle, and Stalker offers “Lock adult (18+)” for genres the portal flags. Right-click a category or group for a quick Lock / Unlock.",
|
||||
"DISCLAIMER": "This is a child lock, not a security feature: the app's data files are readable by anyone with access to this computer.",
|
||||
"ENABLED_TOAST": "Parental lock enabled",
|
||||
"DISABLED_TOAST": "Parental lock disabled",
|
||||
@@ -1918,6 +1918,8 @@
|
||||
"LOCKED_COUNT": "Locked",
|
||||
"LOCK_CATEGORY": "Lock category",
|
||||
"UNLOCK_CATEGORY": "Unlock category",
|
||||
"LOCK_GROUP": "Lock group",
|
||||
"UNLOCK_GROUP": "Unlock group",
|
||||
"MANAGE_LOCKS": "Lock categories",
|
||||
"MANAGE_LOCKS_DESCRIPTION": "Locked categories and their channels disappear from the whole app until the parental PIN is entered.",
|
||||
"LOCK_CENSORED": "Lock adult (18+)",
|
||||
|
||||
@@ -567,7 +567,7 @@
|
||||
"CHANGE_PIN": "Change PIN",
|
||||
"CHANGE_PIN_DESCRIPTION": "Asks for the current PIN first.",
|
||||
"HOW_TO_LOCK": "Choosing what to lock",
|
||||
"HOW_TO_LOCK_DESCRIPTION": "Open a source, then use the lock icons in the category list's manage dialog (Xtream, M3U groups) or the lock button above Stalker categories.",
|
||||
"HOW_TO_LOCK_DESCRIPTION": "Open a source, then use “Manage categories” (the tune icon above the category list; “Manage groups” in an M3U playlist): every row has a lock toggle, and Stalker offers “Lock adult (18+)” for genres the portal flags. Right-click a category or group for a quick Lock / Unlock.",
|
||||
"DISCLAIMER": "This is a child lock, not a security feature: the app's data files are readable by anyone with access to this computer.",
|
||||
"ENABLED_TOAST": "Parental lock enabled",
|
||||
"DISABLED_TOAST": "Parental lock disabled",
|
||||
@@ -1918,6 +1918,8 @@
|
||||
"LOCKED_COUNT": "Locked",
|
||||
"LOCK_CATEGORY": "Lock category",
|
||||
"UNLOCK_CATEGORY": "Unlock category",
|
||||
"LOCK_GROUP": "Lock group",
|
||||
"UNLOCK_GROUP": "Unlock group",
|
||||
"MANAGE_LOCKS": "Lock categories",
|
||||
"MANAGE_LOCKS_DESCRIPTION": "Locked categories and their channels disappear from the whole app until the parental PIN is entered.",
|
||||
"LOCK_CENSORED": "Lock adult (18+)",
|
||||
|
||||
@@ -567,7 +567,7 @@
|
||||
"CHANGE_PIN": "Change PIN",
|
||||
"CHANGE_PIN_DESCRIPTION": "Asks for the current PIN first.",
|
||||
"HOW_TO_LOCK": "Choosing what to lock",
|
||||
"HOW_TO_LOCK_DESCRIPTION": "Open a source, then use the lock icons in the category list's manage dialog (Xtream, M3U groups) or the lock button above Stalker categories.",
|
||||
"HOW_TO_LOCK_DESCRIPTION": "Open a source, then use “Manage categories” (the tune icon above the category list; “Manage groups” in an M3U playlist): every row has a lock toggle, and Stalker offers “Lock adult (18+)” for genres the portal flags. Right-click a category or group for a quick Lock / Unlock.",
|
||||
"DISCLAIMER": "This is a child lock, not a security feature: the app's data files are readable by anyone with access to this computer.",
|
||||
"ENABLED_TOAST": "Parental lock enabled",
|
||||
"DISABLED_TOAST": "Parental lock disabled",
|
||||
@@ -1918,6 +1918,8 @@
|
||||
"LOCKED_COUNT": "Locked",
|
||||
"LOCK_CATEGORY": "Lock category",
|
||||
"UNLOCK_CATEGORY": "Unlock category",
|
||||
"LOCK_GROUP": "Lock group",
|
||||
"UNLOCK_GROUP": "Unlock group",
|
||||
"MANAGE_LOCKS": "Lock categories",
|
||||
"MANAGE_LOCKS_DESCRIPTION": "Locked categories and their channels disappear from the whole app until the parental PIN is entered.",
|
||||
"LOCK_CENSORED": "Lock adult (18+)",
|
||||
|
||||
@@ -567,7 +567,7 @@
|
||||
"CHANGE_PIN": "Change PIN",
|
||||
"CHANGE_PIN_DESCRIPTION": "Asks for the current PIN first.",
|
||||
"HOW_TO_LOCK": "Choosing what to lock",
|
||||
"HOW_TO_LOCK_DESCRIPTION": "Open a source, then use the lock icons in the category list's manage dialog (Xtream, M3U groups) or the lock button above Stalker categories.",
|
||||
"HOW_TO_LOCK_DESCRIPTION": "Open a source, then use “Manage categories” (the tune icon above the category list; “Manage groups” in an M3U playlist): every row has a lock toggle, and Stalker offers “Lock adult (18+)” for genres the portal flags. Right-click a category or group for a quick Lock / Unlock.",
|
||||
"DISCLAIMER": "This is a child lock, not a security feature: the app's data files are readable by anyone with access to this computer.",
|
||||
"ENABLED_TOAST": "Parental lock enabled",
|
||||
"DISABLED_TOAST": "Parental lock disabled",
|
||||
@@ -1918,6 +1918,8 @@
|
||||
"LOCKED_COUNT": "Locked",
|
||||
"LOCK_CATEGORY": "Lock category",
|
||||
"UNLOCK_CATEGORY": "Unlock category",
|
||||
"LOCK_GROUP": "Lock group",
|
||||
"UNLOCK_GROUP": "Unlock group",
|
||||
"MANAGE_LOCKS": "Lock categories",
|
||||
"MANAGE_LOCKS_DESCRIPTION": "Locked categories and their channels disappear from the whole app until the parental PIN is entered.",
|
||||
"LOCK_CENSORED": "Lock adult (18+)",
|
||||
|
||||
@@ -567,7 +567,7 @@
|
||||
"CHANGE_PIN": "Change PIN",
|
||||
"CHANGE_PIN_DESCRIPTION": "Asks for the current PIN first.",
|
||||
"HOW_TO_LOCK": "Choosing what to lock",
|
||||
"HOW_TO_LOCK_DESCRIPTION": "Open a source, then use the lock icons in the category list's manage dialog (Xtream, M3U groups) or the lock button above Stalker categories.",
|
||||
"HOW_TO_LOCK_DESCRIPTION": "Open a source, then use “Manage categories” (the tune icon above the category list; “Manage groups” in an M3U playlist): every row has a lock toggle, and Stalker offers “Lock adult (18+)” for genres the portal flags. Right-click a category or group for a quick Lock / Unlock.",
|
||||
"DISCLAIMER": "This is a child lock, not a security feature: the app's data files are readable by anyone with access to this computer.",
|
||||
"ENABLED_TOAST": "Parental lock enabled",
|
||||
"DISABLED_TOAST": "Parental lock disabled",
|
||||
@@ -1918,6 +1918,8 @@
|
||||
"LOCKED_COUNT": "Locked",
|
||||
"LOCK_CATEGORY": "Lock category",
|
||||
"UNLOCK_CATEGORY": "Unlock category",
|
||||
"LOCK_GROUP": "Lock group",
|
||||
"UNLOCK_GROUP": "Unlock group",
|
||||
"MANAGE_LOCKS": "Lock categories",
|
||||
"MANAGE_LOCKS_DESCRIPTION": "Locked categories and their channels disappear from the whole app until the parental PIN is entered.",
|
||||
"LOCK_CENSORED": "Lock adult (18+)",
|
||||
|
||||
@@ -567,7 +567,7 @@
|
||||
"CHANGE_PIN": "Change PIN",
|
||||
"CHANGE_PIN_DESCRIPTION": "Asks for the current PIN first.",
|
||||
"HOW_TO_LOCK": "Choosing what to lock",
|
||||
"HOW_TO_LOCK_DESCRIPTION": "Open a source, then use the lock icons in the category list's manage dialog (Xtream, M3U groups) or the lock button above Stalker categories.",
|
||||
"HOW_TO_LOCK_DESCRIPTION": "Open a source, then use “Manage categories” (the tune icon above the category list; “Manage groups” in an M3U playlist): every row has a lock toggle, and Stalker offers “Lock adult (18+)” for genres the portal flags. Right-click a category or group for a quick Lock / Unlock.",
|
||||
"DISCLAIMER": "This is a child lock, not a security feature: the app's data files are readable by anyone with access to this computer.",
|
||||
"ENABLED_TOAST": "Parental lock enabled",
|
||||
"DISABLED_TOAST": "Parental lock disabled",
|
||||
@@ -1918,6 +1918,8 @@
|
||||
"LOCKED_COUNT": "Locked",
|
||||
"LOCK_CATEGORY": "Lock category",
|
||||
"UNLOCK_CATEGORY": "Unlock category",
|
||||
"LOCK_GROUP": "Lock group",
|
||||
"UNLOCK_GROUP": "Unlock group",
|
||||
"MANAGE_LOCKS": "Lock categories",
|
||||
"MANAGE_LOCKS_DESCRIPTION": "Locked categories and their channels disappear from the whole app until the parental PIN is entered.",
|
||||
"LOCK_CENSORED": "Lock adult (18+)",
|
||||
|
||||
@@ -567,7 +567,7 @@
|
||||
"CHANGE_PIN": "Сменить PIN",
|
||||
"CHANGE_PIN_DESCRIPTION": "Сначала запрашивается текущий PIN.",
|
||||
"HOW_TO_LOCK": "Что блокировать",
|
||||
"HOW_TO_LOCK_DESCRIPTION": "Откройте источник и используйте значки замка в диалоге управления категориями (Xtream, группы M3U) или кнопку замка над категориями Stalker.",
|
||||
"HOW_TO_LOCK_DESCRIPTION": "Откройте источник и нажмите «Управлять категориями» (иконка настройки над списком категорий; в плейлисте M3U — «Управление группами»): у каждой строки есть переключатель замка, а для Stalker доступно «Заблокировать 18+» для жанров, помеченных порталом. Правый клик по категории или группе — быстрое «Заблокировать / Разблокировать».",
|
||||
"DISCLAIMER": "Это детская блокировка, а не защита: файлы данных приложения доступны любому, у кого есть доступ к этому компьютеру.",
|
||||
"ENABLED_TOAST": "Родительский контроль включён",
|
||||
"DISABLED_TOAST": "Родительский контроль выключен",
|
||||
@@ -1918,6 +1918,8 @@
|
||||
"LOCKED_COUNT": "Заблокировано",
|
||||
"LOCK_CATEGORY": "Заблокировать категорию",
|
||||
"UNLOCK_CATEGORY": "Разблокировать категорию",
|
||||
"LOCK_GROUP": "Заблокировать группу",
|
||||
"UNLOCK_GROUP": "Разблокировать группу",
|
||||
"MANAGE_LOCKS": "Заблокировать категории",
|
||||
"MANAGE_LOCKS_DESCRIPTION": "Заблокированные категории и их каналы исчезают из всего приложения, пока не введён родительский PIN.",
|
||||
"LOCK_CENSORED": "Заблокировать 18+",
|
||||
|
||||
@@ -567,7 +567,7 @@
|
||||
"CHANGE_PIN": "Change PIN",
|
||||
"CHANGE_PIN_DESCRIPTION": "Asks for the current PIN first.",
|
||||
"HOW_TO_LOCK": "Choosing what to lock",
|
||||
"HOW_TO_LOCK_DESCRIPTION": "Open a source, then use the lock icons in the category list's manage dialog (Xtream, M3U groups) or the lock button above Stalker categories.",
|
||||
"HOW_TO_LOCK_DESCRIPTION": "Open a source, then use “Manage categories” (the tune icon above the category list; “Manage groups” in an M3U playlist): every row has a lock toggle, and Stalker offers “Lock adult (18+)” for genres the portal flags. Right-click a category or group for a quick Lock / Unlock.",
|
||||
"DISCLAIMER": "This is a child lock, not a security feature: the app's data files are readable by anyone with access to this computer.",
|
||||
"ENABLED_TOAST": "Parental lock enabled",
|
||||
"DISABLED_TOAST": "Parental lock disabled",
|
||||
@@ -1918,6 +1918,8 @@
|
||||
"LOCKED_COUNT": "Locked",
|
||||
"LOCK_CATEGORY": "Lock category",
|
||||
"UNLOCK_CATEGORY": "Unlock category",
|
||||
"LOCK_GROUP": "Lock group",
|
||||
"UNLOCK_GROUP": "Unlock group",
|
||||
"MANAGE_LOCKS": "Lock categories",
|
||||
"MANAGE_LOCKS_DESCRIPTION": "Locked categories and their channels disappear from the whole app until the parental PIN is entered.",
|
||||
"LOCK_CENSORED": "Lock adult (18+)",
|
||||
|
||||
@@ -567,7 +567,7 @@
|
||||
"CHANGE_PIN": "Change PIN",
|
||||
"CHANGE_PIN_DESCRIPTION": "Asks for the current PIN first.",
|
||||
"HOW_TO_LOCK": "Choosing what to lock",
|
||||
"HOW_TO_LOCK_DESCRIPTION": "Open a source, then use the lock icons in the category list's manage dialog (Xtream, M3U groups) or the lock button above Stalker categories.",
|
||||
"HOW_TO_LOCK_DESCRIPTION": "Open a source, then use “Manage categories” (the tune icon above the category list; “Manage groups” in an M3U playlist): every row has a lock toggle, and Stalker offers “Lock adult (18+)” for genres the portal flags. Right-click a category or group for a quick Lock / Unlock.",
|
||||
"DISCLAIMER": "This is a child lock, not a security feature: the app's data files are readable by anyone with access to this computer.",
|
||||
"ENABLED_TOAST": "Parental lock enabled",
|
||||
"DISABLED_TOAST": "Parental lock disabled",
|
||||
@@ -1918,6 +1918,8 @@
|
||||
"LOCKED_COUNT": "Locked",
|
||||
"LOCK_CATEGORY": "Lock category",
|
||||
"UNLOCK_CATEGORY": "Unlock category",
|
||||
"LOCK_GROUP": "Lock group",
|
||||
"UNLOCK_GROUP": "Unlock group",
|
||||
"MANAGE_LOCKS": "Lock categories",
|
||||
"MANAGE_LOCKS_DESCRIPTION": "Locked categories and their channels disappear from the whole app until the parental PIN is entered.",
|
||||
"LOCK_CENSORED": "Lock adult (18+)",
|
||||
|
||||
@@ -567,7 +567,7 @@
|
||||
"CHANGE_PIN": "Change PIN",
|
||||
"CHANGE_PIN_DESCRIPTION": "Asks for the current PIN first.",
|
||||
"HOW_TO_LOCK": "Choosing what to lock",
|
||||
"HOW_TO_LOCK_DESCRIPTION": "Open a source, then use the lock icons in the category list's manage dialog (Xtream, M3U groups) or the lock button above Stalker categories.",
|
||||
"HOW_TO_LOCK_DESCRIPTION": "Open a source, then use “Manage categories” (the tune icon above the category list; “Manage groups” in an M3U playlist): every row has a lock toggle, and Stalker offers “Lock adult (18+)” for genres the portal flags. Right-click a category or group for a quick Lock / Unlock.",
|
||||
"DISCLAIMER": "This is a child lock, not a security feature: the app's data files are readable by anyone with access to this computer.",
|
||||
"ENABLED_TOAST": "Parental lock enabled",
|
||||
"DISABLED_TOAST": "Parental lock disabled",
|
||||
@@ -1918,6 +1918,8 @@
|
||||
"LOCKED_COUNT": "Locked",
|
||||
"LOCK_CATEGORY": "Lock category",
|
||||
"UNLOCK_CATEGORY": "Unlock category",
|
||||
"LOCK_GROUP": "Lock group",
|
||||
"UNLOCK_GROUP": "Unlock group",
|
||||
"MANAGE_LOCKS": "Lock categories",
|
||||
"MANAGE_LOCKS_DESCRIPTION": "Locked categories and their channels disappear from the whole app until the parental PIN is entered.",
|
||||
"LOCK_CENSORED": "Lock adult (18+)",
|
||||
|
||||
@@ -151,7 +151,10 @@ dialog renders a lock toggle per row and the whole dialog opens only after the
|
||||
PIN. Locks are a separate concept from `hidden`: they live in the renderer's
|
||||
lock store keyed by provider category id and are mirrored into
|
||||
`categories.locked` through `DB_SET_CATEGORY_LOCKS`, so a refresh keeps them
|
||||
the same way it keeps `hidden`. Contract: [parental lock](parental-lock.md).
|
||||
the same way it keeps `hidden`. The dialog is the one entry point on every
|
||||
portal type (Stalker's "Manage categories" button opens its lock-only
|
||||
dialog); a right-click on a category row offers a single-row Lock/Unlock
|
||||
through the same lock store. Contract: [parental lock](parental-lock.md).
|
||||
|
||||
## Files Changed
|
||||
|
||||
|
||||
@@ -284,46 +284,47 @@ on either side.
|
||||
disclaimer. Everything applies immediately (it gates on the PIN), outside
|
||||
the shared dirty form; `createSettingsFromFormValue` carries both values
|
||||
from the current settings so Save cannot undo them.
|
||||
- Lock toggles in the Xtream category-management dialog and the M3U group
|
||||
dialog (rendered only while the feature is on), plus a new Stalker
|
||||
`StalkerCategoryLockDialogComponent` reached from a lock button above the
|
||||
categories rail; it offers "Lock adult (18+)" for genres the portal flags
|
||||
`censored`. All three list the locked names and can rewrite the locks, so
|
||||
each opens only after `requestUnlock()` succeeds — and closes itself,
|
||||
discarding the draft, the moment `active` becomes true again (idle relock,
|
||||
Lock now), since the PIN gate covers only the opening. The M3U group
|
||||
dialog does this only while it carries lock toggles; the plain hide/show
|
||||
editor is not behind the PIN. The Xtream dialog loads
|
||||
its candidates through the capability-selected data source
|
||||
(`IXtreamDataSource.getAllCategories`, which the PWA source answers from
|
||||
its session cache or the API), so PWA users can set locks too; the
|
||||
hide/show checkboxes remain Electron-only. The relock timeout persists
|
||||
through the same undo-on-failure pattern as the switch
|
||||
(`setRelockMinutes` reverts the in-memory value and the facade shows the
|
||||
settings save-failure snackbar). The M3U dialog hands its lock
|
||||
list back to `ChannelListContainerComponent`, which awaits the write and
|
||||
reports a failed save in a snackbar (the dialog has closed by then). On
|
||||
Electron the `categories.locked` re-stamp (`setCategoryLocks`) clears and
|
||||
re-locks one playlist/type inside ONE transaction, so a failed restamp
|
||||
keeps the previous index instead of leaving every category unlocked. The
|
||||
store commits BEFORE that re-stamp, so a failed re-stamp rolls the store
|
||||
back to the previous locks AND re-stamps every touched type from them (a
|
||||
backup restore stamps three types, and the ones before the failing type
|
||||
already carry the new locks; a category must not be recorded and shown
|
||||
as locked while Electron reads, which filter by the index alone, still
|
||||
serve it), and the store revision consumers reload on is published only
|
||||
once every touched type is stamped, so a reload cannot read a later type
|
||||
through its old stamps; if the rollback write or its re-stamp fails too, the playlist is
|
||||
re-stamped on the next store access, and every launch re-derives the
|
||||
index from the store for each playlist that has locks — which is why a
|
||||
write that removes a playlist's LAST lock clears the index first and
|
||||
drops the key afterwards (the reconcile finds playlists only through
|
||||
their key; an interruption then leaves store-with-lock and index-without,
|
||||
which the next reconcile repairs toward locked; a store recovered by a
|
||||
later successful read marks its playlists stale the same way) — awaited inside
|
||||
the store's `load()`, so `readable` (and with it every catalog read the
|
||||
renderer gates) stays false until the index agrees with the store; a
|
||||
re-stamp that keeps failing keeps the session fail-closed.
|
||||
- One entry point per rail, the same on every portal type: the "Manage
|
||||
categories" button (`tune` icon; "Manage groups" on an M3U playlist).
|
||||
Xtream and M3U render lock toggles inside their existing hide/show
|
||||
dialogs while the feature is on; Stalker has no hide/show dialog, so its
|
||||
button — shown only while the feature is on — opens the lock-only
|
||||
`StalkerCategoryLockDialogComponent`, which also offers "Lock adult
|
||||
(18+)" for genres the portal flags `censored`. All three list the locked
|
||||
names and can rewrite the locks, so each opens only after
|
||||
`requestUnlock()` succeeds — and closes itself, discarding the draft, the
|
||||
moment `active` becomes true again (idle relock, Lock now), since the
|
||||
PIN gate covers only the opening. The M3U group dialog does this only
|
||||
while it carries lock toggles; the plain hide/show editor is not behind
|
||||
the PIN. The Xtream dialog loads its candidates through the
|
||||
capability-selected data source (`IXtreamDataSource.getAllCategories`,
|
||||
which the PWA source answers from its session cache or the API), so PWA
|
||||
users can set locks too; the hide/show checkboxes remain Electron-only.
|
||||
A deliberate non-choice: no dedicated lock button in the rail header
|
||||
(the header already carries search, sort and manage, and the lock is a
|
||||
category-management concern) and no keyword-based "adult" recognition
|
||||
for Xtream/M3U (provider naming is arbitrary; the only automation is the
|
||||
portal's own `censored` flag on Stalker).
|
||||
- Right-click accelerator: a `contextmenu` on a category row (Xtream and
|
||||
Stalker rail, `WorkspaceContextCategoryViewComponent`) or an M3U group
|
||||
row opens the shared `CategoryLockMenuComponent` (`libs/ui/components`)
|
||||
with one "Lock / Unlock category (group)" item, only while the feature
|
||||
is on. The portal rail persists through
|
||||
`WorkspaceCategoryLockActionService` (PIN gate, then one-id edit of the
|
||||
lock store, failure snackbar); the M3U rail emits the edited list to
|
||||
`ChannelListContainerComponent`, which persists it like the dialog's
|
||||
result. Bulk actions stay in the dialog, and while the session is locked
|
||||
a locked category is not in the rail, so unlocking always goes through
|
||||
the "N locked · Enter PIN to show" row or the dialog. The M3U dialog
|
||||
hands its lock list back to `ChannelListContainerComponent`, which
|
||||
awaits the write and reports a failed save in a snackbar (the dialog has
|
||||
closed by then). On Electron the `categories.locked` re-stamp
|
||||
(`setCategoryLocks`) clears and re-locks one playlist/type inside ONE
|
||||
transaction, so a failed restamp keeps the previous index instead of
|
||||
leaving every category unlocked. The relock timeout persists through the
|
||||
same undo-on-failure pattern as the switch (`setRelockMinutes` reverts
|
||||
the in-memory value and the facade shows the settings save-failure
|
||||
snackbar).
|
||||
- Header lock/unlock button and the `parental-lock-now` /
|
||||
`parental-unlock` palette commands.
|
||||
|
||||
|
||||
@@ -32,3 +32,4 @@ export * from './lib/watched-badge/watched-badge.component';
|
||||
export * from './lib/window-controls/window-controls.component';
|
||||
|
||||
export * from './lib/channel-scroll-focus/channel-scroll-focus.directive';
|
||||
export * from './lib/category-lock-menu/category-lock-menu.component';
|
||||
@@ -0,0 +1,49 @@
|
||||
import { Component, viewChild } from '@angular/core';
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { CategoryLockMenuComponent } from './category-lock-menu.component';
|
||||
|
||||
@Component({
|
||||
imports: [CategoryLockMenuComponent],
|
||||
template: `<app-category-lock-menu
|
||||
(toggleRequested)="requested.push($event)"
|
||||
/>`,
|
||||
})
|
||||
class HostComponent {
|
||||
readonly menu = viewChild.required(CategoryLockMenuComponent);
|
||||
readonly requested: boolean[] = [];
|
||||
}
|
||||
|
||||
describe('CategoryLockMenuComponent', () => {
|
||||
it('opens at the pointer and requests the opposite lock state', async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [
|
||||
HostComponent,
|
||||
NoopAnimationsModule,
|
||||
TranslateModule.forRoot(),
|
||||
],
|
||||
}).compileComponents();
|
||||
const fixture = TestBed.createComponent(HostComponent);
|
||||
fixture.detectChanges();
|
||||
const host = fixture.componentInstance;
|
||||
const event = new MouseEvent('contextmenu', {
|
||||
clientX: 40,
|
||||
clientY: 60,
|
||||
cancelable: true,
|
||||
});
|
||||
|
||||
host.menu().open(event, true);
|
||||
await Promise.resolve();
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(event.defaultPrevented).toBe(true);
|
||||
expect(host.menu().position()).toEqual({ x: '40px', y: '60px' });
|
||||
const item = document.querySelector<HTMLButtonElement>(
|
||||
'[data-test-id="category-lock-menu-toggle"]'
|
||||
);
|
||||
expect(item?.textContent).toContain('PARENTAL_LOCK.UNLOCK_CATEGORY');
|
||||
item?.click();
|
||||
expect(host.requested).toEqual([false]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,85 @@
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
input,
|
||||
output,
|
||||
signal,
|
||||
viewChild,
|
||||
} from '@angular/core';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatMenuModule, MatMenuTrigger } from '@angular/material/menu';
|
||||
import { TranslatePipe } from '@ngx-translate/core';
|
||||
|
||||
/**
|
||||
* Right-click accelerator of the parental lock: one positioned context menu
|
||||
* with a single "Lock … / Unlock …" item. Hosts (the portal category rail,
|
||||
* the M3U groups rail) call `open()` from a `contextmenu` event and persist
|
||||
* the emitted target state themselves — the menu knows nothing about
|
||||
* playlists or lock stores. Bulk actions and the full list stay in the
|
||||
* "Manage categories" dialog; this only shortens the single-category case.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'app-category-lock-menu',
|
||||
imports: [MatIconModule, MatMenuModule, TranslatePipe],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
template: `
|
||||
<div
|
||||
aria-hidden="true"
|
||||
class="category-lock-menu__anchor"
|
||||
[style.left]="position().x"
|
||||
[style.top]="position().y"
|
||||
[matMenuTriggerFor]="menu"
|
||||
#trigger="matMenuTrigger"
|
||||
></div>
|
||||
<mat-menu #menu="matMenu">
|
||||
<button
|
||||
mat-menu-item
|
||||
type="button"
|
||||
data-test-id="category-lock-menu-toggle"
|
||||
(click)="toggleRequested.emit(!locked())"
|
||||
>
|
||||
<mat-icon>{{ locked() ? 'lock_open' : 'lock' }}</mat-icon>
|
||||
<span>{{
|
||||
(locked() ? unlockLabelKey() : lockLabelKey()) | translate
|
||||
}}</span>
|
||||
</button>
|
||||
</mat-menu>
|
||||
`,
|
||||
styles: `
|
||||
:host {
|
||||
display: contents;
|
||||
}
|
||||
.category-lock-menu__anchor {
|
||||
position: fixed;
|
||||
width: 0;
|
||||
height: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
`,
|
||||
})
|
||||
export class CategoryLockMenuComponent {
|
||||
readonly lockLabelKey = input('PARENTAL_LOCK.LOCK_CATEGORY');
|
||||
readonly unlockLabelKey = input('PARENTAL_LOCK.UNLOCK_CATEGORY');
|
||||
/** The desired state: `true` = lock the row, `false` = unlock it. */
|
||||
readonly toggleRequested = output<boolean>();
|
||||
|
||||
readonly locked = signal(false);
|
||||
readonly position = signal({ x: '0px', y: '0px' });
|
||||
private readonly trigger = viewChild.required<MatMenuTrigger>('trigger');
|
||||
|
||||
/** Opens the menu at the pointer for a row whose lock state is `locked`. */
|
||||
open(event: MouseEvent, locked: boolean): void {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
this.locked.set(locked);
|
||||
this.position.set({
|
||||
x: `${event.clientX}px`,
|
||||
y: `${event.clientY}px`,
|
||||
});
|
||||
const trigger = this.trigger();
|
||||
if (trigger.menuOpen) {
|
||||
trigger.closeMenu();
|
||||
}
|
||||
queueMicrotask(() => this.trigger().openMenu());
|
||||
}
|
||||
}
|
||||
+10
@@ -127,6 +127,9 @@
|
||||
: null
|
||||
"
|
||||
(click)="selectGroup(group.key)"
|
||||
(contextmenu)="
|
||||
onGroupContextMenu(group.key, $event)
|
||||
"
|
||||
>
|
||||
<span class="nav-item-label">
|
||||
@if (group.key) {
|
||||
@@ -354,3 +357,10 @@
|
||||
<span>{{ 'CHANNELS.SHOW_DETAILS' | translate }}</span>
|
||||
</button>
|
||||
</mat-menu>
|
||||
|
||||
<app-category-lock-menu
|
||||
#groupLockMenu
|
||||
lockLabelKey="PARENTAL_LOCK.LOCK_GROUP"
|
||||
unlockLabelKey="PARENTAL_LOCK.UNLOCK_GROUP"
|
||||
(toggleRequested)="onGroupLockToggle($event)"
|
||||
/>
|
||||
+31
@@ -537,6 +537,37 @@ describe('GroupsViewComponent', () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it('locks and unlocks one group from the right-click menu behind the PIN gate', async () => {
|
||||
const lockedGroupTitlesChanged = jest.fn();
|
||||
component.lockedGroupTitlesChanged.subscribe(lockedGroupTitlesChanged);
|
||||
fixture.componentRef.setInput('lockedGroupTitles', ['News']);
|
||||
fixture.detectChanges();
|
||||
const event = new MouseEvent('contextmenu', { cancelable: true });
|
||||
|
||||
component.onGroupContextMenu('Sports', event);
|
||||
expect(event.defaultPrevented).toBe(true);
|
||||
await component.onGroupLockToggle(true);
|
||||
expect(lockedGroupTitlesChanged).toHaveBeenLastCalledWith([
|
||||
'News',
|
||||
'Sports',
|
||||
]);
|
||||
|
||||
component.onGroupContextMenu('News', event);
|
||||
await component.onGroupLockToggle(false);
|
||||
expect(lockedGroupTitlesChanged).toHaveBeenLastCalledWith([]);
|
||||
|
||||
parentalLock.requestUnlock.mockResolvedValueOnce(false);
|
||||
component.onGroupContextMenu('Movies', event);
|
||||
await component.onGroupLockToggle(true);
|
||||
expect(lockedGroupTitlesChanged).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it('offers no group lock menu while the feature is off', () => {
|
||||
const event = new MouseEvent('contextmenu', { cancelable: true });
|
||||
component.onGroupContextMenu('Sports', event);
|
||||
expect(event.defaultPrevented).toBe(false);
|
||||
});
|
||||
|
||||
it('keeps the manage-groups dialog closed when the parental PIN is refused', async () => {
|
||||
// The dialog names every locked group and can rewrite the locks.
|
||||
parentalLock.requestUnlock.mockResolvedValueOnce(false);
|
||||
|
||||
+37
@@ -41,6 +41,7 @@ import { resolveChannelLogo } from '../channel-logo-fallback.util';
|
||||
import { EpgMappingDialogComponent } from '../epg-mapping-dialog/epg-mapping-dialog.component';
|
||||
import { ChannelDetailsDialogComponent } from '../channel-details-dialog/channel-details-dialog.component';
|
||||
import { ChannelListItemComponent } from '../channel-list-item/channel-list-item.component';
|
||||
import { CategoryLockMenuComponent } from '../../category-lock-menu/category-lock-menu.component';
|
||||
import { ResizableDirective } from '../../resizable/resizable.directive';
|
||||
import {
|
||||
GroupManagementDialogComponent,
|
||||
@@ -70,6 +71,7 @@ interface FilteredGroupView {
|
||||
styleUrls: ['./groups-view.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [
|
||||
CategoryLockMenuComponent,
|
||||
ChannelScrollFocusDirective,
|
||||
ChannelListItemComponent,
|
||||
MatButtonModule,
|
||||
@@ -188,6 +190,10 @@ export class GroupsViewComponent {
|
||||
*/
|
||||
readonly selectedGroupChange = output<string | null>();
|
||||
|
||||
private readonly groupLockMenu =
|
||||
viewChild<CategoryLockMenuComponent>('groupLockMenu');
|
||||
private groupLockKey: string | null = null;
|
||||
|
||||
readonly isGroupSearchOpen = signal(false);
|
||||
readonly localGroupSearchTerm = signal('');
|
||||
readonly selectedGroupKey = signal<string | null>(null);
|
||||
@@ -489,6 +495,37 @@ export class GroupsViewComponent {
|
||||
this.localGroupSearchTerm.set(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Right-click accelerator of the parental lock for one group; the host
|
||||
* persists the emitted list exactly as it does for the dialog. No menu
|
||||
* while the feature is off (`lockedGroupTitles` is null then).
|
||||
*/
|
||||
onGroupContextMenu(groupKey: string, event: MouseEvent): void {
|
||||
const locked = this.lockedGroupTitles();
|
||||
if (locked === null) {
|
||||
return;
|
||||
}
|
||||
this.groupLockKey = groupKey;
|
||||
this.groupLockMenu()?.open(event, locked.includes(groupKey));
|
||||
}
|
||||
|
||||
async onGroupLockToggle(lock: boolean): Promise<void> {
|
||||
const groupKey = this.groupLockKey;
|
||||
this.groupLockKey = null;
|
||||
const current = this.lockedGroupTitles();
|
||||
if (groupKey === null || current === null) {
|
||||
return;
|
||||
}
|
||||
if (!(await this.parentalLock.requestUnlock())) {
|
||||
return;
|
||||
}
|
||||
this.lockedGroupTitlesChanged.emit(
|
||||
lock
|
||||
? [...new Set([...current, groupKey])]
|
||||
: current.filter((title) => title !== groupKey)
|
||||
);
|
||||
}
|
||||
|
||||
async openGroupManagement(): Promise<void> {
|
||||
// The dialog lists every group by name, locked ones included, and can
|
||||
// rewrite the locks — so it sits behind the PIN like the portal
|
||||
|
||||
+1
@@ -23,6 +23,7 @@
|
||||
[attr.aria-disabled]="!interactionEnabled()"
|
||||
[disabled]="!interactionEnabled()"
|
||||
(click)="onCategoryClick(item)"
|
||||
(contextmenu)="onCategoryContextMenu(item, $event)"
|
||||
>
|
||||
<span class="nav-item-label">
|
||||
{{
|
||||
|
||||
+13
-2
@@ -12,7 +12,7 @@ import { MatListModule } from '@angular/material/list';
|
||||
import { TranslatePipe } from '@ngx-translate/core';
|
||||
import { WorkspaceContextErrorViewComponent } from './workspace-context-error-view.component';
|
||||
|
||||
interface WorkspaceCategoryViewItem {
|
||||
export interface WorkspaceCategoryViewItem {
|
||||
readonly category_id?: string | number;
|
||||
readonly category_name?: string;
|
||||
readonly count?: number;
|
||||
@@ -28,7 +28,6 @@ interface WorkspaceCategoryViewItem {
|
||||
styleUrl: './workspace-context-category-view.component.scss',
|
||||
})
|
||||
export class WorkspaceContextCategoryViewComponent {
|
||||
|
||||
readonly items = input<ReadonlyArray<WorkspaceCategoryViewItem>>([]);
|
||||
readonly selectedCategoryId = input<string | number | null | undefined>();
|
||||
readonly itemCounts = input<Map<number, number>>(new Map());
|
||||
@@ -64,6 +63,11 @@ export class WorkspaceContextCategoryViewComponent {
|
||||
private readonly hostEl = inject(ElementRef<HTMLElement>);
|
||||
|
||||
readonly categoryClicked = output<WorkspaceCategoryViewItem>();
|
||||
/** Right-click on a row; the host decides whether a menu applies. */
|
||||
readonly categoryContextMenuRequested = output<{
|
||||
item: WorkspaceCategoryViewItem;
|
||||
event: MouseEvent;
|
||||
}>();
|
||||
|
||||
constructor() {
|
||||
effect(() => {
|
||||
@@ -128,6 +132,13 @@ export class WorkspaceContextCategoryViewComponent {
|
||||
return this.itemCounts().has(Number(item.id ?? item.category_id));
|
||||
}
|
||||
|
||||
onCategoryContextMenu(
|
||||
item: WorkspaceCategoryViewItem,
|
||||
event: MouseEvent
|
||||
): void {
|
||||
this.categoryContextMenuRequested.emit({ item, event });
|
||||
}
|
||||
|
||||
onCategoryClick(item: WorkspaceCategoryViewItem): void {
|
||||
if (!this.interactionEnabled()) {
|
||||
return;
|
||||
|
||||
+122
@@ -0,0 +1,122 @@
|
||||
import { signal } from '@angular/core';
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { ParentalLockService } from '@iptvnator/services';
|
||||
import { WorkspaceCategoryLockActionService } from './workspace-category-lock-action.service';
|
||||
|
||||
describe('WorkspaceCategoryLockActionService', () => {
|
||||
const snackBar = { open: jest.fn() };
|
||||
const parentalLock = {
|
||||
enabled: signal(true),
|
||||
requestUnlock: jest.fn(async () => true),
|
||||
lockedXtreamIds: jest.fn(() => [7]),
|
||||
lockedStalkerIds: jest.fn(() => ['9']),
|
||||
setXtreamLocks: jest.fn(async () => true),
|
||||
setStalkerLocks: jest.fn(async () => true),
|
||||
};
|
||||
let service: WorkspaceCategoryLockActionService;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
parentalLock.requestUnlock.mockResolvedValue(true);
|
||||
parentalLock.setXtreamLocks.mockResolvedValue(true);
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
{ provide: ParentalLockService, useValue: parentalLock },
|
||||
{ provide: MatSnackBar, useValue: snackBar },
|
||||
{
|
||||
provide: TranslateService,
|
||||
useValue: { instant: (key: string) => key },
|
||||
},
|
||||
],
|
||||
});
|
||||
service = TestBed.inject(WorkspaceCategoryLockActionService);
|
||||
});
|
||||
|
||||
it('reads the Xtream lock state through the provider id', () => {
|
||||
const base = {
|
||||
provider: 'xtreams' as const,
|
||||
playlistId: 'p',
|
||||
section: 'vod',
|
||||
};
|
||||
expect(
|
||||
service.isLocked({ ...base, item: { id: 1, xtream_id: 7 } })
|
||||
).toBe(true);
|
||||
expect(service.isLocked({ ...base, item: { category_id: '7' } })).toBe(
|
||||
true
|
||||
);
|
||||
expect(
|
||||
service.isLocked({ ...base, item: { id: 2, xtream_id: 8 } })
|
||||
).toBe(false);
|
||||
expect(parentalLock.lockedXtreamIds).toHaveBeenCalledWith(
|
||||
'p',
|
||||
'movies'
|
||||
);
|
||||
});
|
||||
|
||||
it('adds and removes one Xtream category behind the PIN gate', async () => {
|
||||
const target = {
|
||||
provider: 'xtreams' as const,
|
||||
playlistId: 'p',
|
||||
section: 'live',
|
||||
item: { id: 2, xtream_id: 8 },
|
||||
};
|
||||
await expect(service.setLocked(target, true)).resolves.toBe(true);
|
||||
expect(parentalLock.setXtreamLocks).toHaveBeenCalledWith(
|
||||
'p',
|
||||
'live',
|
||||
[7, 8]
|
||||
);
|
||||
|
||||
await service.setLocked(
|
||||
{ ...target, item: { id: 1, xtream_id: 7 } },
|
||||
false
|
||||
);
|
||||
expect(parentalLock.setXtreamLocks).toHaveBeenLastCalledWith(
|
||||
'p',
|
||||
'live',
|
||||
[]
|
||||
);
|
||||
});
|
||||
|
||||
it('toggles a Stalker genre and never the All pseudo-category', async () => {
|
||||
const target = {
|
||||
provider: 'stalker' as const,
|
||||
playlistId: 'p',
|
||||
section: 'itv',
|
||||
item: { category_id: '5' },
|
||||
};
|
||||
expect(
|
||||
service.isLocked({ ...target, item: { category_id: '9' } })
|
||||
).toBe(true);
|
||||
await expect(service.setLocked(target, true)).resolves.toBe(true);
|
||||
expect(parentalLock.setStalkerLocks).toHaveBeenCalledWith('p', 'itv', [
|
||||
'9',
|
||||
'5',
|
||||
]);
|
||||
await expect(
|
||||
service.setLocked({ ...target, item: { category_id: '*' } }, true)
|
||||
).resolves.toBe(false);
|
||||
});
|
||||
|
||||
it('does nothing when the PIN is refused and reports a failed write', async () => {
|
||||
const target = {
|
||||
provider: 'xtreams' as const,
|
||||
playlistId: 'p',
|
||||
section: 'live',
|
||||
item: { xtream_id: 8 },
|
||||
};
|
||||
parentalLock.requestUnlock.mockResolvedValueOnce(false);
|
||||
await expect(service.setLocked(target, true)).resolves.toBe(false);
|
||||
expect(parentalLock.setXtreamLocks).not.toHaveBeenCalled();
|
||||
|
||||
parentalLock.setXtreamLocks.mockResolvedValueOnce(false);
|
||||
await expect(service.setLocked(target, true)).resolves.toBe(false);
|
||||
expect(snackBar.open).toHaveBeenCalledWith(
|
||||
'PARENTAL_LOCK.SAVE_FAILED',
|
||||
'CLOSE',
|
||||
expect.any(Object)
|
||||
);
|
||||
});
|
||||
});
|
||||
+132
@@ -0,0 +1,132 @@
|
||||
import { inject, Injectable } from '@angular/core';
|
||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { ParentalLockService } from '@iptvnator/services';
|
||||
import {
|
||||
toParentalLockStalkerCategoryType,
|
||||
toParentalLockXtreamCategoryType,
|
||||
} from '@iptvnator/shared/interfaces';
|
||||
|
||||
export interface CategoryLockTarget {
|
||||
readonly provider: 'xtreams' | 'stalker';
|
||||
readonly playlistId: string;
|
||||
readonly section: string;
|
||||
readonly item: {
|
||||
readonly category_id?: string | number;
|
||||
readonly xtream_id?: number;
|
||||
readonly id?: string | number;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* The single-category lock toggle behind the category rail's right-click
|
||||
* menu. Same persistence as the "Manage categories" dialog (the lock store
|
||||
* is the source of truth), same PIN gate, same failure snackbar — only the
|
||||
* entry point differs.
|
||||
*/
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class WorkspaceCategoryLockActionService {
|
||||
private readonly parentalLock = inject(ParentalLockService);
|
||||
private readonly snackBar = inject(MatSnackBar);
|
||||
private readonly translate = inject(TranslateService);
|
||||
|
||||
/** The menu exists only while the feature is on. */
|
||||
readonly enabled = this.parentalLock.enabled;
|
||||
|
||||
/**
|
||||
* Xtream rows carry the provider id as `xtream_id` (Electron) or as
|
||||
* `category_id` (PWA); Stalker genres are keyed by `category_id`.
|
||||
*/
|
||||
isLocked(target: CategoryLockTarget): boolean {
|
||||
if (target.provider === 'xtreams') {
|
||||
const type = toParentalLockXtreamCategoryType(target.section);
|
||||
const id = this.xtreamProviderId(target);
|
||||
return (
|
||||
!!type &&
|
||||
id !== null &&
|
||||
this.parentalLock
|
||||
.lockedXtreamIds(target.playlistId, type)
|
||||
.includes(id)
|
||||
);
|
||||
}
|
||||
const type = toParentalLockStalkerCategoryType(target.section);
|
||||
const id = this.stalkerId(target);
|
||||
return (
|
||||
!!type &&
|
||||
id !== null &&
|
||||
this.parentalLock
|
||||
.lockedStalkerIds(target.playlistId, type)
|
||||
.includes(id)
|
||||
);
|
||||
}
|
||||
|
||||
/** Locks or unlocks one category; false when refused or failed. */
|
||||
async setLocked(
|
||||
target: CategoryLockTarget,
|
||||
locked: boolean
|
||||
): Promise<boolean> {
|
||||
if (!(await this.parentalLock.requestUnlock())) {
|
||||
return false;
|
||||
}
|
||||
const saved = await this.persist(target, locked);
|
||||
if (!saved) {
|
||||
this.snackBar.open(
|
||||
this.translate.instant('PARENTAL_LOCK.SAVE_FAILED'),
|
||||
this.translate.instant('CLOSE'),
|
||||
{ duration: 5000 }
|
||||
);
|
||||
}
|
||||
return saved;
|
||||
}
|
||||
|
||||
private persist(
|
||||
target: CategoryLockTarget,
|
||||
locked: boolean
|
||||
): Promise<boolean> {
|
||||
if (target.provider === 'xtreams') {
|
||||
const type = toParentalLockXtreamCategoryType(target.section);
|
||||
const id = this.xtreamProviderId(target);
|
||||
if (!type || id === null) {
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
const current = this.parentalLock.lockedXtreamIds(
|
||||
target.playlistId,
|
||||
type
|
||||
);
|
||||
return this.parentalLock.setXtreamLocks(
|
||||
target.playlistId,
|
||||
type,
|
||||
locked
|
||||
? [...current, id]
|
||||
: current.filter((entry) => entry !== id)
|
||||
);
|
||||
}
|
||||
const type = toParentalLockStalkerCategoryType(target.section);
|
||||
const id = this.stalkerId(target);
|
||||
if (!type || id === null) {
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
const current = this.parentalLock.lockedStalkerIds(
|
||||
target.playlistId,
|
||||
type
|
||||
);
|
||||
return this.parentalLock.setStalkerLocks(
|
||||
target.playlistId,
|
||||
type,
|
||||
locked ? [...current, id] : current.filter((entry) => entry !== id)
|
||||
);
|
||||
}
|
||||
|
||||
private xtreamProviderId(target: CategoryLockTarget): number | null {
|
||||
const id = Number(target.item.xtream_id ?? target.item.category_id);
|
||||
return Number.isFinite(id) ? id : null;
|
||||
}
|
||||
|
||||
private stalkerId(target: CategoryLockTarget): string | null {
|
||||
const raw = target.item.category_id ?? target.item.id;
|
||||
if (raw === undefined || raw === null || String(raw) === '*') {
|
||||
return null;
|
||||
}
|
||||
return String(raw);
|
||||
}
|
||||
}
|
||||
+19
-6
@@ -78,19 +78,21 @@
|
||||
<mat-icon>tune</mat-icon>
|
||||
</button>
|
||||
}
|
||||
@if (canLockStalkerCategories()) {
|
||||
@if (canManageStalkerCategories()) {
|
||||
<button
|
||||
type="button"
|
||||
mat-icon-button
|
||||
class="context-header__action"
|
||||
data-test-id="context-lock-categories"
|
||||
(click)="openLockStalkerCategories()"
|
||||
[matTooltip]="'PARENTAL_LOCK.MANAGE_LOCKS' | translate"
|
||||
data-test-id="context-manage-categories"
|
||||
(click)="openManageStalkerCategories()"
|
||||
[matTooltip]="
|
||||
'WORKSPACE.CONTEXT.MANAGE_CATEGORIES' | translate
|
||||
"
|
||||
[attr.aria-label]="
|
||||
'PARENTAL_LOCK.MANAGE_LOCKS' | translate
|
||||
'WORKSPACE.CONTEXT.MANAGE_CATEGORIES' | translate
|
||||
"
|
||||
>
|
||||
<mat-icon>lock</mat-icon>
|
||||
<mat-icon>tune</mat-icon>
|
||||
</button>
|
||||
}
|
||||
@if (canHideCategories()) {
|
||||
@@ -170,6 +172,9 @@
|
||||
[interactionEnabled]="isXtreamCategoryInteractionEnabled()"
|
||||
[statusText]="xtreamStatusText()"
|
||||
(categoryClicked)="onXtreamCategoryClicked($event)"
|
||||
(categoryContextMenuRequested)="
|
||||
onCategoryContextMenu('xtreams', $event)
|
||||
"
|
||||
/>
|
||||
}
|
||||
} @else if (isStalkerCategories()) {
|
||||
@@ -202,6 +207,9 @@
|
||||
[countDisplayMode]="stalkerCountDisplayMode()"
|
||||
[omitMissingCounts]="true"
|
||||
(categoryClicked)="onStalkerCategoryClicked($any($event))"
|
||||
(categoryContextMenuRequested)="
|
||||
onCategoryContextMenu('stalker', $event)
|
||||
"
|
||||
/>
|
||||
}
|
||||
}
|
||||
@@ -227,3 +235,8 @@
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
|
||||
<app-category-lock-menu
|
||||
#categoryLockMenu
|
||||
(toggleRequested)="onCategoryLockToggle($event)"
|
||||
/>
|
||||
+12
-8
@@ -473,9 +473,9 @@ describe('WorkspaceContextPanelComponent', () => {
|
||||
el.textContent?.includes('For adults')
|
||||
);
|
||||
|
||||
expect(documentary?.querySelector('.item-count')?.textContent).toContain(
|
||||
'190'
|
||||
);
|
||||
expect(
|
||||
documentary?.querySelector('.item-count')?.textContent
|
||||
).toContain('190');
|
||||
expect(adults?.querySelector('.item-count')).toBeNull();
|
||||
});
|
||||
|
||||
@@ -527,7 +527,9 @@ describe('WorkspaceContextPanelComponent', () => {
|
||||
|
||||
button?.click();
|
||||
|
||||
expect(liveSidebarService.stateOf('portal')()).toBe('categories-hidden');
|
||||
expect(liveSidebarService.stateOf('portal')()).toBe(
|
||||
'categories-hidden'
|
||||
);
|
||||
});
|
||||
|
||||
it('offers it for Stalker itv and radio too', () => {
|
||||
@@ -826,7 +828,9 @@ describe('WorkspaceContextPanelComponent', () => {
|
||||
) as never
|
||||
);
|
||||
|
||||
expect(fixture.componentInstance.stalkerCategoryErrorDescription()).toBe(
|
||||
expect(
|
||||
fixture.componentInstance.stalkerCategoryErrorDescription()
|
||||
).toBe(
|
||||
'PORTALS.ERROR_VIEW.STALKER_DEVICE_CONFLICT device conflict - device_id mismatch — Your STB is damaged.'
|
||||
);
|
||||
});
|
||||
@@ -836,9 +840,9 @@ describe('WorkspaceContextPanelComponent', () => {
|
||||
new StalkerPortalError('blocked', 'Account disabled') as never
|
||||
);
|
||||
|
||||
expect(fixture.componentInstance.stalkerCategoryErrorDescription()).toBe(
|
||||
'Account disabled'
|
||||
);
|
||||
expect(
|
||||
fixture.componentInstance.stalkerCategoryErrorDescription()
|
||||
).toBe('Account disabled');
|
||||
});
|
||||
});
|
||||
});
|
||||
+63
-5
@@ -35,7 +35,15 @@ import {
|
||||
toParentalLockStalkerCategoryType,
|
||||
toParentalLockXtreamCategoryType,
|
||||
} from '@iptvnator/shared/interfaces';
|
||||
import { WorkspaceContextCategoryViewComponent } from './components/workspace-context-category-view.component';
|
||||
import { CategoryLockMenuComponent } from '@iptvnator/ui/components';
|
||||
import {
|
||||
WorkspaceCategoryViewItem,
|
||||
WorkspaceContextCategoryViewComponent,
|
||||
} from './components/workspace-context-category-view.component';
|
||||
import {
|
||||
CategoryLockTarget,
|
||||
WorkspaceCategoryLockActionService,
|
||||
} from './workspace-category-lock-action.service';
|
||||
import { WorkspaceContextErrorViewComponent } from './components/workspace-context-error-view.component';
|
||||
import { hasActiveLiveCategoryRoute } from './workspace-context-panel-route.utils';
|
||||
import { WorkspaceShellContextDrawerService } from '@iptvnator/workspace/shell/util';
|
||||
@@ -63,6 +71,7 @@ interface WorkspaceCategoryLike {
|
||||
@Component({
|
||||
selector: 'app-workspace-context-panel',
|
||||
imports: [
|
||||
CategoryLockMenuComponent,
|
||||
MatIconButton,
|
||||
MatIcon,
|
||||
MatMenuModule,
|
||||
@@ -94,6 +103,12 @@ export class WorkspaceContextPanelComponent {
|
||||
);
|
||||
private readonly liveSidebarState = inject(LiveLayoutSidebarStateService);
|
||||
private readonly parentalLock = inject(ParentalLockService);
|
||||
private readonly categoryLockAction = inject(
|
||||
WorkspaceCategoryLockActionService
|
||||
);
|
||||
private readonly categoryLockMenu =
|
||||
viewChild<CategoryLockMenuComponent>('categoryLockMenu');
|
||||
private categoryLockTarget: CategoryLockTarget | null = null;
|
||||
|
||||
readonly context = input.required<WorkspaceContextRoute>();
|
||||
readonly section = input.required<string>();
|
||||
@@ -180,8 +195,12 @@ export class WorkspaceContextPanelComponent {
|
||||
readonly canManageXtreamCategories = computed(
|
||||
() => this.isXtreamCategories() && this.xtreamSelectedTypeCountsReady()
|
||||
);
|
||||
/** Stalker has no hide/show dialog; its lock dialog exists only with the lock on. */
|
||||
readonly canLockStalkerCategories = computed(
|
||||
/**
|
||||
* Stalker has no hide/show dialog, so its "Manage categories" is the
|
||||
* lock dialog alone and exists only while the lock is on — same button,
|
||||
* same icon as the Xtream rail, so the entry point is one pattern.
|
||||
*/
|
||||
readonly canManageStalkerCategories = computed(
|
||||
() =>
|
||||
this.isStalkerCategories() &&
|
||||
this.parentalLock.enabled() &&
|
||||
@@ -460,8 +479,47 @@ export class WorkspaceContextPanelComponent {
|
||||
await this.parentalLock.requestUnlock();
|
||||
}
|
||||
|
||||
async openLockStalkerCategories(): Promise<void> {
|
||||
if (!this.canLockStalkerCategories()) {
|
||||
/**
|
||||
* Right-click accelerator: one row's lock toggled without the dialog.
|
||||
* Silently no menu while the feature is off — the row keeps its
|
||||
* ordinary behaviour.
|
||||
*/
|
||||
onCategoryContextMenu(
|
||||
provider: 'xtreams' | 'stalker',
|
||||
request: { item: WorkspaceCategoryViewItem; event: MouseEvent }
|
||||
): void {
|
||||
if (!this.categoryLockAction.enabled()) {
|
||||
return;
|
||||
}
|
||||
const target: CategoryLockTarget = {
|
||||
provider,
|
||||
playlistId: this.context().playlistId,
|
||||
section: this.section(),
|
||||
item: request.item,
|
||||
};
|
||||
if (
|
||||
provider === 'stalker' &&
|
||||
String(request.item.category_id ?? request.item.id) === '*'
|
||||
) {
|
||||
return;
|
||||
}
|
||||
this.categoryLockTarget = target;
|
||||
this.categoryLockMenu()?.open(
|
||||
request.event,
|
||||
this.categoryLockAction.isLocked(target)
|
||||
);
|
||||
}
|
||||
|
||||
async onCategoryLockToggle(locked: boolean): Promise<void> {
|
||||
const target = this.categoryLockTarget;
|
||||
this.categoryLockTarget = null;
|
||||
if (target) {
|
||||
await this.categoryLockAction.setLocked(target, locked);
|
||||
}
|
||||
}
|
||||
|
||||
async openManageStalkerCategories(): Promise<void> {
|
||||
if (!this.canManageStalkerCategories()) {
|
||||
return;
|
||||
}
|
||||
if (!(await this.parentalLock.requestUnlock())) {
|
||||
|
||||
Reference in new issue
Block a user