diff --git a/apps/web/src/assets/i18n/ar.json b/apps/web/src/assets/i18n/ar.json index c8affaae4..451e8e65b 100644 --- a/apps/web/src/assets/i18n/ar.json +++ b/apps/web/src/assets/i18n/ar.json @@ -128,6 +128,14 @@ "AUTO_REFRESH_UPDATE_SUCCESS": "نجاح! تم تحديث قوائم التشغيل بنجاح", "AUTO_REFRESH_ENABLED": "تم تفعيل التحديث التلقائي" }, + "ADD_PLAYLIST": { + "PICK_METHOD": "اختر طريقة الاتصال، ويمكنك إعادة تسميتها لاحقًا.", + "METHOD_URL_SUB": "ألصق رابطًا لملف .m3u أو .m3u8 بعيد", + "METHOD_FILE_SUB": "حمّل قائمة تشغيل من جهازك", + "METHOD_XTREAM_SUB": "اتصل باستخدام المضيف واسم المستخدم وكلمة المرور", + "METHOD_STALKER_SUB": "عنوان MAC ورابط البوابة", + "METHOD_TEXT_SUB": "للقوائم العابرة أو عندما يتوفر لديك النص فقط" + }, "FILE_UPLOAD": { "DRAG_DROP": "اسحب وأفلت الملفات هنا", "OR": "أو", diff --git a/apps/web/src/assets/i18n/ary.json b/apps/web/src/assets/i18n/ary.json index 2f77bec25..3152339a4 100644 --- a/apps/web/src/assets/i18n/ary.json +++ b/apps/web/src/assets/i18n/ary.json @@ -128,6 +128,14 @@ "AUTO_REFRESH_UPDATE_SUCCESS": "نجاح! قوائم التشغيل تحدثات بنجاح", "AUTO_REFRESH_ENABLED": "التحديث التلقائي مفعل" }, + "ADD_PLAYLIST": { + "PICK_METHOD": "اختار كيفاش بغيتي تربط. تقدر تبدل الاسم من بعد.", + "METHOD_URL_SUB": "لصق رابط ل ملف .m3u ولا .m3u8 بعيد", + "METHOD_FILE_SUB": "زيد قائمة تشغيل من الجهاز ديالك", + "METHOD_XTREAM_SUB": "ربط بالخادم، اسم المستخدم وكلمة المرور", + "METHOD_STALKER_SUB": "عنوان MAC ورابط البوابة", + "METHOD_TEXT_SUB": "للقوائم المؤقتة ولا إلى عندك غير النص" + }, "FILE_UPLOAD": { "DRAG_DROP": "جر وأفلت الملفات هنا", "OR": "أو", diff --git a/apps/web/src/assets/i18n/by.json b/apps/web/src/assets/i18n/by.json index 093ecfdcc..a4881a1ec 100644 --- a/apps/web/src/assets/i18n/by.json +++ b/apps/web/src/assets/i18n/by.json @@ -128,6 +128,14 @@ "AUTO_REFRESH_UPDATE_SUCCESS": "Гатова! Плэйлісты былі паспяхова абноўлены", "AUTO_REFRESH_ENABLED": "Аўтаабнаўленне ўключана" }, + "ADD_PLAYLIST": { + "PICK_METHOD": "Выберыце, як вы хочаце падключыцца. Перайменаваць можна пазней.", + "METHOD_URL_SUB": "Устаўце спасылку на выдалены .m3u або .m3u8", + "METHOD_FILE_SUB": "Загрузіце плэйліст са свайго камп'ютара", + "METHOD_XTREAM_SUB": "Падключэнне праз хост, імя карыстальніка і пароль", + "METHOD_STALKER_SUB": "MAC-адрас і URL партала", + "METHOD_TEXT_SUB": "Для разавых спісаў або калі ў вас ёсць толькі тэкст" + }, "FILE_UPLOAD": { "DRAG_DROP": "Перацягніце файл у гэтую вобласць", "OR": "або", diff --git a/apps/web/src/assets/i18n/de.json b/apps/web/src/assets/i18n/de.json index 35e04c7e9..257aa0d7e 100644 --- a/apps/web/src/assets/i18n/de.json +++ b/apps/web/src/assets/i18n/de.json @@ -128,6 +128,14 @@ "AUTO_REFRESH_UPDATE_SUCCESS": "Erfolg! Die Playlists wurden erfolgreich aktualisiert", "AUTO_REFRESH_ENABLED": "Auto-Refresh aktiviert" }, + "ADD_PLAYLIST": { + "PICK_METHOD": "Wählen Sie aus, wie Sie sich verbinden möchten. Sie können den Namen später ändern.", + "METHOD_URL_SUB": "Fügen Sie einen Link zu einer .m3u- oder .m3u8-Datei ein", + "METHOD_FILE_SUB": "Laden Sie eine Wiedergabeliste von Ihrem Computer hoch", + "METHOD_XTREAM_SUB": "Verbinden mit Host, Benutzername und Passwort", + "METHOD_STALKER_SUB": "MAC-Adresse und Portal-URL", + "METHOD_TEXT_SUB": "Für einmalige Listen oder wenn Sie nur Text haben" + }, "FILE_UPLOAD": { "DRAG_DROP": "Ziehen Sie Dateien per Drag & Drop", "OR": "oder", diff --git a/apps/web/src/assets/i18n/el.json b/apps/web/src/assets/i18n/el.json index 1c37a158d..c0920bff8 100644 --- a/apps/web/src/assets/i18n/el.json +++ b/apps/web/src/assets/i18n/el.json @@ -128,6 +128,14 @@ "AUTO_REFRESH_UPDATE_SUCCESS": "Επιτυχία! Οι λίστες αναπαραγωγής ενημερώθηκαν με επιτυχία", "AUTO_REFRESH_ENABLED": "Η αυτόματη ανανέωση είναι ενεργοποιημένη" }, + "ADD_PLAYLIST": { + "PICK_METHOD": "Επιλέξτε τρόπο σύνδεσης. Μπορείτε να τη μετονομάσετε αργότερα.", + "METHOD_URL_SUB": "Επικολλήστε έναν σύνδεσμο σε απομακρυσμένο .m3u ή .m3u8", + "METHOD_FILE_SUB": "Ανεβάστε μια λίστα αναπαραγωγής από τον υπολογιστή σας", + "METHOD_XTREAM_SUB": "Σύνδεση με διακομιστή, όνομα χρήστη και κωδικό πρόσβασης", + "METHOD_STALKER_SUB": "MAC address και URL πύλης", + "METHOD_TEXT_SUB": "Για λίστες μίας χρήσης ή όταν έχετε μόνο κείμενο" + }, "FILE_UPLOAD": { "DRAG_DROP": "Σύρετε και αποθέστε αρχεία εδώ", "OR": "or", diff --git a/apps/web/src/assets/i18n/en.json b/apps/web/src/assets/i18n/en.json index d3a739b52..e97319201 100644 --- a/apps/web/src/assets/i18n/en.json +++ b/apps/web/src/assets/i18n/en.json @@ -128,6 +128,14 @@ "AUTO_REFRESH_UPDATE_SUCCESS": "Success! The playlists were successfully updated", "AUTO_REFRESH_ENABLED": "Auto-refresh enabled" }, + "ADD_PLAYLIST": { + "PICK_METHOD": "Pick how you want to connect. You can rename it later.", + "METHOD_URL_SUB": "Paste a link to a remote .m3u or .m3u8", + "METHOD_FILE_SUB": "Upload a playlist from your computer", + "METHOD_XTREAM_SUB": "Connect with host, username and password", + "METHOD_STALKER_SUB": "MAC address and portal URL", + "METHOD_TEXT_SUB": "For one-off lists or when you only have text" + }, "FILE_UPLOAD": { "DRAG_DROP": "Drop an .m3u or .m3u8 file here", "OR": "or", diff --git a/apps/web/src/assets/i18n/es.json b/apps/web/src/assets/i18n/es.json index 1d98ff72d..299aebf11 100644 --- a/apps/web/src/assets/i18n/es.json +++ b/apps/web/src/assets/i18n/es.json @@ -128,6 +128,14 @@ "AUTO_REFRESH_UPDATE_SUCCESS": "¡Listo! Las listas de reproducción se actualizaron correctamente", "AUTO_REFRESH_ENABLED": "Auto-actualización activada" }, + "ADD_PLAYLIST": { + "PICK_METHOD": "Elige cómo quieres conectar. Puedes renombrarla más tarde.", + "METHOD_URL_SUB": "Pega un enlace a un archivo .m3u o .m3u8 remoto", + "METHOD_FILE_SUB": "Sube una lista de reproducción desde tu computadora", + "METHOD_XTREAM_SUB": "Conecta con host, usuario y contraseña", + "METHOD_STALKER_SUB": "Dirección MAC y URL del portal", + "METHOD_TEXT_SUB": "Para listas puntuales o cuando solo tienes texto" + }, "FILE_UPLOAD": { "DRAG_DROP": "Arrastre & suelte archivos aquí", "OR": "o", diff --git a/apps/web/src/assets/i18n/fr.json b/apps/web/src/assets/i18n/fr.json index 4164c6b39..5cd91e573 100644 --- a/apps/web/src/assets/i18n/fr.json +++ b/apps/web/src/assets/i18n/fr.json @@ -128,6 +128,14 @@ "AUTO_REFRESH_UPDATE_SUCCESS": "Succès ! Les listes de lecture ont été mises à jour avec succès", "AUTO_REFRESH_ENABLED": "Actualisation automatique activée" }, + "ADD_PLAYLIST": { + "PICK_METHOD": "Choisissez comment vous souhaitez vous connecter. Vous pourrez renommer la liste plus tard.", + "METHOD_URL_SUB": "Collez un lien vers un fichier .m3u ou .m3u8 distant", + "METHOD_FILE_SUB": "Téléversez une liste de lecture depuis votre ordinateur", + "METHOD_XTREAM_SUB": "Connectez-vous avec hôte, nom d'utilisateur et mot de passe", + "METHOD_STALKER_SUB": "Adresse MAC et URL du portail", + "METHOD_TEXT_SUB": "Pour les listes ponctuelles ou quand vous n'avez que du texte" + }, "FILE_UPLOAD": { "DRAG_DROP": "Glissez un fichier ici", "OR": "ou", diff --git a/apps/web/src/assets/i18n/it.json b/apps/web/src/assets/i18n/it.json index 88524018b..c18d2f6d3 100644 --- a/apps/web/src/assets/i18n/it.json +++ b/apps/web/src/assets/i18n/it.json @@ -128,6 +128,14 @@ "AUTO_REFRESH_UPDATE_SUCCESS": "Le playlist sono state aggiornate correttamente", "AUTO_REFRESH_ENABLED": "Aggiornamento automatico attivo" }, + "ADD_PLAYLIST": { + "PICK_METHOD": "Scegli come connetterti. Puoi rinominarla in seguito.", + "METHOD_URL_SUB": "Incolla un link a un file .m3u o .m3u8 remoto", + "METHOD_FILE_SUB": "Carica una playlist dal tuo computer", + "METHOD_XTREAM_SUB": "Connetti con host, nome utente e password", + "METHOD_STALKER_SUB": "Indirizzo MAC e URL del portale", + "METHOD_TEXT_SUB": "Per liste occasionali o quando hai solo il testo" + }, "FILE_UPLOAD": { "DRAG_DROP": "Trascina e rilascia qui i file", "OR": "o", diff --git a/apps/web/src/assets/i18n/ja.json b/apps/web/src/assets/i18n/ja.json index 46e9283fc..d4220a6dd 100644 --- a/apps/web/src/assets/i18n/ja.json +++ b/apps/web/src/assets/i18n/ja.json @@ -128,6 +128,14 @@ "AUTO_REFRESH_UPDATE_SUCCESS": "成功!プレイリストが正常に更新されました", "AUTO_REFRESH_ENABLED": "自動更新が有効です" }, + "ADD_PLAYLIST": { + "PICK_METHOD": "接続方法を選択してください。名前は後から変更できます。", + "METHOD_URL_SUB": "リモートの.m3uまたは.m3u8へのリンクを貼り付け", + "METHOD_FILE_SUB": "コンピューターからプレイリストをアップロード", + "METHOD_XTREAM_SUB": "ホスト、ユーザー名、パスワードで接続", + "METHOD_STALKER_SUB": "MACアドレスとポータルURL", + "METHOD_TEXT_SUB": "一度限りのリストやテキストしかない場合に" + }, "FILE_UPLOAD": { "DRAG_DROP": "ここにファイルをドラッグ&ドロップ", "OR": "または", diff --git a/apps/web/src/assets/i18n/ko.json b/apps/web/src/assets/i18n/ko.json index 7f0858fe4..3b5e8a7b2 100644 --- a/apps/web/src/assets/i18n/ko.json +++ b/apps/web/src/assets/i18n/ko.json @@ -128,6 +128,14 @@ "AUTO_REFRESH_UPDATE_SUCCESS": "성공! 재생목록이 성공적으로 업데이트되었습니다", "AUTO_REFRESH_ENABLED": "자동 새로 고침 활성화됨" }, + "ADD_PLAYLIST": { + "PICK_METHOD": "연결 방식을 선택하세요. 이름은 나중에 변경할 수 있습니다.", + "METHOD_URL_SUB": "원격 .m3u 또는 .m3u8 링크를 붙여넣으세요", + "METHOD_FILE_SUB": "내 컴퓨터에서 재생목록을 업로드하세요", + "METHOD_XTREAM_SUB": "호스트, 사용자 이름, 비밀번호로 연결하세요", + "METHOD_STALKER_SUB": "MAC 주소와 포털 URL", + "METHOD_TEXT_SUB": "일회성 목록이나 텍스트만 있을 때 사용하세요" + }, "FILE_UPLOAD": { "DRAG_DROP": "파일을 드래그 앤 드롭", "OR": "또는", diff --git a/apps/web/src/assets/i18n/nl.json b/apps/web/src/assets/i18n/nl.json index 2a086f612..a8e4f6b0d 100644 --- a/apps/web/src/assets/i18n/nl.json +++ b/apps/web/src/assets/i18n/nl.json @@ -128,6 +128,14 @@ "AUTO_REFRESH_UPDATE_SUCCESS": "Gelukt! De afspeellijsten zijn succesvol bijgewerkt", "AUTO_REFRESH_ENABLED": "Automatisch vernieuwen ingeschakeld" }, + "ADD_PLAYLIST": { + "PICK_METHOD": "Kies hoe je verbinding wilt maken. Je kunt het later hernoemen.", + "METHOD_URL_SUB": "Plak een link naar een externe .m3u of .m3u8", + "METHOD_FILE_SUB": "Upload een afspeellijst vanaf je computer", + "METHOD_XTREAM_SUB": "Maak verbinding met host, gebruikersnaam en wachtwoord", + "METHOD_STALKER_SUB": "MAC-adres en portal-URL", + "METHOD_TEXT_SUB": "Voor eenmalige lijsten of als je alleen tekst hebt" + }, "FILE_UPLOAD": { "DRAG_DROP": "Sleep bestanden hierheen", "OR": "of", diff --git a/apps/web/src/assets/i18n/pl.json b/apps/web/src/assets/i18n/pl.json index 218b610ae..3b6d4176c 100644 --- a/apps/web/src/assets/i18n/pl.json +++ b/apps/web/src/assets/i18n/pl.json @@ -128,6 +128,14 @@ "AUTO_REFRESH_UPDATE_SUCCESS": "Sukces! Listy odtwarzania zostały pomyślnie zaktualizowane", "AUTO_REFRESH_ENABLED": "Automatyczne odświeżanie włączone" }, + "ADD_PLAYLIST": { + "PICK_METHOD": "Wybierz sposób połączenia. Nazwę można zmienić później.", + "METHOD_URL_SUB": "Wklej link do zdalnego pliku .m3u lub .m3u8", + "METHOD_FILE_SUB": "Prześlij listę odtwarzania z komputera", + "METHOD_XTREAM_SUB": "Połącz przy użyciu hosta, nazwy użytkownika i hasła", + "METHOD_STALKER_SUB": "Adres MAC i URL portalu", + "METHOD_TEXT_SUB": "Do jednorazowych list lub gdy dostępny jest tylko tekst" + }, "FILE_UPLOAD": { "DRAG_DROP": "Przeciągnij i upuść pliki tutaj", "OR": "lub", diff --git a/apps/web/src/assets/i18n/pt.json b/apps/web/src/assets/i18n/pt.json index a8066d7d9..04cbbc308 100644 --- a/apps/web/src/assets/i18n/pt.json +++ b/apps/web/src/assets/i18n/pt.json @@ -128,6 +128,14 @@ "AUTO_REFRESH_UPDATE_SUCCESS": "Sucesso! As playlists foram atualizadas com sucesso", "AUTO_REFRESH_ENABLED": "Atualização automática ativada" }, + "ADD_PLAYLIST": { + "PICK_METHOD": "Escolha como você quer se conectar. Você pode renomeá-la depois.", + "METHOD_URL_SUB": "Cole um link para um arquivo .m3u ou .m3u8 remoto", + "METHOD_FILE_SUB": "Envie uma playlist do seu computador", + "METHOD_XTREAM_SUB": "Conecte-se com host, nome de usuário e senha", + "METHOD_STALKER_SUB": "Endereço MAC e URL do portal", + "METHOD_TEXT_SUB": "Para listas pontuais ou quando você só tem o texto" + }, "FILE_UPLOAD": { "DRAG_DROP": "Arraste e solte os arquivos aqui", "OR": "ou", diff --git a/apps/web/src/assets/i18n/ru.json b/apps/web/src/assets/i18n/ru.json index 301da391e..e32f8a2fc 100644 --- a/apps/web/src/assets/i18n/ru.json +++ b/apps/web/src/assets/i18n/ru.json @@ -128,6 +128,14 @@ "AUTO_REFRESH_UPDATE_SUCCESS": "Успешно! Плейлисты были успешно обновлены", "AUTO_REFRESH_ENABLED": "Автообновление включено" }, + "ADD_PLAYLIST": { + "PICK_METHOD": "Выберите способ подключения. Переименовать плейлист можно позже.", + "METHOD_URL_SUB": "Вставьте ссылку на удалённый файл .m3u или .m3u8", + "METHOD_FILE_SUB": "Загрузите плейлист с вашего компьютера", + "METHOD_XTREAM_SUB": "Подключение через сервер, имя пользователя и пароль", + "METHOD_STALKER_SUB": "MAC-адрес и URL портала", + "METHOD_TEXT_SUB": "Для разовых плейлистов или когда есть только текст" + }, "FILE_UPLOAD": { "DRAG_DROP": "Перетащите файл в эту область", "OR": "или", diff --git a/apps/web/src/assets/i18n/tr.json b/apps/web/src/assets/i18n/tr.json index db31ba810..993229767 100644 --- a/apps/web/src/assets/i18n/tr.json +++ b/apps/web/src/assets/i18n/tr.json @@ -128,6 +128,14 @@ "AUTO_REFRESH_UPDATE_SUCCESS": "Başarılı! Oynatma listeleri başarıyla güncellendi", "AUTO_REFRESH_ENABLED": "Otomatik yenileme etkin" }, + "ADD_PLAYLIST": { + "PICK_METHOD": "Nasıl bağlanmak istediğinizi seçin. Daha sonra yeniden adlandırabilirsiniz.", + "METHOD_URL_SUB": "Uzak bir .m3u veya .m3u8 bağlantısını yapıştırın", + "METHOD_FILE_SUB": "Bilgisayarınızdan bir oynatma listesi yükleyin", + "METHOD_XTREAM_SUB": "Sunucu, kullanıcı adı ve parola ile bağlanın", + "METHOD_STALKER_SUB": "MAC adresi ve portal URL'si", + "METHOD_TEXT_SUB": "Tek seferlik listeler veya yalnızca metniniz olduğunda" + }, "FILE_UPLOAD": { "DRAG_DROP": "Dosyaları buraya sürükleyip bırakın", "OR": "veya", diff --git a/apps/web/src/assets/i18n/zh.json b/apps/web/src/assets/i18n/zh.json index 9d9f612fc..66e2fd089 100644 --- a/apps/web/src/assets/i18n/zh.json +++ b/apps/web/src/assets/i18n/zh.json @@ -128,6 +128,14 @@ "AUTO_REFRESH_UPDATE_SUCCESS": "成功!播放列表已成功更新", "AUTO_REFRESH_ENABLED": "已启用自动刷新" }, + "ADD_PLAYLIST": { + "PICK_METHOD": "选择您想要的连接方式。稍后可以重命名。", + "METHOD_URL_SUB": "粘贴远程 .m3u 或 .m3u8 的链接", + "METHOD_FILE_SUB": "从您的电脑上传播放列表", + "METHOD_XTREAM_SUB": "使用主机、用户名和密码连接", + "METHOD_STALKER_SUB": "MAC 地址和门户 URL", + "METHOD_TEXT_SUB": "用于一次性列表,或仅有文本时" + }, "FILE_UPLOAD": { "DRAG_DROP": "将文件拖放到此处", "OR": "或", diff --git a/apps/web/src/assets/i18n/zhtw.json b/apps/web/src/assets/i18n/zhtw.json index 971cb0b2e..4b554a0ce 100644 --- a/apps/web/src/assets/i18n/zhtw.json +++ b/apps/web/src/assets/i18n/zhtw.json @@ -128,6 +128,14 @@ "AUTO_REFRESH_UPDATE_SUCCESS": "成功!播放清單已成功更新", "AUTO_REFRESH_ENABLED": "已啟用自動重新整理" }, + "ADD_PLAYLIST": { + "PICK_METHOD": "請選擇您要使用的連線方式,之後仍可重新命名。", + "METHOD_URL_SUB": "貼上遠端 .m3u 或 .m3u8 的連結", + "METHOD_FILE_SUB": "從您的電腦上傳播放清單", + "METHOD_XTREAM_SUB": "使用主機位址、帳號和密碼連線", + "METHOD_STALKER_SUB": "MAC 位址與入口網址", + "METHOD_TEXT_SUB": "適合一次性清單,或您只有文字內容時使用" + }, "FILE_UPLOAD": { "DRAG_DROP": "在此處拖放檔案", "OR": "或者", diff --git a/libs/playlist/import/feature/src/lib/add-playlist-dialog/add-playlist-dialog.component.html b/libs/playlist/import/feature/src/lib/add-playlist-dialog/add-playlist-dialog.component.html index e1cb858d0..c78c7c66f 100644 --- a/libs/playlist/import/feature/src/lib/add-playlist-dialog/add-playlist-dialog.component.html +++ b/libs/playlist/import/feature/src/lib/add-playlist-dialog/add-playlist-dialog.component.html @@ -1,49 +1,41 @@
-

- {{ 'HOME.URL_UPLOAD.ADD_PLAYLIST' | translate }} -

+
+

+ {{ 'HOME.URL_UPLOAD.ADD_PLAYLIST' | translate }} +

+

+ {{ 'HOME.ADD_PLAYLIST.PICK_METHOD' | translate }} +

+
- @for (opt of categoryOptions; track opt.value) { + @for (opt of methodOptions; track opt.value) { }
- @if (category() === 'm3u') { -
- @for (opt of subtypeOptions; track opt.value) { - - } -
- } -
@switch (playlistType()) { @case ('file') { diff --git a/libs/playlist/import/feature/src/lib/add-playlist-dialog/add-playlist-dialog.component.scss b/libs/playlist/import/feature/src/lib/add-playlist-dialog/add-playlist-dialog.component.scss index a2ba853f4..3662ac6e7 100644 --- a/libs/playlist/import/feature/src/lib/add-playlist-dialog/add-playlist-dialog.component.scss +++ b/libs/playlist/import/feature/src/lib/add-playlist-dialog/add-playlist-dialog.component.scss @@ -17,53 +17,92 @@ ); } -// ─── Segmented control ────────────────────────────────────────────────────── -.apd-segmented { - display: grid; - grid-auto-flow: column; - grid-auto-columns: 1fr; - background: var(--apd-segment-track); - border-radius: 12px; - padding: 4px; - gap: 2px; - margin-bottom: 14px; - - &--sub { - background: color-mix( - in srgb, - var(--apd-segment-track) 85%, - transparent - ); - } +// ─── Dialog head (title + helper subtitle) ────────────────────────────────── +.apd-head { + display: flex; + flex-direction: column; + gap: 4px; + margin: 0 0 8px; + padding: 0; } -.apd-segment { - appearance: none; - border: 0; - background: transparent; - color: var(--apd-body); - padding: 8px 14px; - border-radius: 9px; - font: inherit; +.apd-title { + margin: 0; + font-size: 20px; + font-weight: 600; + letter-spacing: -0.01em; + color: var(--apd-heading); +} + +.apd-sub { + margin: 0; font-size: 13px; - font-weight: 500; + color: var(--apd-body); + line-height: 1.45; +} + +// ─── Method grid (5 flat cards instead of 2-level segmented control) ──────── +// One card per source method. Replaces the prior 3×3 type×subtype matrix +// which had 9 cells of which only 5 were real (Xtream-via-file, etc. didn't +// exist). Each card is self-describing — icon + label + one-line sub. +.apd-method-grid { + display: grid; + grid-template-columns: repeat(5, minmax(0, 1fr)); + gap: 10px; + margin: 4px 0 16px; +} + +.apd-method { + appearance: none; + background: var(--apd-segment-track); + border: 1px solid var(--apd-border); + border-radius: 10px; + padding: 12px 12px 14px; + display: flex; + flex-direction: column; + gap: 6px; cursor: pointer; - line-height: 1.3; + text-align: left; + font: inherit; + color: var(--apd-heading); + min-width: 0; transition: background-color 160ms ease, - color 160ms ease, + border-color 160ms ease, box-shadow 160ms ease; &:hover:not(.active) { - color: var(--apd-heading); + background: var(--apd-segment-active); + border-color: color-mix(in srgb, var(--apd-accent) 32%, transparent); + + .apd-method__icon { + background: color-mix( + in srgb, + var(--apd-accent) 14%, + transparent + ); + color: var(--apd-accent); + } } &.active { - background: var(--apd-segment-active); - color: var(--apd-heading); - box-shadow: - 0 1px 2px rgba(0, 0, 0, 0.25), - inset 0 0 0 1px color-mix(in srgb, white 6%, transparent); + background: color-mix(in srgb, var(--apd-accent) 12%, transparent); + border-color: var(--apd-accent); + box-shadow: 0 1px 2px + color-mix(in srgb, var(--apd-accent) 32%, transparent); + + .apd-method__icon { + background: color-mix( + in srgb, + var(--apd-accent) 22%, + transparent + ); + color: var(--apd-accent); + } + + .apd-method__title { + color: var(--apd-accent); + } } &:focus-visible { @@ -72,6 +111,59 @@ } } +.apd-method__icon { + width: 32px; + height: 32px; + display: grid; + place-items: center; + border-radius: 8px; + background: color-mix( + in srgb, + var(--apd-surface-raised) 75%, + transparent + ); + color: var(--apd-muted); + transition: + background-color 160ms ease, + color 160ms ease; + + mat-icon { + font-size: 18px; + width: 18px; + height: 18px; + line-height: 18px; + } +} + +.apd-method__title { + font-size: 13px; + font-weight: 600; + line-height: 1.25; + color: var(--apd-heading); + transition: color 160ms ease; +} + +.apd-method__sub { + font-size: 11.5px; + color: var(--apd-muted); + line-height: 1.4; +} + +// On narrow widths the 5-column grid wraps to 2 columns to stay legible. +// 480px keeps the dialog usable down to small modals; below that it stacks +// fully. +@media (max-width: 720px) { + .apd-method-grid { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } +} + +@media (max-width: 480px) { + .apd-method-grid { + grid-template-columns: minmax(0, 1fr); + } +} + // ─── Form area ────────────────────────────────────────────────────────────── .apd-form-area { min-height: 0; diff --git a/libs/playlist/import/feature/src/lib/add-playlist-dialog/add-playlist-dialog.component.spec.ts b/libs/playlist/import/feature/src/lib/add-playlist-dialog/add-playlist-dialog.component.spec.ts index 35d82a259..ff90595af 100644 --- a/libs/playlist/import/feature/src/lib/add-playlist-dialog/add-playlist-dialog.component.spec.ts +++ b/libs/playlist/import/feature/src/lib/add-playlist-dialog/add-playlist-dialog.component.spec.ts @@ -178,18 +178,55 @@ describe('AddPlaylistDialogComponent', () => { expect(component.isClearDisabled()).toBeTruthy(); }); + it('defaults to the URL method when no deep-link type is provided', () => { + // Constructor-injected MAT_DIALOG_DATA is null in this spec setup, + // so the dialog should land on its default first-row method. + expect(component.method()).toBe('url'); + expect(component.playlistType()).toBe('url'); + }); + + it.each([ + ['url' as PlaylistType, 'url' as PlaylistType], + ['file' as PlaylistType, 'file' as PlaylistType], + ['text' as PlaylistType, 'text' as PlaylistType], + ['xtream' as PlaylistType, 'xtream' as PlaylistType], + ['stalker' as PlaylistType, 'stalker' as PlaylistType], + ])( + 'opens directly on the %s method when MAT_DIALOG_DATA.type === %s (deep-link from rail / shell action)', + (deepLinkType, expectedMethod) => { + // Rebuild the component with a deep-link to confirm the + // flat-method signal honours the legacy { type } payload shape. + TestBed.resetTestingModule(); + TestBed.configureTestingModule({ + providers: [ + { provide: DataService, useValue: dataService }, + { provide: MatDialogRef, useValue: dialogRef }, + { provide: Store, useValue: store }, + { provide: MatSnackBar, useValue: { open: jest.fn() } }, + { + provide: TranslateService, + useValue: { + instant: jest.fn((value: string) => value), + }, + }, + { + provide: MAT_DIALOG_DATA, + useValue: { type: deepLinkType }, + }, + ], + }); + const deepLinked = TestBed.runInInjectionContext( + () => new AddPlaylistDialogComponent() + ); + + expect(deepLinked.method()).toBe(expectedMethod); + expect(deepLinked.playlistType()).toBe(expectedMethod); + } + ); + function selectType(type: PlaylistType): void { - if (type === 'xtream') { - component.category.set('xtream'); - return; - } - - if (type === 'stalker') { - component.category.set('stalker'); - return; - } - - component.category.set('m3u'); - component.m3uSubType.set(type); + // The dialog now uses a single `method` signal across all 5 source + // types — no more category × subtype matrix. + component.method.set(type); } }); diff --git a/libs/playlist/import/feature/src/lib/add-playlist-dialog/add-playlist-dialog.component.ts b/libs/playlist/import/feature/src/lib/add-playlist-dialog/add-playlist-dialog.component.ts index 1a6202b8e..fa6d86330 100644 --- a/libs/playlist/import/feature/src/lib/add-playlist-dialog/add-playlist-dialog.component.ts +++ b/libs/playlist/import/feature/src/lib/add-playlist-dialog/add-playlist-dialog.component.ts @@ -12,6 +12,7 @@ import { MatDialogModule, MatDialogRef, } from '@angular/material/dialog'; +import { MatIcon } from '@angular/material/icon'; import { MatSnackBar } from '@angular/material/snack-bar'; import { Store } from '@ngrx/store'; import { TranslateModule, TranslateService } from '@ngx-translate/core'; @@ -25,17 +26,16 @@ import { TextImportComponent } from '../text-import/text-import.component'; import { UrlUploadComponent } from '../url-upload/url-upload.component'; import { XtreamCodeImportComponent } from '../xtream-code-import/xtream-code-import.component'; -type PlaylistCategory = 'm3u' | Extract; -type M3uSubType = Extract; - -interface CategoryOption { - value: PlaylistCategory; - label: string; -} - -interface SubtypeOption { - value: M3uSubType; +/** + * Flat 5-method option model — replaces the prior category × subtype matrix + * (M3U/Xtream/Stalker × URL/File/Text) which created 9 combinations of which + * only 5 were real. Now each entry IS a method, no nesting. + */ +export interface PlaylistMethodOption { + value: PlaylistType; + icon: string; labelKey: string; + subKey: string; } @Component({ @@ -43,6 +43,7 @@ interface SubtypeOption { FileUploadComponent, MatButtonModule, MatDialogModule, + MatIcon, StalkerPortalImportComponent, TextImportComponent, TranslateModule, @@ -70,42 +71,53 @@ export class AddPlaylistDialogComponent { readonly xtreamImport = viewChild(XtreamCodeImportComponent); readonly stalkerImport = viewChild(StalkerPortalImportComponent); - readonly category = signal('m3u'); - readonly m3uSubType = signal('url'); + readonly method = signal('url'); - readonly categoryOptions: CategoryOption[] = [ - { value: 'm3u', label: 'M3U' }, - { value: 'xtream', label: 'Xtream' }, - { value: 'stalker', label: 'Stalker' }, + // Order matches the v0.22 mockup left-to-right: URL first (Most common), + // then File, Xtream credentials, Stalker portal, raw text paste. Each + // entry stands on its own — no nested subtypes. + readonly methodOptions: PlaylistMethodOption[] = [ + { + value: 'url', + icon: 'public', + labelKey: 'HOME.TABS.URL_UPLOAD', + subKey: 'HOME.ADD_PLAYLIST.METHOD_URL_SUB', + }, + { + value: 'file', + icon: 'folder_open', + labelKey: 'HOME.TABS.FILE_UPLOAD', + subKey: 'HOME.ADD_PLAYLIST.METHOD_FILE_SUB', + }, + { + value: 'xtream', + icon: 'vpn_key', + labelKey: 'HOME.TABS.XTREME_IMPORT', + subKey: 'HOME.ADD_PLAYLIST.METHOD_XTREAM_SUB', + }, + { + value: 'stalker', + icon: 'cast', + labelKey: 'HOME.TABS.STALKER_PORTAL_IMPORT', + subKey: 'HOME.ADD_PLAYLIST.METHOD_STALKER_SUB', + }, + { + value: 'text', + icon: 'subject', + labelKey: 'HOME.TABS.TEXT_IMPORT', + subKey: 'HOME.ADD_PLAYLIST.METHOD_TEXT_SUB', + }, ]; - readonly subtypeOptions: SubtypeOption[] = [ - { value: 'url', labelKey: 'HOME.TABS.URL_UPLOAD' }, - { value: 'file', labelKey: 'HOME.TABS.FILE_UPLOAD' }, - { value: 'text', labelKey: 'HOME.TABS.TEXT_IMPORT' }, - ]; - - readonly playlistType = computed(() => { - const cat = this.category(); - if (cat === 'xtream') return 'xtream'; - if (cat === 'stalker') return 'stalker'; - return this.m3uSubType(); - }); + /** + * Backwards-compatible alias. The template's @switch and the action + * buttons key off this; keeping the name avoids churn in 5 case branches. + */ + readonly playlistType = computed(() => this.method()); constructor() { if (this.data?.type) { - this.initFromType(this.data.type); - } - } - - private initFromType(type: PlaylistType): void { - if (type === 'xtream') { - this.category.set('xtream'); - } else if (type === 'stalker') { - this.category.set('stalker'); - } else { - this.category.set('m3u'); - this.m3uSubType.set(type as M3uSubType); + this.method.set(this.data.type); } }