From 7028ff404988b449069d19f766799834a4f1b296 Mon Sep 17 00:00:00 2001 From: 4gray Date: Mon, 13 Oct 2025 00:24:54 +0200 Subject: [PATCH] refactor(nx): app restructuring --- .devcontainer/Dockerfile | 16 - .devcontainer/devcontainer.json | 42 - .gitignore | 97 +- .prettierignore | 11 +- .../src/app/api/main.preload.ts | 3 + .../src/app/events/playlist.events.ts | 58 +- apps/web/eslint.config.mjs | 57 +- apps/web/project.json | 1 + apps/web/src/app/app-routing.module.ts | 12 +- apps/web/src/app/app.component.html | 10 +- apps/web/src/app/app.component.spec.ts | 10 +- apps/web/src/app/app.component.ts | 70 +- apps/web/src/app/app.module.ts | 13 +- .../home/file-upload/file-upload.component.ts | 48 +- apps/web/src/app/home/home.component.spec.ts | 4 +- apps/web/src/app/home/home.component.ts | 9 +- .../playlist-info.component.html | 124 -- .../stalker-portal-import.component.ts | 4 +- .../url-upload/url-upload.component.spec.ts | 2 +- .../home/url-upload/url-upload.component.ts | 2 +- .../xtream-code-import.component.ts | 4 +- apps/web/src/app/services/player.service.ts | 9 +- apps/web/src/app/services/pwa.service.ts | 10 +- .../app/services/settings-store.service.ts | 13 +- .../src/app/services/settings.service.spec.ts | 4 +- apps/web/src/app/services/settings.service.ts | 4 +- apps/web/src/app/services/tauri.service.ts | 107 +- .../app/services/whats-new.service.spec.ts | 38 - .../app/services/whats-new.service.stub.ts | 6 - .../web/src/app/services/whats-new.service.ts | 315 ---- .../src/app/settings/settings.component.html | 648 +++---- .../src/app/settings/settings.component.scss | 4 + .../app/settings/settings.component.spec.ts | 17 +- .../src/app/settings/settings.component.ts | 61 +- .../add-playlist-dialog.component.ts | 25 +- .../components/header/header.component.html | 30 +- .../components/header/header.component.ts | 80 +- .../favorites-button.component.ts | 10 +- .../recently-viewed.component.ts | 7 +- .../stalker-favorites.component.ts | 7 +- .../stalker-main-container.component.ts | 12 +- .../stalker-search.component.ts | 6 +- .../stalker-shell/stalker-shell.component.ts | 4 +- apps/web/src/app/stalker/stalker.store.ts | 14 +- .../account-info/account-info.component.ts | 5 +- .../favorites/favorites.component.ts | 2 +- .../live-stream-layout.component.ts | 3 +- .../navigation/navigation.component.ts | 7 +- .../player-dialog/player-dialog.component.ts | 2 +- .../playlist-error-view.component.ts | 14 +- .../portal-channels-list.component.ts | 4 +- .../search-results.component.ts | 4 +- .../services/favorites.service.ts | 2 +- .../xtream-tauri/with-favorites.feature.ts | 2 +- .../src/app/xtream-tauri/with-recent-items.ts | 2 +- .../xtream-shell/xtream-shell.component.ts | 2 +- apps/web/src/app/xtream-tauri/xtream-state.ts | 6 +- apps/web/src/app/xtream-tauri/xtream.store.ts | 14 +- .../category-content-view.component.html | 1 + .../category-content-view.component.ts | 7 +- .../category-view/category-view.component.ts | 18 +- .../navigation-bar.component.ts | 29 +- .../player-dialog/player-dialog.component.ts | 22 +- .../playlist-error-view.component.ts | 13 +- .../serial-details.component.ts | 15 +- .../vod-details/vod-details.component.ts | 2 +- .../xtream/xtream-main-container.component.ts | 1025 +++++----- .../updates/0110/import-playlist-as-text.png | Bin 31674 -> 0 bytes .../assets/updates/0110/multi-epg-view.png | Bin 147113 -> 0 bytes .../updates/0110/multiple-epg-sources.png | Bin 20051 -> 0 bytes .../src/assets/updates/0130/mpv-player.png | Bin 80432 -> 0 bytes .../src/assets/updates/060/channel-logos.png | Bin 25130 -> 0 bytes .../assets/updates/060/custom-user-agent.png | Bin 14522 -> 0 bytes .../web/src/assets/updates/060/dark-theme.png | Bin 20856 -> 0 bytes .../src/assets/updates/070/auto-refresh.png | Bin 40115 -> 0 bytes .../assets/updates/070/refresh-playlist.png | Bin 51914 -> 0 bytes .../src/assets/updates/080/channel-info.png | Bin 65196 -> 0 bytes .../src/assets/updates/080/translations.png | Bin 11086 -> 0 bytes apps/web/src/assets/updates/090/languages.png | Bin 19947 -> 0 bytes .../assets/updates/090/reorder-playlists.gif | Bin 105423 -> 0 bytes .../assets/updates/090/subtitle-option.png | Bin 19591 -> 0 bytes apps/web/src/global.d.ts | 5 + apps/web/src/index.html | 4 +- .../shared/xtream-live-stream.interface.ts | 2 +- .../src/shared/xtream-vod-stream.interface.ts | 2 +- apps/web/tsconfig.spec.json | 33 +- eslint.config.mjs | 5 +- libs/m3u-state/README.md | 7 + libs/m3u-state/eslint.config.mjs | 34 + libs/m3u-state/jest.config.ts | 21 + libs/m3u-state/project.json | 21 + libs/m3u-state/src/index.ts | 5 + .../m3u-state/src/lib}/actions.ts | 4 +- .../m3u-state/src/lib}/effects.ts | 58 +- .../m3u-state/src/lib}/playlists.state.ts | 2 +- .../m3u-state/src/lib}/reducers.ts | 36 +- .../m3u-state/src/lib}/router.selectors.ts | 0 .../m3u-state/src/lib}/selectors.ts | 38 +- .../state => libs/m3u-state/src/lib}/state.ts | 2 +- libs/m3u-state/src/test-setup.ts | 6 + libs/m3u-state/tsconfig.json | 30 + libs/m3u-state/tsconfig.lib.json | 17 + libs/m3u-state/tsconfig.spec.json | 17 + libs/services/README.md | 11 + libs/services/eslint.config.mjs | 21 + libs/services/jest.config.ts | 13 + libs/services/package.json | 11 + libs/services/project.json | 26 + libs/services/src/index.ts | 7 + .../services/src/lib}/data.service.ts | 6 +- .../services/src/lib}/database.service.ts | 2 +- .../services/src/lib}/dialog.service.spec.ts | 7 +- .../services/src/lib}/dialog.service.ts | 8 +- .../services/src/lib}/epg.service.spec.ts | 3 +- .../services/src/lib}/epg.service.ts | 26 +- .../services/src/lib}/playlists.service.ts | 119 +- .../src/lib}/portal-status.service.ts | 2 +- .../services/src/lib}/sort.service.ts | 2 +- libs/services/tsconfig.json | 23 + libs/services/tsconfig.lib.json | 10 + libs/services/tsconfig.spec.json | 15 + libs/shared/interfaces/src/index.ts | 15 + .../interfaces/src/lib}/channel.model.ts | 0 .../interfaces/src/lib}/epg-channel.model.ts | 0 .../interfaces/src/lib}/epg-item.interface.ts | 0 .../interfaces/src/lib}/epg-program.model.ts | 0 .../interfaces/src/lib}/indexed-db.config.ts | 0 .../interfaces/src/lib}/ipc-command.class.ts | 0 .../interfaces/src/lib}/ipc-commands.ts | 15 +- .../interfaces/src/lib}/language.enum.ts | 0 .../interfaces/src/lib}/playlist-meta.type.ts | 2 +- .../interfaces/src/lib}/settings.interface.ts | 0 .../interfaces/src/lib}/store-keys.enum.ts | 0 .../interfaces/src/lib}/stream-format.enum.ts | 0 .../shared/interfaces/src/lib}/theme.enum.ts | 0 .../src/lib}/xtream-item.interface.ts | 0 .../src/lib}/xtream-serie-item.interface.ts | 0 libs/ui/components/README.md | 7 + libs/ui/components/eslint.config.mjs | 34 + libs/ui/components/jest.config.ts | 21 + libs/ui/components/project.json | 21 + libs/ui/components/src/index.ts | 16 + .../lib}/art-player/art-player.component.ts | 4 +- .../audio-player/audio-player.component.scss | 0 .../audio-player/audio-player.component.ts | 13 +- .../channel-list-container.component.html | 0 .../channel-list-container.component.scss | 0 .../channel-list-container.component.spec.ts | 10 +- .../channel-list-container.component.ts | 26 +- .../channel-list-item.component.scss | 0 .../channel-list-item.component.ts | 0 .../epg-item-description.component.html | 0 .../epg-item-description.component.scss | 0 .../epg-item-description.component.spec.ts | 2 +- .../epg-item-description.component.ts | 3 +- .../epg-list-item.component.html | 0 .../epg-list-item.component.scss | 0 .../epg-list-item.component.spec.ts | 6 +- .../epg-list-item/epg-list-item.component.ts | 6 +- .../src/lib}/epg-list/epg-list.component.html | 0 .../src/lib}/epg-list/epg-list.component.scss | 0 .../lib}/epg-list/epg-list.component.spec.ts | 12 +- .../src/lib}/epg-list/epg-list.component.ts | 60 +- .../html-video-player.component.html | 0 .../html-video-player.component.scss | 0 .../html-video-player.component.spec.ts | 2 +- .../html-video-player.component.ts | 28 +- .../info-overlay/info-overlay.component.html | 0 .../info-overlay/info-overlay.component.scss | 0 .../info-overlay.component.spec.ts | 14 +- .../info-overlay/info-overlay.component.ts | 5 +- .../multi-epg-container.component.html | 0 .../multi-epg-container.component.scss | 0 .../multi-epg-container.component.ts | 6 +- .../playlist-info.component.html | 152 ++ .../playlist-info/playlist-info.component.ts | 46 +- .../playlist-item.component.html | 0 .../playlist-item.component.scss | 0 .../playlist-item.component.spec.ts | 2 +- .../playlist-item/playlist-item.component.ts | 26 +- .../recent-playlists.component.html | 0 .../recent-playlists.component.scss | 0 .../recent-playlists.component.spec.ts | 11 +- .../recent-playlists.component.ts | 92 +- .../sidebar/sidebar.component.html | 0 .../sidebar/sidebar.component.scss | 0 .../video-player/sidebar/sidebar.component.ts | 7 +- .../toolbar/toolbar.component.html | 0 .../toolbar/toolbar.component.scss | 0 .../video-player/toolbar/toolbar.component.ts | 8 +- .../video-player/video-player.component.html | 14 +- .../video-player/video-player.component.scss | 0 .../video-player.component.spec.ts | 10 +- .../video-player/video-player.component.ts | 108 +- .../lib}/vjs-player/vjs-player.component.html | 0 .../lib}/vjs-player/vjs-player.component.scss | 0 .../lib}/vjs-player/vjs-player.component.ts | 0 libs/ui/components/src/test-setup.ts | 6 + libs/ui/components/tsconfig.json | 30 + libs/ui/components/tsconfig.lib.json | 17 + libs/ui/components/tsconfig.spec.json | 17 + libs/ui/pipes/README.md | 7 + libs/ui/pipes/eslint.config.mjs | 34 + libs/ui/pipes/jest.config.ts | 21 + libs/ui/pipes/project.json | 21 + libs/ui/pipes/src/index.ts | 4 + .../ui/pipes/src/lib}/filter-by-type.pipe.ts | 2 +- .../ui/pipes/src/lib}/filter.pipe.ts | 0 .../pipes/src/lib}/moment-date.pipe.spec.ts | 3 +- .../ui/pipes/src/lib}/moment-date.pipe.ts | 0 .../ui/pipes/src/lib}/sort.pipe.ts | 0 libs/ui/pipes/src/test-setup.ts | 6 + libs/ui/pipes/tsconfig.json | 30 + libs/ui/pipes/tsconfig.lib.json | 17 + libs/ui/pipes/tsconfig.spec.json | 17 + libs/ui/shared-portals/README.md | 2 + libs/ui/shared-portals/eslint.config.mjs | 34 + libs/ui/shared-portals/jest.config.ts | 21 + libs/ui/shared-portals/project.json | 25 + libs/ui/shared-portals/src/index.ts | 3 + .../src/lib}/epg-view/epg-view.component.html | 0 .../src/lib}/epg-view/epg-view.component.scss | 0 .../src/lib}/epg-view/epg-view.component.ts | 8 +- .../live-stream-layout.component.html | 0 .../live-stream-layout.component.scss | 0 .../live-stream-layout.component.ts | 16 +- .../web-player-view.component.html | 0 .../web-player-view.component.scss | 0 .../web-player-view.component.ts | 17 +- libs/ui/shared-portals/src/test-setup.ts | 6 + libs/ui/shared-portals/tsconfig.json | 30 + libs/ui/shared-portals/tsconfig.lib.json | 19 + libs/ui/shared-portals/tsconfig.spec.json | 22 + nx.json | 176 +- package-lock.json | 1665 ++++++++++++----- package.json | 7 +- tsconfig.base.json | 7 +- 237 files changed, 4015 insertions(+), 2969 deletions(-) delete mode 100644 .devcontainer/Dockerfile delete mode 100644 .devcontainer/devcontainer.json delete mode 100644 apps/web/src/app/home/recent-playlists/playlist-info/playlist-info.component.html delete mode 100644 apps/web/src/app/services/whats-new.service.spec.ts delete mode 100644 apps/web/src/app/services/whats-new.service.stub.ts delete mode 100644 apps/web/src/app/services/whats-new.service.ts delete mode 100644 apps/web/src/assets/updates/0110/import-playlist-as-text.png delete mode 100644 apps/web/src/assets/updates/0110/multi-epg-view.png delete mode 100644 apps/web/src/assets/updates/0110/multiple-epg-sources.png delete mode 100644 apps/web/src/assets/updates/0130/mpv-player.png delete mode 100644 apps/web/src/assets/updates/060/channel-logos.png delete mode 100644 apps/web/src/assets/updates/060/custom-user-agent.png delete mode 100644 apps/web/src/assets/updates/060/dark-theme.png delete mode 100644 apps/web/src/assets/updates/070/auto-refresh.png delete mode 100644 apps/web/src/assets/updates/070/refresh-playlist.png delete mode 100644 apps/web/src/assets/updates/080/channel-info.png delete mode 100644 apps/web/src/assets/updates/080/translations.png delete mode 100644 apps/web/src/assets/updates/090/languages.png delete mode 100644 apps/web/src/assets/updates/090/reorder-playlists.gif delete mode 100644 apps/web/src/assets/updates/090/subtitle-option.png create mode 100644 libs/m3u-state/README.md create mode 100644 libs/m3u-state/eslint.config.mjs create mode 100644 libs/m3u-state/jest.config.ts create mode 100644 libs/m3u-state/project.json create mode 100644 libs/m3u-state/src/index.ts rename {apps/web/src/app/state => libs/m3u-state/src/lib}/actions.ts (95%) rename {apps/web/src/app/state => libs/m3u-state/src/lib}/effects.ts (89%) rename {apps/web/src/app/state => libs/m3u-state/src/lib}/playlists.state.ts (92%) rename {apps/web/src/app/state => libs/m3u-state/src/lib}/reducers.ts (89%) rename {apps/web/src/app/state => libs/m3u-state/src/lib}/router.selectors.ts (100%) rename {apps/web/src/app/state => libs/m3u-state/src/lib}/selectors.ts (76%) rename {apps/web/src/app/state => libs/m3u-state/src/lib}/state.ts (90%) create mode 100644 libs/m3u-state/src/test-setup.ts create mode 100644 libs/m3u-state/tsconfig.json create mode 100644 libs/m3u-state/tsconfig.lib.json create mode 100644 libs/m3u-state/tsconfig.spec.json create mode 100644 libs/services/README.md create mode 100644 libs/services/eslint.config.mjs create mode 100644 libs/services/jest.config.ts create mode 100644 libs/services/package.json create mode 100644 libs/services/project.json create mode 100644 libs/services/src/index.ts rename {apps/web/src/app/services => libs/services/src/lib}/data.service.ts (81%) rename {apps/web/src/app/services => libs/services/src/lib}/database.service.ts (99%) rename {apps/web/src/app/services => libs/services/src/lib}/dialog.service.spec.ts (79%) rename {apps/web/src/app/services => libs/services/src/lib}/dialog.service.ts (63%) rename {apps/web/src/app/services => libs/services/src/lib}/epg.service.spec.ts (98%) rename {apps/web/src/app/services => libs/services/src/lib}/epg.service.ts (86%) rename {apps/web/src/app/services => libs/services/src/lib}/playlists.service.ts (78%) rename {apps/web/src/app/services => libs/services/src/lib}/portal-status.service.ts (98%) rename {apps/web/src/app/services => libs/services/src/lib}/sort.service.ts (97%) create mode 100644 libs/services/tsconfig.json create mode 100644 libs/services/tsconfig.lib.json create mode 100644 libs/services/tsconfig.spec.json rename {apps/web/src/app/shared => libs/shared/interfaces/src/lib}/channel.model.ts (100%) rename {apps/web/src/app/player/models => libs/shared/interfaces/src/lib}/epg-channel.model.ts (100%) rename {apps/web/src/app/xtream => libs/shared/interfaces/src/lib}/epg-item.interface.ts (100%) rename {apps/web/src/app/player/models => libs/shared/interfaces/src/lib}/epg-program.model.ts (100%) rename {apps/web/src/app => libs/shared/interfaces/src/lib}/indexed-db.config.ts (100%) rename {apps/web/src/shared => libs/shared/interfaces/src/lib}/ipc-command.class.ts (100%) rename {apps/web/src/shared => libs/shared/interfaces/src/lib}/ipc-commands.ts (80%) rename {apps/web/src/app/settings => libs/shared/interfaces/src/lib}/language.enum.ts (100%) rename {apps/web/src/app/shared => libs/shared/interfaces/src/lib}/playlist-meta.type.ts (88%) rename {apps/web/src/app/settings => libs/shared/interfaces/src/lib}/settings.interface.ts (100%) rename {apps/web/src/app/shared/enums => libs/shared/interfaces/src/lib}/store-keys.enum.ts (100%) rename {apps/web/src/app/settings => libs/shared/interfaces/src/lib}/stream-format.enum.ts (100%) rename {apps/web/src/app/settings => libs/shared/interfaces/src/lib}/theme.enum.ts (100%) rename {apps/web/src/shared => libs/shared/interfaces/src/lib}/xtream-item.interface.ts (100%) rename {apps/web/src/shared => libs/shared/interfaces/src/lib}/xtream-serie-item.interface.ts (100%) create mode 100644 libs/ui/components/README.md create mode 100644 libs/ui/components/eslint.config.mjs create mode 100644 libs/ui/components/jest.config.ts create mode 100644 libs/ui/components/project.json create mode 100644 libs/ui/components/src/index.ts rename {apps/web/src/app/player/components => libs/ui/components/src/lib}/art-player/art-player.component.ts (98%) rename {apps/web/src/app/player/components => libs/ui/components/src/lib}/audio-player/audio-player.component.scss (100%) rename {apps/web/src/app/player/components => libs/ui/components/src/lib}/audio-player/audio-player.component.ts (94%) rename {apps/web/src/app/player/components => libs/ui/components/src/lib}/channel-list-container/channel-list-container.component.html (100%) rename {apps/web/src/app/player/components => libs/ui/components/src/lib}/channel-list-container/channel-list-container.component.scss (100%) rename {apps/web/src/app/player/components => libs/ui/components/src/lib}/channel-list-container/channel-list-container.component.spec.ts (97%) rename {apps/web/src/app/player/components => libs/ui/components/src/lib}/channel-list-container/channel-list-container.component.ts (90%) rename {apps/web/src/app/player/components => libs/ui/components/src/lib}/channel-list-container/channel-list-item/channel-list-item.component.scss (100%) rename {apps/web/src/app/player/components => libs/ui/components/src/lib}/channel-list-container/channel-list-item/channel-list-item.component.ts (100%) rename {apps/web/src/app/player/components => libs/ui/components/src/lib}/epg-list/epg-item-description/epg-item-description.component.html (100%) rename {apps/web/src/app/player/components => libs/ui/components/src/lib}/epg-list/epg-item-description/epg-item-description.component.scss (100%) rename {apps/web/src/app/player/components => libs/ui/components/src/lib}/epg-list/epg-item-description/epg-item-description.component.spec.ts (96%) rename {apps/web/src/app/player/components => libs/ui/components/src/lib}/epg-list/epg-item-description/epg-item-description.component.ts (90%) rename {apps/web/src/app/player/components => libs/ui/components/src/lib}/epg-list/epg-list-item/epg-list-item.component.html (100%) rename {apps/web/src/app/player/components => libs/ui/components/src/lib}/epg-list/epg-list-item/epg-list-item.component.scss (100%) rename {apps/web/src/app/player/components => libs/ui/components/src/lib}/epg-list/epg-list-item/epg-list-item.component.spec.ts (92%) rename {apps/web/src/app/player/components => libs/ui/components/src/lib}/epg-list/epg-list-item/epg-list-item.component.ts (81%) rename {apps/web/src/app/player/components => libs/ui/components/src/lib}/epg-list/epg-list.component.html (100%) rename {apps/web/src/app/player/components => libs/ui/components/src/lib}/epg-list/epg-list.component.scss (100%) rename {apps/web/src/app/player/components => libs/ui/components/src/lib}/epg-list/epg-list.component.spec.ts (94%) rename {apps/web/src/app/player/components => libs/ui/components/src/lib}/epg-list/epg-list.component.ts (85%) rename {apps/web/src/app/player/components => libs/ui/components/src/lib}/html-video-player/html-video-player.component.html (100%) rename {apps/web/src/app/player/components => libs/ui/components/src/lib}/html-video-player/html-video-player.component.scss (100%) rename {apps/web/src/app/player/components => libs/ui/components/src/lib}/html-video-player/html-video-player.component.spec.ts (95%) rename {apps/web/src/app/player/components => libs/ui/components/src/lib}/html-video-player/html-video-player.component.ts (89%) rename {apps/web/src/app/player/components => libs/ui/components/src/lib}/info-overlay/info-overlay.component.html (100%) rename {apps/web/src/app/player/components => libs/ui/components/src/lib}/info-overlay/info-overlay.component.scss (100%) rename {apps/web/src/app/player/components => libs/ui/components/src/lib}/info-overlay/info-overlay.component.spec.ts (67%) rename {apps/web/src/app/player/components => libs/ui/components/src/lib}/info-overlay/info-overlay.component.ts (92%) rename {apps/web/src/app/player/components => libs/ui/components/src/lib}/multi-epg/multi-epg-container.component.html (100%) rename {apps/web/src/app/player/components => libs/ui/components/src/lib}/multi-epg/multi-epg-container.component.scss (100%) rename {apps/web/src/app/player/components => libs/ui/components/src/lib}/multi-epg/multi-epg-container.component.ts (97%) create mode 100644 libs/ui/components/src/lib/recent-playlists/playlist-info/playlist-info.component.html rename {apps/web/src/app/home => libs/ui/components/src/lib}/recent-playlists/playlist-info/playlist-info.component.ts (85%) rename {apps/web/src/app/home => libs/ui/components/src/lib}/recent-playlists/playlist-item/playlist-item.component.html (100%) rename {apps/web/src/app/home => libs/ui/components/src/lib}/recent-playlists/playlist-item/playlist-item.component.scss (100%) rename {apps/web/src/app/home => libs/ui/components/src/lib}/recent-playlists/playlist-item/playlist-item.component.spec.ts (94%) rename {apps/web/src/app/home => libs/ui/components/src/lib}/recent-playlists/playlist-item/playlist-item.component.ts (75%) rename {apps/web/src/app/home => libs/ui/components/src/lib}/recent-playlists/recent-playlists.component.html (100%) rename {apps/web/src/app/home => libs/ui/components/src/lib}/recent-playlists/recent-playlists.component.scss (100%) rename {apps/web/src/app/home => libs/ui/components/src/lib}/recent-playlists/recent-playlists.component.spec.ts (92%) rename {apps/web/src/app/home => libs/ui/components/src/lib}/recent-playlists/recent-playlists.component.ts (80%) rename {apps/web/src/app/player/components => libs/ui/components/src/lib}/video-player/sidebar/sidebar.component.html (100%) rename {apps/web/src/app/player/components => libs/ui/components/src/lib}/video-player/sidebar/sidebar.component.scss (100%) rename {apps/web/src/app/player/components => libs/ui/components/src/lib}/video-player/sidebar/sidebar.component.ts (87%) rename {apps/web/src/app/player/components => libs/ui/components/src/lib}/video-player/toolbar/toolbar.component.html (100%) rename {apps/web/src/app/player/components => libs/ui/components/src/lib}/video-player/toolbar/toolbar.component.scss (100%) rename {apps/web/src/app/player/components => libs/ui/components/src/lib}/video-player/toolbar/toolbar.component.ts (91%) rename {apps/web/src/app/player/components => libs/ui/components/src/lib}/video-player/video-player.component.html (90%) rename {apps/web/src/app/player/components => libs/ui/components/src/lib}/video-player/video-player.component.scss (100%) rename {apps/web/src/app/player/components => libs/ui/components/src/lib}/video-player/video-player.component.spec.ts (90%) rename {apps/web/src/app/player/components => libs/ui/components/src/lib}/video-player/video-player.component.ts (80%) rename {apps/web/src/app/player/components => libs/ui/components/src/lib}/vjs-player/vjs-player.component.html (100%) rename {apps/web/src/app/player/components => libs/ui/components/src/lib}/vjs-player/vjs-player.component.scss (100%) rename {apps/web/src/app/player/components => libs/ui/components/src/lib}/vjs-player/vjs-player.component.ts (100%) create mode 100644 libs/ui/components/src/test-setup.ts create mode 100644 libs/ui/components/tsconfig.json create mode 100644 libs/ui/components/tsconfig.lib.json create mode 100644 libs/ui/components/tsconfig.spec.json create mode 100644 libs/ui/pipes/README.md create mode 100644 libs/ui/pipes/eslint.config.mjs create mode 100644 libs/ui/pipes/jest.config.ts create mode 100644 libs/ui/pipes/project.json create mode 100644 libs/ui/pipes/src/index.ts rename {apps/web/src/app/shared/pipes => libs/ui/pipes/src/lib}/filter-by-type.pipe.ts (75%) rename {apps/web/src/app/shared/pipes => libs/ui/pipes/src/lib}/filter.pipe.ts (100%) rename {apps/web/src/app/shared/pipes => libs/ui/pipes/src/lib}/moment-date.pipe.spec.ts (73%) rename {apps/web/src/app/shared/pipes => libs/ui/pipes/src/lib}/moment-date.pipe.ts (100%) rename {apps/web/src/app/shared/pipes => libs/ui/pipes/src/lib}/sort.pipe.ts (100%) create mode 100644 libs/ui/pipes/src/test-setup.ts create mode 100644 libs/ui/pipes/tsconfig.json create mode 100644 libs/ui/pipes/tsconfig.lib.json create mode 100644 libs/ui/pipes/tsconfig.spec.json create mode 100644 libs/ui/shared-portals/README.md create mode 100644 libs/ui/shared-portals/eslint.config.mjs create mode 100644 libs/ui/shared-portals/jest.config.ts create mode 100644 libs/ui/shared-portals/project.json create mode 100644 libs/ui/shared-portals/src/index.ts rename {apps/web/src/app/portals => libs/ui/shared-portals/src/lib}/epg-view/epg-view.component.html (100%) rename {apps/web/src/app/portals => libs/ui/shared-portals/src/lib}/epg-view/epg-view.component.scss (100%) rename {apps/web/src/app/portals => libs/ui/shared-portals/src/lib}/epg-view/epg-view.component.ts (84%) rename {apps/web/src/app/portals => libs/ui/shared-portals/src/lib}/live-stream-layout/live-stream-layout.component.html (100%) rename {apps/web/src/app/portals => libs/ui/shared-portals/src/lib}/live-stream-layout/live-stream-layout.component.scss (100%) rename {apps/web/src/app/portals => libs/ui/shared-portals/src/lib}/live-stream-layout/live-stream-layout.component.ts (75%) rename {apps/web/src/app/portals => libs/ui/shared-portals/src/lib}/web-player-view/web-player-view.component.html (100%) rename {apps/web/src/app/portals => libs/ui/shared-portals/src/lib}/web-player-view/web-player-view.component.scss (100%) rename {apps/web/src/app/portals => libs/ui/shared-portals/src/lib}/web-player-view/web-player-view.component.ts (73%) create mode 100644 libs/ui/shared-portals/src/test-setup.ts create mode 100644 libs/ui/shared-portals/tsconfig.json create mode 100644 libs/ui/shared-portals/tsconfig.lib.json create mode 100644 libs/ui/shared-portals/tsconfig.spec.json diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile deleted file mode 100644 index fc03295fa..000000000 --- a/.devcontainer/Dockerfile +++ /dev/null @@ -1,16 +0,0 @@ -# See here for image contents: https://github.com/microsoft/vscode-dev-containers/tree/v0.203.0/containers/typescript-node/.devcontainer/base.Dockerfile - -# [Choice] Node.js version (use -bullseye variants on local arm64/Apple Silicon): 16, 14, 12, 16-bullseye, 14-bullseye, 12-bullseye, 16-buster, 14-buster, 12-buster -ARG VARIANT="16-bullseye" -FROM mcr.microsoft.com/vscode/devcontainers/typescript-node:0-${VARIANT} - -# [Optional] Uncomment this section to install additional OS packages. -# RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ -# && apt-get -y install --no-install-recommends - -# [Optional] Uncomment if you want to install an additional version of node using nvm -ARG EXTRA_NODE_VERSION=16.10 -RUN su node -c "source /usr/local/share/nvm/nvm.sh && nvm install ${EXTRA_NODE_VERSION}" - -# [Optional] Uncomment if you want to install more global node packages -# RUN su node -c "npm install -g " diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json deleted file mode 100644 index b0277212a..000000000 --- a/.devcontainer/devcontainer.json +++ /dev/null @@ -1,42 +0,0 @@ -// For format details, see https://aka.ms/devcontainer.json. For config options, see the README at: -// https://github.com/microsoft/vscode-dev-containers/tree/v0.203.0/containers/typescript-node -{ - "name": "Node.js & TypeScript", - "runArgs": ["--init"], - "build": { - "dockerfile": "Dockerfile", - // Update 'VARIANT' to pick a Node version: 16, 14, 12. - // Append -bullseye or -buster to pin to an OS version. - // Use -bullseye variants on local on arm64/Apple Silicon. - "args": { - "VARIANT": "16-bullseye" - } - }, - - // Set *default* container specific settings.json values on container create. - "settings": { - "editor.formatOnSave": true - }, - - // Add the IDs of extensions you want installed when the container is created. - "extensions": [ - "dbaeumer.vscode-eslint", - "GitHub.copilot", - "esbenp.prettier-vscode", - "JohannesRudolph.file-ext-switcher", - "yzhang.markdown-all-in-one", - "streetsidesoftware.code-spell-checker", - "GitHub.vscode-pull-request-github", - "wayou.vscode-todo-highlight", - "Gruntfuggly.todo-tree" - ], - - // Use 'forwardPorts' to make a list of ports inside the container available locally. - // "forwardPorts": [], - - // Use 'postCreateCommand' to run commands after the container is created. - "postCreateCommand": "npm install", - - // Comment out connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root. - "remoteUser": "node" -} diff --git a/.gitignore b/.gitignore index 18c36bbbd..ab4096c76 100644 --- a/.gitignore +++ b/.gitignore @@ -1,64 +1,65 @@ -# See http://help.github.com/ignore-files/ for more about ignoring files. +# See http://help.github.com/ignore-files/ for more about ignoring files. -# compiled output -/dist -/build -/tmp -/out-tsc -/app-builds -/release +#compiled output +/dist +/build +/tmp +/out-tsc +/app-builds +/release /e2e/tracing -/e2e/screenshots -/test-results -api.js -epg-worker.js -main.js -menu.js +/e2e/screenshots +/test-results +api.js +epg-worker.js +main.js +menu.js src/**/*.js -!src/karma.conf.js -*.js.map -data.db -preload.js -shared/*.js -playwright-report +!src/karma.conf.js +*.js.map +data.db +preload.js +shared/*.js +playwright-report .nx -server.js +server.js -# dependencies -/node_modules +# dependencies +/node_modules -# IDEs and editors -/.idea +# IDEs and editors +/.idea .project -.classpath -.c9/ -*.launch -.settings/ -*.sublime-workspace +.classpath +.c9/ +*.launch +.settings/ +*.sublime-workspace -# IDE - VSCode +# IDE - VSCode .vscode/* -.vscode/settings.json -!.vscode/tasks.json +.vscode/settings.json +!.vscode/tasks.json !.vscode/launch.json -!.vscode/extensions.json +!.vscode/extensions.json -# misc -/.sass-cache -/connect.lock +# misc +/.sass-cache +/connect.lock /coverage -/libpeerconnection.log -npm-debug.log -testem.log -/typings -.angular/* +/libpeerconnection.log +npm-debug.log +testem.log +/typings +.angular/* # e2e -/e2e/*.js -!/e2e/protractor.conf.js -/e2e/*.map -/e2e/db +/e2e/*.js +!/e2e/protractor.conf.js +/e2e/*.map +/e2e/db -# System Files +# System Files .DS_Store -Thumbs.db +Thumbs.db +.angular diff --git a/.prettierignore b/.prettierignore index 3e7de75be..991c586b4 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,5 +1,6 @@ -*.yml -dist -build -coverage -release \ No newline at end of file +*.yml +dist +build +coverage +release +.angular diff --git a/apps/electron-backend/src/app/api/main.preload.ts b/apps/electron-backend/src/app/api/main.preload.ts index ab62c2f51..00eb3efba 100644 --- a/apps/electron-backend/src/app/api/main.preload.ts +++ b/apps/electron-backend/src/app/api/main.preload.ts @@ -5,4 +5,7 @@ contextBridge.exposeInMainWorld('electron', { platform: process.platform, fetchPlaylistByUrl: (url: string) => ipcRenderer.invoke('fetch-playlist-by-url', url), + updatePlaylistFromFilePath: (filePath: string, title: string) => + ipcRenderer.invoke('update-playlist-from-file-path', filePath, title), + openPlaylistFromFile: () => ipcRenderer.invoke('open-playlist-from-file'), }); diff --git a/apps/electron-backend/src/app/events/playlist.events.ts b/apps/electron-backend/src/app/events/playlist.events.ts index d0dfba480..81cecee45 100644 --- a/apps/electron-backend/src/app/events/playlist.events.ts +++ b/apps/electron-backend/src/app/events/playlist.events.ts @@ -4,9 +4,10 @@ */ import axios from 'axios'; -import { ipcMain } from 'electron'; +import { dialog, ipcMain } from 'electron'; import { parse } from 'iptv-playlist-parser'; import { createPlaylistObject, getFilenameFromUrl } from 'm3u-utils'; +import { readFile } from 'node:fs/promises'; export default class PlaylistEvents { static bootstrapPlaylistEvents(): Electron.IpcMain { @@ -16,7 +17,6 @@ export default class PlaylistEvents { const https = require('https'); -// Retrieve app version ipcMain.handle('fetch-playlist-by-url', async (event, url) => { try { const agent = new https.Agent({ @@ -45,3 +45,57 @@ ipcMain.handle('fetch-playlist-by-url', async (event, url) => { throw error; } }); + +ipcMain.handle( + 'update-playlist-from-file-path', + async (event, filePath, title) => { + const playlist = await readFile(filePath, 'utf-8'); + const parsedPlaylist = parse(playlist); + const playlistObject = createPlaylistObject( + title, + parsedPlaylist, + filePath, + 'FILE' + ); + return playlistObject; + } +); + +ipcMain.handle('open-playlist-from-file', async () => { + // 1. Await the result from the dialog + const { canceled, filePaths } = await dialog.showOpenDialog({ + properties: ['openFile'], + filters: [ + { name: 'Playlists', extensions: ['m3u', 'm3u8', 'pls', 'txt'] }, + { name: 'All Files', extensions: ['*'] }, + ], + }); + + if (canceled || filePaths.length === 0) { + console.log('User canceled file open dialog.'); + return null; + } + + const filePath = filePaths[0]; + + try { + const fileContent = await readFile(filePath, 'utf-8'); + + const parsedPlaylist = parse(fileContent); + const playlistObject = createPlaylistObject( + 'from file', + parsedPlaylist, + filePath, + 'FILE' + ); + + console.log('Sending playlist object to renderer:', playlistObject); + + // 5. Return the final object. This value is sent back to Angular. + return playlistObject; + } catch (error) { + console.error('Error reading or parsing the file:', error); + // It's good practice to throw the error so the renderer can catch it + throw new Error('Failed to process the selected file.'); + } +}); diff --git a/apps/web/eslint.config.mjs b/apps/web/eslint.config.mjs index 9b3aa715c..cd8ecd4da 100644 --- a/apps/web/eslint.config.mjs +++ b/apps/web/eslint.config.mjs @@ -2,33 +2,36 @@ import nx from '@nx/eslint-plugin'; import baseConfig from '../../eslint.config.mjs'; export default [ - ...baseConfig, - ...nx.configs['flat/angular'], - ...nx.configs['flat/angular-template'], - { - files: ['**/*.ts'], - rules: { - '@angular-eslint/directive-selector': [ - 'error', - { - type: 'attribute', - prefix: 'app', - style: 'camelCase', + ...baseConfig, + ...nx.configs['flat/angular'], + ...nx.configs['flat/angular-template'], + { + files: ['**/*.ts'], + rules: { + '@angular-eslint/directive-selector': [ + 'error', + { + type: 'attribute', + prefix: 'app', + style: 'camelCase', + }, + ], + '@angular-eslint/component-selector': [ + 'error', + { + type: 'element', + prefix: 'app', + style: 'kebab-case', + }, + ], + }, + }, + { + files: ['**/*.html'], + // Override or add rules here + rules: { + '@angular-eslint/template/click-events-have-key-events': 'off', + '@angular-eslint/template/interactive-supports-focus': 'off', }, - ], - '@angular-eslint/component-selector': [ - 'error', - { - type: 'element', - prefix: 'app', - style: 'kebab-case', - }, - ], }, - }, - { - files: ['**/*.html'], - // Override or add rules here - rules: {}, - }, ]; diff --git a/apps/web/project.json b/apps/web/project.json index 9826b30d2..3a6db88bc 100644 --- a/apps/web/project.json +++ b/apps/web/project.json @@ -30,6 +30,7 @@ }, "configurations": { "production": { + "baseHref": "./", "budgets": [ { "type": "initial", diff --git a/apps/web/src/app/app-routing.module.ts b/apps/web/src/app/app-routing.module.ts index 4fc628b99..4d41d63b7 100644 --- a/apps/web/src/app/app-routing.module.ts +++ b/apps/web/src/app/app-routing.module.ts @@ -13,23 +13,17 @@ const routes: Routes = [ { path: 'playlists', loadComponent: () => - import( - './player/components/video-player/video-player.component' - ).then((c) => c.VideoPlayerComponent), + import('components').then((c) => c.VideoPlayerComponent), }, { path: 'iptv', loadComponent: () => - import( - './player/components/video-player/video-player.component' - ).then((c) => c.VideoPlayerComponent), + import('components').then((c) => c.VideoPlayerComponent), }, { path: 'playlists/:id', loadComponent: () => - import( - './player/components/video-player/video-player.component' - ).then((c) => c.VideoPlayerComponent), + import('components').then((c) => c.VideoPlayerComponent), }, { path: 'settings', diff --git a/apps/web/src/app/app.component.html b/apps/web/src/app/app.component.html index d2aa479f4..67e7bd4cd 100644 --- a/apps/web/src/app/app.component.html +++ b/apps/web/src/app/app.component.html @@ -1,9 +1 @@ - - -@if (isDialogVisible$ | async) { - -} + diff --git a/apps/web/src/app/app.component.spec.ts b/apps/web/src/app/app.component.spec.ts index 8a21096dc..2061a1fb9 100644 --- a/apps/web/src/app/app.component.spec.ts +++ b/apps/web/src/app/app.component.spec.ts @@ -14,16 +14,16 @@ import { MockComponent, MockModule, MockPipe, MockProviders } from 'ng-mocks'; import { NgxIndexedDBService } from 'ngx-indexed-db'; import { NgxWhatsNewComponent } from 'ngx-whats-new'; import { of } from 'rxjs'; +import { DataService } from '../../../../libs/services/src/lib/data.service'; +import { PlaylistsService } from '../../../../libs/services/src/lib/playlists.service'; +import { Language } from '../../../../libs/shared/interfaces/src/lib/language.enum'; +import { STORE_KEY } from '../../../../libs/shared/interfaces/src/lib/store-keys.enum'; +import { Theme } from '../../../../libs/shared/interfaces/src/lib/theme.enum'; import { AppComponent } from './app.component'; -import { DataService } from './services/data.service'; import { ElectronServiceStub } from './services/electron.service.stub'; -import { PlaylistsService } from './services/playlists.service'; import { SettingsService } from './services/settings.service'; import { WhatsNewService } from './services/whats-new.service'; import { WhatsNewServiceStub } from './services/whats-new.service.stub'; -import { Language } from './settings/language.enum'; -import { Theme } from './settings/theme.enum'; -import { STORE_KEY } from './shared/enums/store-keys.enum'; jest.spyOn(global.console, 'error').mockImplementation(() => {}); diff --git a/apps/web/src/app/app.component.ts b/apps/web/src/app/app.component.ts index 65f81e8df..c881504eb 100644 --- a/apps/web/src/app/app.component.ts +++ b/apps/web/src/app/app.component.ts @@ -4,25 +4,23 @@ import { MatSnackBar } from '@angular/material/snack-bar'; import { Router } from '@angular/router'; import { Store } from '@ngrx/store'; import { TranslateService } from '@ngx-translate/core'; -import * as semver from 'semver'; -import { IpcCommand } from '../shared/ipc-command.class'; +/* import * as semver from 'semver'; */ +import * as PlaylistActions from 'm3u-state'; import { ERROR, + IpcCommand, + Language, OPEN_FILE, + Settings, SETTINGS_UPDATE, - SHOW_WHATS_NEW, + STORE_KEY, + Theme, VIEW_ADD_PLAYLIST, VIEW_SETTINGS, -} from '../shared/ipc-commands'; -import { DataService } from './services/data.service'; -import { EpgService } from './services/epg.service'; +} from 'shared-interfaces'; +import { DataService } from '../../../../libs/services/src/lib/data.service'; +import { EpgService } from '../../../../libs/services/src/lib/epg.service'; import { SettingsService } from './services/settings.service'; -import { WhatsNewService } from './services/whats-new.service'; -import { Language } from './settings/language.enum'; -import { Settings } from './settings/settings.interface'; -import { Theme } from './settings/theme.enum'; -import { STORE_KEY } from './shared/enums/store-keys.enum'; -import * as PlaylistActions from './state/actions'; import { RecentlyViewedComponent } from './xtream-tauri/recently-viewed/recently-viewed.component'; import { SearchResultsComponent } from './xtream-tauri/search-results/search-results.component'; @@ -40,22 +38,11 @@ export class AppComponent implements OnInit, OnDestroy { private snackBar = inject(MatSnackBar); private translate = inject(TranslateService); private settingsService = inject(SettingsService); - private whatsNewService = inject(WhatsNewService); - - /** Visibility flag of the "what is new" modal dialog */ - readonly isDialogVisible$ = this.whatsNewService.dialogState$; - - /** Dialog options */ - readonly options = this.whatsNewService.options; - - /** Modals to show for the updated version of the application */ - modals: any[] = []; /** List of ipc commands with function mapping */ private readonly commandsList = [ new IpcCommand(VIEW_ADD_PLAYLIST, () => this.navigateToRoute('/')), new IpcCommand(VIEW_SETTINGS, () => this.navigateToRoute('/settings')), - new IpcCommand(SHOW_WHATS_NEW, () => this.showWhatsNewDialog()), new IpcCommand(ERROR, (response: { message: string; status: number }) => this.showErrorAsNotification(response) ), @@ -107,7 +94,7 @@ export class AppComponent implements OnInit, OnDestroy { this.setRendererListeners(); this.initSettings(); - this.handleWhatsNewDialog(); + /* this.handleWhatsNewDialog(); */ } /** @@ -173,36 +160,17 @@ export class AppComponent implements OnInit, OnDestroy { /** * Checks the actual version of the application and shows the "what is new" dialog if the updated version was detected */ - handleWhatsNewDialog(): void { + /* handleWhatsNewDialog(): void { const actualVersion = this.dataService.getAppVersion(); this.settingsService .getValueFromLocalStorage(STORE_KEY.Version) - .subscribe((version: string) => { - const isNewVersion = semver.gt( - actualVersion, - version || '0.0.0' - ); - if (!version || isNewVersion) { - this.modals = - this.whatsNewService.getModalsByVersion(actualVersion); - this.setDialogVisibility(true); - } + .subscribe(() => { this.settingsService.setValueToLocalStorage( STORE_KEY.Version, actualVersion ); }); - } - - /** - * Sets the visibility flag of the modal window - * @param visible show/hide window flag - */ - setDialogVisibility(visible: boolean): void { - if (this.modals.length > 0) { - this.whatsNewService.changeDialogVisibleState(visible); - } - } + } */ /** * Navigate to the specified route @@ -212,16 +180,6 @@ export class AppComponent implements OnInit, OnDestroy { this.router.navigateByUrl(route); } - /** - * Shows the "what is new" dialog - */ - showWhatsNewDialog(): void { - this.modals = this.whatsNewService.getModalsByVersion( - this.dataService.getAppVersion() - ); - this.setDialogVisibility(true); - } - showErrorAsNotification(response: { message: string; status: number }) { this.snackBar.open( `Error: ${response?.message ?? 'Something went wrong'} (Status: ${ diff --git a/apps/web/src/app/app.module.ts b/apps/web/src/app/app.module.ts index 73ee8da7b..a6d6af81e 100644 --- a/apps/web/src/app/app.module.ts +++ b/apps/web/src/app/app.module.ts @@ -7,27 +7,23 @@ import { NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { ServiceWorkerModule } from '@angular/service-worker'; +import { DataService } from '@iptvnator/services'; import { EffectsModule } from '@ngrx/effects'; import { StoreRouterConnectingModule, routerReducer } from '@ngrx/router-store'; import { StoreModule } from '@ngrx/store'; import { StoreDevtoolsModule } from '@ngrx/store-devtools'; import { TranslateLoader, TranslateModule } from '@ngx-translate/core'; import { TranslateHttpLoader } from '@ngx-translate/http-loader'; -import { isTauri } from '@tauri-apps/api/core'; +import { PlaylistEffects, playlistReducer } from 'm3u-state'; import { NgxIndexedDBModule, NgxIndexedDBService } from 'ngx-indexed-db'; import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; -import { NgxWhatsNewModule } from 'ngx-whats-new'; -import 'reflect-metadata'; +import { dbConfig } from 'shared-interfaces'; import { AppConfig } from '../environments/environment'; import '../polyfills'; import { AppRoutingModule } from './app-routing.module'; import { AppComponent } from './app.component'; -import { dbConfig } from './indexed-db.config'; -import { DataService } from './services/data.service'; import { PwaService } from './services/pwa.service'; import { TauriService } from './services/tauri.service'; -import { PlaylistEffects } from './state/effects'; -import { playlistReducer } from './state/reducers'; // AoT requires an exported function for factories export function HttpLoaderFactory(http: HttpClient): TranslateHttpLoader { @@ -54,7 +50,6 @@ export function DataFactory() { AppConfig.environment === 'WEB' ? NgxIndexedDBModule.forRoot(dbConfig) : [], - NgxWhatsNewModule, NgxIndexedDBModule.forRoot(dbConfig), NgxSkeletonLoaderModule.forRoot({ animation: 'pulse', @@ -68,7 +63,7 @@ export function DataFactory() { }, }), ServiceWorkerModule.register('ngsw-worker.js', { - enabled: AppConfig.production && !isTauri(), + enabled: AppConfig.production && !!window.electron, registrationStrategy: 'registerWhenStable:30000', }), StoreModule.forRoot({ diff --git a/apps/web/src/app/home/file-upload/file-upload.component.ts b/apps/web/src/app/home/file-upload/file-upload.component.ts index 56b478343..b4de5ff75 100644 --- a/apps/web/src/app/home/file-upload/file-upload.component.ts +++ b/apps/web/src/app/home/file-upload/file-upload.component.ts @@ -2,10 +2,7 @@ import { Component, EventEmitter, inject, Output } from '@angular/core'; import { MatIconModule } from '@angular/material/icon'; import { Store } from '@ngrx/store'; import { TranslatePipe } from '@ngx-translate/core'; -import { isTauri } from '@tauri-apps/api/core'; -import { open } from '@tauri-apps/plugin-dialog'; -import { readTextFile } from '@tauri-apps/plugin-fs'; -import { parsePlaylist } from '../../state/actions'; +import { addPlaylist } from 'm3u-state'; import { DragDropFileUploadDirective } from './drag-drop-file-upload.directive'; @Component({ @@ -25,6 +22,8 @@ export class FileUploadComponent { private readonly store = inject(Store); + private isDesktop = !!window.electron; + allowedContentTypes = [ 'application/mpegurl', 'application/x-mpegurl', @@ -36,27 +35,26 @@ export class FileUploadComponent { ]; async openDialog(fileField: HTMLInputElement) { - if (isTauri()) { - const path = await open({ - multiple: false, - directory: false, - filters: [ - { - name: 'Playlist files', - extensions: ['m3u', 'm3u8'], - }, - ], - }); - const title = path.split('/').pop(); - const fileContent = await readTextFile(path); - this.store.dispatch( - parsePlaylist({ - uploadType: 'FILE', - playlist: fileContent, - title, - path, + if (this.isDesktop) { + window.electron + .openPlaylistFromFile() + .then((playlistObject) => { + if (playlistObject) { + console.log( + 'Received playlist from Electron:', + playlistObject + ); + this.store.dispatch( + addPlaylist({ playlist: playlistObject }) + ); + } else { + // User canceled the dialog + console.log('File selection was canceled.'); + } }) - ); + .catch((error) => { + console.error('Error opening file:', error); + }); this.closeDialog.emit(); } else { fileField.click(); @@ -64,6 +62,8 @@ export class FileUploadComponent { } upload(fileList: FileList) { + if (this.isDesktop) return; + if (!this.allowedContentTypes.includes(fileList[0].type)) { this.fileRejected.emit(fileList[0].name); return; diff --git a/apps/web/src/app/home/home.component.spec.ts b/apps/web/src/app/home/home.component.spec.ts index c378d69f1..5059a194b 100644 --- a/apps/web/src/app/home/home.component.spec.ts +++ b/apps/web/src/app/home/home.component.spec.ts @@ -10,10 +10,10 @@ import { MockStore, provideMockStore } from '@ngrx/store/testing'; import { TranslateModule } from '@ngx-translate/core'; import { MockComponent, MockModule, MockProvider } from 'ng-mocks'; import { Observable } from 'rxjs'; -import { DataService } from '../services/data.service'; +import { DataService } from '../../../../../libs/services/src/lib/data.service'; +import { RecentPlaylistsComponent } from '../../../../../libs/ui/components/src/lib/recent-playlists/recent-playlists.component'; import { ElectronServiceStub } from '../services/electron.service.stub'; import { HomeComponent } from './home.component'; -import { RecentPlaylistsComponent } from './recent-playlists/recent-playlists.component'; describe('HomeComponent', () => { let component: HomeComponent; diff --git a/apps/web/src/app/home/home.component.ts b/apps/web/src/app/home/home.component.ts index 0438db85a..632f5cd73 100644 --- a/apps/web/src/app/home/home.component.ts +++ b/apps/web/src/app/home/home.component.ts @@ -1,14 +1,13 @@ import { Component, inject, OnDestroy } from '@angular/core'; import { MatProgressBarModule } from '@angular/material/progress-bar'; import { MatSnackBar } from '@angular/material/snack-bar'; +import { DataService } from '@iptvnator/services'; import { Store } from '@ngrx/store'; import { TranslatePipe } from '@ngx-translate/core'; -import { Playlist } from 'shared-interfaces'; -import { ERROR, PLAYLIST_PARSE_RESPONSE } from '../../shared/ipc-commands'; -import { DataService } from '../services/data.service'; +import { addPlaylist } from 'm3u-state'; +import { ERROR, Playlist, PLAYLIST_PARSE_RESPONSE } from 'shared-interfaces'; +import { RecentPlaylistsComponent } from '../../../../../libs/ui/components/src/lib/recent-playlists/recent-playlists.component'; import { HeaderComponent } from '../shared/components/header/header.component'; -import { addPlaylist } from '../state/actions'; -import { RecentPlaylistsComponent } from './recent-playlists/recent-playlists.component'; @Component({ selector: 'app-home', diff --git a/apps/web/src/app/home/recent-playlists/playlist-info/playlist-info.component.html b/apps/web/src/app/home/recent-playlists/playlist-info/playlist-info.component.html deleted file mode 100644 index dfecda1db..000000000 --- a/apps/web/src/app/home/recent-playlists/playlist-info/playlist-info.component.html +++ /dev/null @@ -1,124 +0,0 @@ -
-

- {{ 'HOME.PLAYLISTS.INFO_DIALOG.PLAYLIST_DETAILS' | translate }} -

- - - {{ - 'HOME.PLAYLISTS.INFO_DIALOG.TITLE' | translate - }} - - - - {{ - 'HOME.PLAYLISTS.INFO_DIALOG.SERVER_URL' | translate - }} - - - - {{ - 'HOME.PLAYLISTS.INFO_DIALOG.MAC_ADDRESS' | translate - }} - - - - {{ - 'HOME.PLAYLISTS.INFO_DIALOG.SERVER_URL' | translate - }} - - - - {{ - 'HOME.PLAYLISTS.INFO_DIALOG.USERNAME' | translate - }} - - - - {{ - 'HOME.PLAYLISTS.INFO_DIALOG.PASSWORD' | translate - }} - - - - {{ - 'HOME.PLAYLISTS.INFO_DIALOG.FROM_URL' | translate - }} - - - - {{ - 'HOME.PLAYLISTS.INFO_DIALOG.ORIGINAL_FILENAME' | translate - }} - - - - {{ - 'HOME.PLAYLISTS.INFO_DIALOG.IMPORT_DATE' | translate - }} - - - - {{ - 'HOME.PLAYLISTS.INFO_DIALOG.CHANNELS' | translate - }} - - - - {{ - 'HOME.PLAYLISTS.INFO_DIALOG.USER_AGENT' | translate - }} - - {{ - 'HOME.PLAYLISTS.INFO_DIALOG.CUSTOM_USER_AGENT' | translate - }} - - - - {{ 'HOME.PLAYLISTS.INFO_DIALOG.FILE_PATH' | translate }} - - - - {{ 'HOME.PLAYLISTS.INFO_DIALOG.UPDATE_FAILED' | translate }} - - - - - {{ 'HOME.PLAYLISTS.INFO_DIALOG.AUTO_UPDATE' | translate }} - -

- {{ - 'HOME.PLAYLISTS.INFO_DIALOG.AUTO_UPDATE_DESCRIPTION' - | translate - }} -

-
-
- - - - - - -
diff --git a/apps/web/src/app/home/stalker-portal-import/stalker-portal-import.component.ts b/apps/web/src/app/home/stalker-portal-import/stalker-portal-import.component.ts index 40b91299a..3174c17c4 100644 --- a/apps/web/src/app/home/stalker-portal-import/stalker-portal-import.component.ts +++ b/apps/web/src/app/home/stalker-portal-import/stalker-portal-import.component.ts @@ -11,10 +11,10 @@ import { MatFormFieldModule } from '@angular/material/form-field'; import { MatInputModule } from '@angular/material/input'; import { Store } from '@ngrx/store'; import { TranslatePipe } from '@ngx-translate/core'; +import { addPlaylist } from 'm3u-state'; import { Playlist } from 'shared-interfaces'; import { v4 as uuid } from 'uuid'; -import { DataService } from '../../services/data.service'; -import { addPlaylist } from '../../state/actions'; +import { DataService } from '../../../../../../libs/services/src/lib/data.service'; @Component({ imports: [ diff --git a/apps/web/src/app/home/url-upload/url-upload.component.spec.ts b/apps/web/src/app/home/url-upload/url-upload.component.spec.ts index 940b8605c..4d9263bd4 100644 --- a/apps/web/src/app/home/url-upload/url-upload.component.spec.ts +++ b/apps/web/src/app/home/url-upload/url-upload.component.spec.ts @@ -6,7 +6,7 @@ import { By } from '@angular/platform-browser'; import { NoopAnimationsModule } from '@angular/platform-browser/animations'; import { TranslateModule } from '@ngx-translate/core'; import { MockModule } from 'ng-mocks'; -import { DataService } from '../../services/data.service'; +import { DataService } from '../../../../../../libs/services/src/lib/data.service'; import { ElectronServiceStub } from '../../services/electron.service.stub'; import { UrlUploadComponent } from './url-upload.component'; diff --git a/apps/web/src/app/home/url-upload/url-upload.component.ts b/apps/web/src/app/home/url-upload/url-upload.component.ts index 7b6f7945b..29020ad95 100644 --- a/apps/web/src/app/home/url-upload/url-upload.component.ts +++ b/apps/web/src/app/home/url-upload/url-upload.component.ts @@ -21,7 +21,7 @@ export class UrlUploadComponent implements OnInit { @Output() urlAdded: EventEmitter = new EventEmitter(); form: FormGroup; - readonly isDesktop = window.electron; + readonly isDesktop = !!window.electron; ngOnInit() { const urlRegex = '(https?://.*?)'; diff --git a/apps/web/src/app/home/xtream-code-import/xtream-code-import.component.ts b/apps/web/src/app/home/xtream-code-import/xtream-code-import.component.ts index 4de99414e..1cbcc263d 100644 --- a/apps/web/src/app/home/xtream-code-import/xtream-code-import.component.ts +++ b/apps/web/src/app/home/xtream-code-import/xtream-code-import.component.ts @@ -12,13 +12,13 @@ import { MatIcon } from '@angular/material/icon'; import { MatInputModule } from '@angular/material/input'; import { Store } from '@ngrx/store'; import { TranslatePipe } from '@ngx-translate/core'; +import { addPlaylist } from 'm3u-state'; import { Playlist } from 'shared-interfaces'; import { v4 as uuid } from 'uuid'; import { PortalStatus, PortalStatusService, -} from '../../services/portal-status.service'; -import { addPlaylist } from '../../state/actions'; +} from '../../../../../../libs/services/src/lib/portal-status.service'; @Component({ imports: [ diff --git a/apps/web/src/app/services/player.service.ts b/apps/web/src/app/services/player.service.ts index 0850e33ba..6c9a03784 100644 --- a/apps/web/src/app/services/player.service.ts +++ b/apps/web/src/app/services/player.service.ts @@ -1,13 +1,16 @@ import { Injectable, inject } from '@angular/core'; import { MatDialog } from '@angular/material/dialog'; -import { OPEN_MPV_PLAYER, OPEN_VLC_PLAYER } from '../../shared/ipc-commands'; -import { VideoPlayer } from '../settings/settings.interface'; +import { + OPEN_MPV_PLAYER, + OPEN_VLC_PLAYER, + VideoPlayer, +} from 'shared-interfaces'; +import { DataService } from '../../../../../libs/services/src/lib/data.service'; import { ExternalPlayerInfoDialogComponent } from '../shared/components/external-player-info-dialog/external-player-info-dialog.component'; import { PlayerDialogComponent, PlayerDialogData, } from '../xtream-tauri/player-dialog/player-dialog.component'; -import { DataService } from './data.service'; import { SettingsStore } from './settings-store.service'; @Injectable({ diff --git a/apps/web/src/app/services/pwa.service.ts b/apps/web/src/app/services/pwa.service.ts index 002fb3cf4..95fbedb09 100644 --- a/apps/web/src/app/services/pwa.service.ts +++ b/apps/web/src/app/services/pwa.service.ts @@ -5,20 +5,20 @@ import { Params } from '@angular/router'; import { SwUpdate } from '@angular/service-worker'; import { Store } from '@ngrx/store'; import { TranslateService } from '@ngx-translate/core'; +import * as PlaylistActions from 'm3u-state'; import { catchError, firstValueFrom, throwError } from 'rxjs'; -import { Playlist } from 'shared-interfaces'; -import { AppConfig } from '../../environments/environment'; import { ERROR, + Playlist, PLAYLIST_PARSE_BY_URL, PLAYLIST_PARSE_RESPONSE, PLAYLIST_UPDATE, STALKER_REQUEST, XTREAM_REQUEST, XTREAM_RESPONSE, -} from '../../shared/ipc-commands'; -import * as PlaylistActions from '../state/actions'; -import { DataService } from './data.service'; +} from 'shared-interfaces'; +import { DataService } from '../../../../../libs/services/src/lib/data.service'; +import { AppConfig } from '../../environments/environment'; @Injectable({ providedIn: 'root', diff --git a/apps/web/src/app/services/settings-store.service.ts b/apps/web/src/app/services/settings-store.service.ts index 19d180a91..3f50a2870 100644 --- a/apps/web/src/app/services/settings-store.service.ts +++ b/apps/web/src/app/services/settings-store.service.ts @@ -8,11 +8,14 @@ import { } from '@ngrx/signals'; import { StorageMap } from '@ngx-pwa/local-storage'; import { firstValueFrom } from 'rxjs'; -import { Language } from '../settings/language.enum'; -import { Settings, VideoPlayer } from '../settings/settings.interface'; -import { StreamFormat } from '../settings/stream-format.enum'; -import { Theme } from '../settings/theme.enum'; -import { STORE_KEY } from '../shared/enums/store-keys.enum'; +import { Language } from '../../../../../libs/shared/interfaces/src/lib/language.enum'; +import { + Settings, + VideoPlayer, +} from '../../../../../libs/shared/interfaces/src/lib/settings.interface'; +import { STORE_KEY } from '../../../../../libs/shared/interfaces/src/lib/store-keys.enum'; +import { StreamFormat } from '../../../../../libs/shared/interfaces/src/lib/stream-format.enum'; +import { Theme } from '../../../../../libs/shared/interfaces/src/lib/theme.enum'; const DEFAULT_SETTINGS: Settings = { player: VideoPlayer.VideoJs, diff --git a/apps/web/src/app/services/settings.service.spec.ts b/apps/web/src/app/services/settings.service.spec.ts index ea9240144..5aa1d2906 100644 --- a/apps/web/src/app/services/settings.service.spec.ts +++ b/apps/web/src/app/services/settings.service.spec.ts @@ -2,8 +2,8 @@ import { HttpClientTestingModule } from '@angular/common/http/testing'; import { inject, TestBed } from '@angular/core/testing'; import { StorageMap } from '@ngx-pwa/local-storage'; import { of } from 'rxjs'; -import { Theme } from '../settings/theme.enum'; -import { STORE_KEY } from '../shared/enums/store-keys.enum'; +import { STORE_KEY } from '../../../../../libs/shared/interfaces/src/lib/store-keys.enum'; +import { Theme } from '../../../../../libs/shared/interfaces/src/lib/theme.enum'; import { SettingsService } from './settings.service'; describe('Service: Settings', () => { diff --git a/apps/web/src/app/services/settings.service.ts b/apps/web/src/app/services/settings.service.ts index 30d7040f0..d484bb1ba 100644 --- a/apps/web/src/app/services/settings.service.ts +++ b/apps/web/src/app/services/settings.service.ts @@ -2,8 +2,8 @@ import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { StorageMap } from '@ngx-pwa/local-storage'; import { catchError, map, Observable } from 'rxjs'; -import { STORE_KEY } from '../shared/enums/store-keys.enum'; -import { Theme } from './../settings/theme.enum'; +import { STORE_KEY } from '../../../../../libs/shared/interfaces/src/lib/store-keys.enum'; +import { Theme } from '../../../../../libs/shared/interfaces/src/lib/theme.enum'; @Injectable({ providedIn: 'root', diff --git a/apps/web/src/app/services/tauri.service.ts b/apps/web/src/app/services/tauri.service.ts index 99db16d8c..a0ee902f8 100644 --- a/apps/web/src/app/services/tauri.service.ts +++ b/apps/web/src/app/services/tauri.service.ts @@ -7,20 +7,20 @@ import { readTextFile } from '@tauri-apps/plugin-fs'; import { fetch } from '@tauri-apps/plugin-http'; import { parse } from 'iptv-playlist-parser'; import { createPlaylistObject } from 'm3u-utils'; -import { Playlist } from 'shared-interfaces'; -import { AppConfig } from '../../environments/environment'; import { AUTO_UPDATE_PLAYLISTS, AUTO_UPDATE_PLAYLISTS_RESPONSE, EPG_GET_PROGRAM_DONE, ERROR, + Playlist, PLAYLIST_PARSE_BY_URL, PLAYLIST_PARSE_RESPONSE, PLAYLIST_UPDATE, PLAYLIST_UPDATE_RESPONSE, XTREAM_RESPONSE, -} from '../../shared/ipc-commands'; -import { DataService } from './data.service'; +} from 'shared-interfaces'; +import { DataService } from '../../../../../libs/services/src/lib/data.service'; +import { AppConfig } from '../../environments/environment'; @Injectable({ providedIn: 'root', @@ -56,32 +56,16 @@ export class TauriService extends DataService { async sendIpcEvent(type: string, payload?: unknown) { if (type === PLAYLIST_PARSE_BY_URL) { - this.fetchFromUrl(payload); + this.fetchM3uPlaylistFromUrl(payload); } else if (type === PLAYLIST_UPDATE) { - const data = payload as { - id: string; - filePath: string; - title: string; - }; - const playlist = await readTextFile(data.filePath); - const parsedPlaylist = parse(playlist); - const playlistObject = createPlaylistObject( - data.title, - parsedPlaylist, - data.filePath, - 'FILE' + // TODO: update playlist from URL? + this.updateM3uPlaylistFromFile( + payload as { + id: string; + filePath: string; + title: string; + } ); - - window.postMessage({ - type: PLAYLIST_UPDATE_RESPONSE, - payload: { - message: 'Success! The playlist was successfully updated', - playlist: { - ...playlistObject, - _id: data.id, - }, - }, - }); } else if (type === 'XTREAM_REQUEST') { return await this.forwardXtreamRequest( payload as { url: string; params: Record } @@ -164,7 +148,7 @@ export class TauriService extends DataService { } } - async fetchStalkerData(payload: { + private async fetchStalkerData(payload: { url: string; macAddress: string; params: Record; @@ -205,49 +189,39 @@ export class TauriService extends DataService { } } - async fetchFromUrl(payload: Partial) { + private async fetchM3uPlaylistFromUrl(payload: Partial) { window.electron.fetchPlaylistByUrl(payload.url).then((result) => { + // TODO: call store and decide and store where to store based on the isTemporary flag etc window.postMessage({ type: PLAYLIST_PARSE_RESPONSE, payload: { ...result, isTemporary: payload.isTemporary }, }); }); - /* try { - const response = await fetch(payload.url); - const responseBody = await response.text(); - const parsedPlaylist = parse(responseBody); - - // Extract playlist name from URL, use "Imported from URL" as fallback - const extractedName = - payload.url && payload.url.length > 1 - ? getFilenameFromUrl(payload.url) - : ''; - const playlistName = - !extractedName || extractedName === 'Untitled playlist' - ? 'Imported from URL' - : extractedName; - - const playlist = createPlaylistObject( - playlistName, - parsedPlaylist, - payload.url, - 'URL' - ); - - window.postMessage({ - type: PLAYLIST_PARSE_RESPONSE, - payload: { ...playlist, isTemporary: payload.isTemporary }, - }); - } catch (error) { - window.postMessage({ - type: ERROR, - message: this.getErrorMessageByStatusCode(error.status), - status: error.status, - }); - } */ } - getErrorMessageByStatusCode(status: number) { + private async updateM3uPlaylistFromFile(data: { + id: string; + filePath: string; + title: string; + }) { + window.electron + .updatePlaylistFromFilePath(data.filePath, data.title) + .then((playlistObject) => { + window.postMessage({ + type: PLAYLIST_UPDATE_RESPONSE, + payload: { + message: + 'Success! The playlist was successfully updated', + playlist: { + ...playlistObject, + _id: data.id, + }, + }, + }); + }); + } + + /* private getErrorMessageByStatusCode(status: number) { let message = 'Something went wrong'; switch (status) { case 0: @@ -261,9 +235,9 @@ export class TauriService extends DataService { break; } return message; - } + } */ - async forwardXtreamRequest(payload: { + private async forwardXtreamRequest(payload: { url: string; params: Record; }) { @@ -347,7 +321,6 @@ export class TauriService extends DataService { ); } - console.log(url, queryParams); const result = await response.json(); return result; } diff --git a/apps/web/src/app/services/whats-new.service.spec.ts b/apps/web/src/app/services/whats-new.service.spec.ts deleted file mode 100644 index bc36c6cfa..000000000 --- a/apps/web/src/app/services/whats-new.service.spec.ts +++ /dev/null @@ -1,38 +0,0 @@ -/* eslint-disable @typescript-eslint/unbound-method */ -import { TestBed, inject } from '@angular/core/testing'; -import { WhatsNewService } from './whats-new.service'; - -describe('Service: WhatsNew', () => { - beforeEach(() => { - TestBed.configureTestingModule({ - providers: [WhatsNewService], - }); - }); - - it('should ...', inject([WhatsNewService], (service: WhatsNewService) => { - expect(service).toBeTruthy(); - })); - - it('should change the state', inject( - [WhatsNewService], - (service: WhatsNewService) => { - jest.spyOn(service.dialogState$, 'next'); - service.changeDialogVisibleState(true); - expect(service.dialogState$.next).toHaveBeenCalledTimes(1); - expect(service.dialogState$.next).toHaveBeenCalledWith(true); - service.changeDialogVisibleState(false); - expect(service.dialogState$.next).toHaveBeenCalledTimes(2); - expect(service.dialogState$.next).toHaveBeenCalledWith(false); - } - )); - - it('should return modal windows for the requested version ', inject( - [WhatsNewService], - (service: WhatsNewService) => { - const version = '0.6.0'; - const modals = service.getModalsByVersion(version); - expect(modals).toBeDefined(); - expect(modals).toHaveLength(service.modals[version].length); - } - )); -}); diff --git a/apps/web/src/app/services/whats-new.service.stub.ts b/apps/web/src/app/services/whats-new.service.stub.ts deleted file mode 100644 index c8f05b035..000000000 --- a/apps/web/src/app/services/whats-new.service.stub.ts +++ /dev/null @@ -1,6 +0,0 @@ -export class WhatsNewServiceStub { - changeDialogVisibleState(): void {} - getModalsByVersion(): any { - return []; - } -} diff --git a/apps/web/src/app/services/whats-new.service.ts b/apps/web/src/app/services/whats-new.service.ts deleted file mode 100644 index 78a557e24..000000000 --- a/apps/web/src/app/services/whats-new.service.ts +++ /dev/null @@ -1,315 +0,0 @@ -import { Injectable } from '@angular/core'; -/* import { ModalWindow } from 'ngx-whats-new/lib/modal-window.interface'; */ -import { BehaviorSubject } from 'rxjs'; - -@Injectable({ - providedIn: 'root', -}) -export class WhatsNewService { - /** Visibility state of the modal */ - dialogState$ = new BehaviorSubject(false); - - /** Array with all available modals */ - modals = { - '0.6.0': [ - { - imageHeight: 470, - imageBgColor: '#333', - imageSrc: './assets/updates/060/dark-theme.png', - title: 'New in v0.6.0 - Dark theme 🎉', - text: 'Finally, the dark theme is now available. Check the settings page of the application to change the theme.', - button: { - text: 'OKAY', - textColor: '#ccc', - bgColor: '#111', - }, - }, - { - imageHeight: 470, - imageBgColor: '#333', - imageSrc: './assets/updates/060/channel-logos.png', - title: 'Channel logos 📺', - text: 'The channel list was extended with the visualization of logotypes (only visible if they are defined in the playlist)', - button: { - text: 'OKAY', - textColor: '#ccc', - bgColor: '#111', - }, - }, - { - imageHeight: 470, - imageBgColor: '#333', - imageSrc: './assets/updates/060/custom-user-agent.png', - title: 'Custom user agent 🕵️‍♂️', - text: 'Some IPTV providers need a specific user-agent in order to play their playlist. Support of custom user agent is available now.', - button: { - text: 'GOT IT', - textColor: '#ccc', - bgColor: '#111', - }, - }, - ], - '0.7.0': [ - { - imageHeight: 500, - imageBgColor: '#333', - imageSrc: './assets/updates/070/refresh-playlist.png', - title: 'New in v0.7.0 - Refresh playlists 🎉', - text: "Now you don't have to delete and re-add a playlist if it changes, you can simply update it directly from the user interface.", - button: { - text: 'Nice!', - textColor: '#ccc', - bgColor: '#111', - }, - }, - { - imageHeight: 500, - imageBgColor: '#333', - imageSrc: './assets/updates/070/auto-refresh.png', - title: 'Auto-refresh playlists 🎉', - text: 'In addition, an auto-refresh playlist function is available so that the playlists will be updated automatically every time the app is started.', - button: { - text: 'Okay', - textColor: '#ccc', - bgColor: '#111', - }, - }, - ], - '0.8.0': [ - { - imageHeight: 310, - imageBgColor: '#333', - imageSrc: './assets/updates/080/translations.png', - title: 'New in v0.8.0 - New translations 🎉', - html: 'The new release includes the localization of the application into two new languages: Korean and Spanish. Many thanks to the contributors for the translation! (@chaeya, @sguinetti, anthonyaxenov)', - button: { - text: 'Nice!', - textColor: '#ccc', - bgColor: '#111', - }, - }, - { - title: 'User-Agent Support on Channel Level', - html: 'Now user-agent support can be specified not only on the playlist level but also on the level of individual channels. For this purpose, support for Kodi playlist format was added to the application.', - button: { - text: 'Next!', - textColor: '#ccc', - bgColor: '#111', - }, - }, - { - title: 'Information about current EPG program', - html: 'Similarly to digital receivers, after you switch channels at the bottom of the screen you will see a pop-up window with information about the current program #51', - imageHeight: 250, - imageBgColor: '#333', - imageSrc: './assets/updates/080/channel-info.png', - button: { - text: 'Next!', - textColor: '#ccc', - bgColor: '#111', - }, - }, - { - title: 'This and that', - html: 'In addition to the already mentioned new features, some bugs have been fixed and the internal dependencies of the application have been updated. Thanks for all the ideas, contributions and first donations.', - button: { - text: 'YAY!', - textColor: '#ccc', - bgColor: '#111', - }, - }, - ], - '0.9.0': [ - { - imageHeight: 310, - imageBgColor: '#333', - imageSrc: './assets/updates/090/reorder-playlists.gif', - title: '🎉 v0.9.0 - rearrange playlists', - html: 'The new version of the app has an option to sort the playlists using drag&drop.', - button: { - text: 'YAY!', - textColor: '#ccc', - bgColor: '#111', - }, - }, - { - imageHeight: 310, - imageBgColor: '#333', - imageSrc: './assets/updates/090/subtitle-option.png', - title: 'Global subtitle display setting', - html: 'The new version has the ability to enable/disable subtitles on a global level for all channels.', - button: { - text: 'YAY!', - textColor: '#ccc', - bgColor: '#111', - }, - }, - { - imageHeight: 310, - imageBgColor: '#333', - imageSrc: './assets/updates/090/languages.png', - title: 'Chinese translation', - html: 'Thanks to @JoJenH for the translation.', - button: { - text: 'YAY!', - textColor: '#ccc', - bgColor: '#111', - }, - }, - { - title: 'This & That', - html: ` - * improved english translation (thanks to @mbuett)
- * bugfixes and visual improvements, see changelog for more details - `, - button: { - text: 'YAY!', - textColor: '#ccc', - bgColor: '#111', - }, - }, - ], - '0.10.0': [ - { - title: "🎉 v0.10.0 - What's new", - html: ` -

Playlist navigation from the sidebar

- Switching between playlists is now possible directly from the sidebar and without interrupting channel viewing. - -

Global favorites

- Additional playlist has been added, which is generated on the fly and contains favorite channels from all existing playlists. - -

PWA

- IPTVnator is now available as a web-application, which means that you can use it directly from the browser. This means that application can be run on smartphones, tablets, set-top boxes or smart tv browsers. - -

French localization

- The app has been translated into another language and is now available in French. Many thanks to @m-p-3 for the translation! - `, - button: { - text: 'Close', - textColor: '#ccc', - bgColor: '#111', - }, - }, - ], - '0.11.0': [ - { - imageHeight: 400, - imageBgColor: '#333', - imageSrc: './assets/updates/0110/multiple-epg-sources.png', - title: 'Multiple EPG sources', - html: 'In the new version of the application, you can add more than one URL as a source of the EPG program.', - button: { - text: 'NEXT', - textColor: '#ccc', - bgColor: '#111', - }, - }, - { - imageHeight: 400, - imageBgColor: '#333', - imageSrc: './assets/updates/0110/multi-epg-view.png', - title: '🎉 Multi-EPG view', - html: 'The first version of multi-EPG view was developed, which is familiar to many users from set-top boxes. At this stage the view works in purely informative mode.', - button: { - text: 'NEXT', - textColor: '#ccc', - bgColor: '#111', - }, - }, - { - imageHeight: 400, - imageBgColor: '#333', - imageSrc: './assets/updates/0110/import-playlist-as-text.png', - title: 'Import playlist from plain text', - html: 'Another playlist import option became available - import m3u(8) as text. Just copy the playlist to the clipboard and paste it into the application without having to save it to disk.', - button: { - text: 'NEXT', - textColor: '#ccc', - bgColor: '#111', - }, - }, - { - title: 'This&that', - html: `

Improvements in PWA

- A number of visual changes have been made to improve the experience of using the app on mobile devices. -
-

Internalization

The localization of the project has been improved, but help is still needed with the translation into different languages. -
-

Dependencies updates

- The basic libraries used in the application have been updated (angular, electron etc)`, - button: { - text: 'CLOSE', - textColor: '#ccc', - bgColor: '#111', - }, - }, - ], - '0.13.0': [ - { - imageHeight: 400, - imageBgColor: '#333', - imageSrc: './assets/updates/0130/mpv-player.png', - title: '🚀 MPV player integration', - html: 'Long-awaited mpv player support, which can now be selected from the settings. Before activating the player from the settings page, make sure it is installed on your system. For more details about mvp integration, check the wiki page.', - button: { - text: 'YAY', - textColor: '#ccc', - bgColor: '#111', - }, - }, - ], - '0.14.0': [ - { - title: '🚀 New in v0.14.0', - html: `The main feature of this release is — Xtream Code IPTV support. The feature is also available in PWA, but works best in electron version of the application and combination with mpv player.
- -

Other updates:

- * possibility to specify custom path for mpv player
- * fixed an annoying bug that appeared when reopening mpv player
- * updated libraries used under the hood
- * fixed bug related to user-agent handling`, - button: { - text: 'GO!', - textColor: '#ccc', - bgColor: '#111', - }, - }, - ], - }; - - /** Options for the "what is new" modal dialogs */ - options = { - width: '500px', - customStyle: { - boxShadow: '0px 0px 10px 5px #111', - backgroundColor: '#333', - textColor: '#eee', - }, - }; - - /** - * Changes the visibility state of the modal dialog - * @param value flag to set - */ - changeDialogVisibleState(value: boolean): void { - this.dialogState$.next(value); - } - - /** - * Returns an array with modals for the provided version of the application - * @param version version string - */ - getModalsByVersion(version: string) { - return this.modals[version] || []; - } - - /** - * Returns modals with latest changes - */ - getLatestChanges() { - const modalsLength = Object.keys(this.modals).length; - const lastVersion = Object.keys(this.modals)[modalsLength - 1]; - return this.modals[lastVersion]; - } -} diff --git a/apps/web/src/app/settings/settings.component.html b/apps/web/src/app/settings/settings.component.html index 0688aa24d..a8a2ec31e 100644 --- a/apps/web/src/app/settings/settings.component.html +++ b/apps/web/src/app/settings/settings.component.html @@ -1,6 +1,6 @@ @if (!isDialog) { - } @else { -

{{ 'SETTINGS.TITLE' | translate }}

+

{{ 'SETTINGS.TITLE' | translate }}

}
-
- @if (isTauri) { - + + @if (isDesktop) { + +
+
+ {{ 'SETTINGS.EPG_URL_LABEL' | translate }} +

Add single or multiple URLs as EPG sources

+
+
+ @for (_ of epgUrl.controls; track _; let i = $index) { +
+ + {{ + 'SETTINGS.EPG_URL_PLACEHOLDER' + | translate + }} + + {{ + 'SETTINGS.EPG_URL_ERROR' | translate + }} + + + +
+ } + +
+
+ +
+ }
-
- {{ 'SETTINGS.EPG_URL_LABEL' | translate }} -

Add single or multiple URLs as EPG sources

-
-
- @for (_ of epgUrl.controls; track _; let i = $index) { -
- - {{ - 'SETTINGS.EPG_URL_PLACEHOLDER' | translate - }} - - {{ - 'SETTINGS.EPG_URL_ERROR' | translate - }} +
+ {{ 'SETTINGS.VIDEO_PLAYER_LABEL' | translate }} +

{{ 'SETTINGS.VIDEO_PLAYER_DESCRIPTION' | translate }}

+
+
+ + {{ + 'SETTINGS.VIDEO_PLAYER_PLACEHOLDER' | translate + }} + + @for (player of players; track player) { + {{ player.label }} + + } + - - -
- } - -
+
+ @if (settingsForm.value.player === 'mpv') { +
+
+ {{ 'SETTINGS.MPV_PLAYER_PATH_LABEL' | translate }} +

+ {{ 'SETTINGS.MPV_PLAYER_PATH_DESCRIPTION' | translate }} +

+
+
+ + {{ + 'SETTINGS.MPV_PLAYER_PATH' | translate + }} + + +
+
+ } + @if (settingsForm.value.player === 'vlc') { +
+
+ {{ 'SETTINGS.VLC_PLAYER_PATH_LABEL' | translate }} +

+ {{ 'SETTINGS.VLC_PLAYER_PATH_DESCRIPTION' | translate }} +

+
+
+ + {{ + 'SETTINGS.VLC_PLAYER_PATH' | translate + }} + + +
+
+ } -
- } -
-
- {{ 'SETTINGS.VIDEO_PLAYER_LABEL' | translate }} -

{{ 'SETTINGS.VIDEO_PLAYER_DESCRIPTION' | translate }}

-
-
- - {{ - 'SETTINGS.VIDEO_PLAYER_PLACEHOLDER' | translate - }} - - @for (player of players; track player) { - {{ player.label }} - - } - - -
-
- @if (settingsForm.value.player === 'mpv') { -
-
- {{ 'SETTINGS.MPV_PLAYER_PATH_LABEL' | translate }} -

- {{ 'SETTINGS.MPV_PLAYER_PATH_DESCRIPTION' | translate }} -

-
-
- - {{ - 'SETTINGS.MPV_PLAYER_PATH' | translate - }} - - -
-
- } - @if (settingsForm.value.player === 'vlc') { -
-
- {{ 'SETTINGS.VLC_PLAYER_PATH_LABEL' | translate }} -

- {{ 'SETTINGS.VLC_PLAYER_PATH_DESCRIPTION' | translate }} -

-
-
- - {{ - 'SETTINGS.VLC_PLAYER_PATH' | translate - }} - - -
-
- } - -
-
- {{ 'SETTINGS.STREAM_FORMAT' | translate }} -

{{ 'SETTINGS.STREAM_FORMAT_DESCRIPTION' | translate }}

-
-
- - {{ - 'SETTINGS.VIDEO_PLAYER_PLACEHOLDER' | translate - }} - - @for (streamFormat of streamFormatEnum | keyvalue; track streamFormat) { - {{ streamFormat.value }} - - } - - -
-
- -
-
- {{ 'SETTINGS.LANGUAGE' | translate }} -

{{ 'SETTINGS.LANGUAGE_DESCRIPTION' | translate }}

-
-
- - {{ - 'SETTINGS.VIDEO_PLAYER_PLACEHOLDER' | translate - }} - - @for (language of languageEnum | keyvalue; track language) { - {{ - 'LANGUAGES.' + language.key | translate - }} - } - - -
-
- -
-
- {{ 'SETTINGS.THEME' | translate }} -

{{ 'SETTINGS.THEME_DESCRIPTION' | translate }}

-
-
- - {{ - 'SETTINGS.VIDEO_PLAYER_PLACEHOLDER' | translate - }} - - @for (theme of themeEnum | keyvalue; track theme) { - {{ - 'THEMES.' + theme.value | translate - }} - } - - -
+
+
+ {{ 'SETTINGS.STREAM_FORMAT' | translate }} +

{{ 'SETTINGS.STREAM_FORMAT_DESCRIPTION' | translate }}

+
+
+ + {{ + 'SETTINGS.VIDEO_PLAYER_PLACEHOLDER' | translate + }} + + @for ( + streamFormat of streamFormatEnum | keyvalue; + track streamFormat + ) { + {{ streamFormat.value }} + + } + + +
-
- {{ 'SETTINGS.SHOW_CAPTIONS' | translate }} -

{{ 'SETTINGS.SHOW_CAPTIONS_DESCRIPTION' | translate }}

-
-
- -
+
+ {{ 'SETTINGS.LANGUAGE' | translate }} +

{{ 'SETTINGS.LANGUAGE_DESCRIPTION' | translate }}

+
+
+ + {{ + 'SETTINGS.VIDEO_PLAYER_PLACEHOLDER' | translate + }} + + @for ( + language of languageEnum | keyvalue; + track language + ) { + {{ + 'LANGUAGES.' + language.key | translate + }} + } + + +
+
+ +
+
+ {{ 'SETTINGS.THEME' | translate }} +

{{ 'SETTINGS.THEME_DESCRIPTION' | translate }}

+
+
+ + {{ + 'SETTINGS.VIDEO_PLAYER_PLACEHOLDER' | translate + }} + + @for (theme of themeEnum | keyvalue; track theme) { + {{ + 'THEMES.' + theme.value | translate + }} + } + + +
+
+ +
+
+ {{ 'SETTINGS.SHOW_CAPTIONS' | translate }} +

{{ 'SETTINGS.SHOW_CAPTIONS_DESCRIPTION' | translate }}

+
+
+ +
@if (settingsForm.value.remoteControl === true) { - -
-
- {{ 'SETTINGS.REMOTE_CONTROL_PORT' | translate }} -

- {{ - 'SETTINGS.REMOTE_CONTROL_PORT_DESCRIPTION' - | translate - }} -

+ +
+
+ {{ 'SETTINGS.REMOTE_CONTROL_PORT' | translate }} +

+ {{ + 'SETTINGS.REMOTE_CONTROL_PORT_DESCRIPTION' + | translate + }} +

+
+
+ + + +
-
- - - -
-
}
-
- {{ 'SETTINGS.VERSION' | translate }} -

{{ 'SETTINGS.VERSION_DESCRIPTION' | translate }}

-
-
- {{ version }}
- {{ updateMessage }} -
+
+ {{ 'SETTINGS.VERSION' | translate }} +

{{ 'SETTINGS.VERSION_DESCRIPTION' | translate }}

+
+
+ {{ version }}
+ {{ updateMessage }} +
-
- {{ 'SETTINGS.IMPORT_EXPORT_DATA' | translate }} -

- {{ 'SETTINGS.IMPORT_EXPORT_DATA_DESCRIPTION' | translate }} -

-
-
-
+
+ - + -
+ > + {{ 'SETTINGS.EXPORT_DATA' | translate }} + +
-
- {{ 'SETTINGS.REMOVE_ALL' | translate }} -

- {{ 'SETTINGS.REMOVE_ALL_DESCRIPTION' | translate }} -

-
-
-
+
+ -
+ color="warn" + > + {{ 'SETTINGS.REMOVE_ALL_BUTTON' | translate }} + +
@if (isPwa) { -
- {{ 'SETTINGS.EPG_NOTE' | translate }} -  {{ - 'SETTINGS.EPG_NOTE_URL_TEXT' | translate - }} -
- } +
+ {{ 'SETTINGS.EPG_NOTE' | translate }} +  {{ + 'SETTINGS.EPG_NOTE_URL_TEXT' | translate + }} +
+ } - -
- - -
+ +
+ + +
-
+ diff --git a/apps/web/src/app/settings/settings.component.scss b/apps/web/src/app/settings/settings.component.scss index 0fbed37d0..46ac2d60e 100644 --- a/apps/web/src/app/settings/settings.component.scss +++ b/apps/web/src/app/settings/settings.component.scss @@ -32,6 +32,10 @@ button { } } +.margin-right { + margin-right: 10px; +} + .action-buttons { margin-top: 10px; diff --git a/apps/web/src/app/settings/settings.component.spec.ts b/apps/web/src/app/settings/settings.component.spec.ts index af28ecac2..dbb5f5460 100644 --- a/apps/web/src/app/settings/settings.component.spec.ts +++ b/apps/web/src/app/settings/settings.component.spec.ts @@ -25,23 +25,19 @@ import { MockProvider, MockProviders, } from 'ng-mocks'; -import { DataService } from '../services/data.service'; +import { Language, StreamFormat, Theme, VideoPlayer } from 'shared-interfaces'; +import { DataService } from '../../../../../libs/services/src/lib/data.service'; import { HeaderComponent } from '../shared/components'; -import { StreamFormat } from './stream-format.enum'; -import { Language } from './language.enum'; import { SettingsComponent } from './settings.component'; -import { VideoPlayer } from './settings.interface'; -import { Theme } from './theme.enum'; import { signal } from '@angular/core'; import { NgxIndexedDBService } from 'ngx-indexed-db'; -import { NgxWhatsNewModule } from 'ngx-whats-new'; import { of } from 'rxjs'; -import { SETTINGS_UPDATE } from '../../../shared/ipc-commands'; -import { DialogService } from '../services/dialog.service'; +import { SETTINGS_UPDATE } from 'shared-interfaces'; +import { DialogService } from '../../../../../libs/services/src/lib/dialog.service'; +import { EpgService } from '../../../../../libs/services/src/lib/epg.service'; +import { PlaylistsService } from '../../../../../libs/services/src/lib/playlists.service'; import { ElectronServiceStub } from '../services/electron.service.stub'; -import { EpgService } from '../services/epg.service'; -import { PlaylistsService } from '../services/playlists.service'; import { SettingsStore } from '../services/settings-store.service'; import { SettingsService } from '../services/settings.service'; @@ -129,7 +125,6 @@ describe('SettingsComponent', () => { MockModule(MatCheckboxModule), MockModule(MatDividerModule), TranslateModule.forRoot(), - NgxWhatsNewModule, ], }).compileComponents(); })); diff --git a/apps/web/src/app/settings/settings.component.ts b/apps/web/src/app/settings/settings.component.ts index 2f6490a4d..9b998b442 100644 --- a/apps/web/src/app/settings/settings.component.ts +++ b/apps/web/src/app/settings/settings.component.ts @@ -32,27 +32,27 @@ import { MatTooltipModule } from '@angular/material/tooltip'; import { Router } from '@angular/router'; import { Store } from '@ngrx/store'; import { TranslateModule, TranslateService } from '@ngx-translate/core'; +import * as PlaylistActions from 'm3u-state'; +import { selectIsEpgAvailable } from 'm3u-state'; import { take } from 'rxjs'; import * as semver from 'semver'; -import { Playlist } from 'shared-interfaces'; import { + Language, + Playlist, SET_MPV_PLAYER_PATH, SET_VLC_PLAYER_PATH, SETTINGS_UPDATE, -} from '../../shared/ipc-commands'; -import { DataService } from '../services/data.service'; -import { DialogService } from '../services/dialog.service'; -import { EpgService } from '../services/epg.service'; -import { PlaylistsService } from '../services/playlists.service'; + StreamFormat, + Theme, + VideoPlayer, +} from 'shared-interfaces'; +import { DataService } from '../../../../../libs/services/src/lib/data.service'; +import { DialogService } from '../../../../../libs/services/src/lib/dialog.service'; +import { EpgService } from '../../../../../libs/services/src/lib/epg.service'; +import { PlaylistsService } from '../../../../../libs/services/src/lib/playlists.service'; import { SettingsStore } from '../services/settings-store.service'; import { HeaderComponent } from '../shared/components/header/header.component'; -import * as PlaylistActions from '../state/actions'; -import { selectIsEpgAvailable } from '../state/selectors'; import { SettingsService } from './../services/settings.service'; -import { Language } from './language.enum'; -import { VideoPlayer } from './settings.interface'; -import { StreamFormat } from './stream-format.enum'; -import { Theme } from './theme.enum'; @Component({ templateUrl: './settings.component.html', @@ -74,6 +74,18 @@ import { Theme } from './theme.enum'; ], }) export class SettingsComponent implements OnInit { + private dialogService = inject(DialogService); + public dataService = inject(DataService); + private epgService = inject(EpgService); + private formBuilder = inject(FormBuilder); + private playlistsService = inject(PlaylistsService); + private router = inject(Router); + private settingsService = inject(SettingsService); + private snackBar = inject(MatSnackBar); + private store = inject(Store); + private translate = inject(TranslateService); + private matDialog = inject(MatDialog); + @Input() isDialog = false; /** List with available languages as enum */ readonly languageEnum = Language; @@ -82,7 +94,7 @@ export class SettingsComponent implements OnInit { readonly streamFormatEnum = StreamFormat; /** Flag that indicates whether the app runs in electron environment */ - readonly isTauri = this.dataService.getAppEnvironment() === 'tauri'; + readonly isDesktop = !!window.electron; isPwa = this.dataService.getAppEnvironment() === 'pwa'; @@ -111,7 +123,7 @@ export class SettingsComponent implements OnInit { id: VideoPlayer.ArtPlayer, label: 'ArtPlayer', }, - ...(this.isTauri ? this.osPlayers : []), + ...(this.isDesktop ? this.osPlayers : []), ]; /** Current version of the app */ @@ -129,7 +141,7 @@ export class SettingsComponent implements OnInit { /** Settings form object */ settingsForm = this.formBuilder.group({ player: [VideoPlayer.VideoJs], - ...(this.isTauri ? { epgUrl: new FormArray([]) } : {}), + ...(this.isDesktop ? { epgUrl: new FormArray([]) } : {}), streamFormat: StreamFormat.M3u8StreamFormat, language: Language.ENGLISH, showCaptions: false, @@ -145,22 +157,7 @@ export class SettingsComponent implements OnInit { private settingsStore = inject(SettingsStore); - /** - * Creates an instance of SettingsComponent and injects - * required dependencies into the component - */ constructor( - private dialogService: DialogService, - public dataService: DataService, - private epgService: EpgService, - private formBuilder: FormBuilder, - private playlistsService: PlaylistsService, - private router: Router, - private settingsService: SettingsService, - private snackBar: MatSnackBar, - private store: Store, - private translate: TranslateService, - private matDialog: MatDialog, @Optional() @Inject(MAT_DIALOG_DATA) data?: { isDialog: boolean } ) { this.isDialog = data?.isDialog ?? false; @@ -182,7 +179,7 @@ export class SettingsComponent implements OnInit { const currentSettings = this.settingsStore.getSettings(); this.settingsForm.patchValue(currentSettings); - if (this.isTauri && currentSettings.epgUrl) { + if (this.isDesktop && currentSettings.epgUrl) { this.setEpgUrls(currentSettings.epgUrl); } } @@ -282,7 +279,7 @@ export class SettingsComponent implements OnInit { */ applyChangedSettings(): void { this.settingsForm.markAsPristine(); - if (this.isTauri) { + if (this.isDesktop) { let epgUrls = this.settingsForm.value.epgUrl; if (epgUrls) { if (!Array.isArray(epgUrls)) { diff --git a/apps/web/src/app/shared/components/add-playlist/add-playlist-dialog.component.ts b/apps/web/src/app/shared/components/add-playlist/add-playlist-dialog.component.ts index fb8d01b36..10be580b8 100644 --- a/apps/web/src/app/shared/components/add-playlist/add-playlist-dialog.component.ts +++ b/apps/web/src/app/shared/components/add-playlist/add-playlist-dialog.component.ts @@ -1,4 +1,4 @@ -import { Component, Inject } from '@angular/core'; +import { Component, inject, Inject } from '@angular/core'; import { MatButtonModule } from '@angular/material/button'; import { MAT_DIALOG_DATA, @@ -8,15 +8,15 @@ import { import { MatSnackBar } from '@angular/material/snack-bar'; import { Store } from '@ngrx/store'; import { TranslateModule, TranslateService } from '@ngx-translate/core'; +import { parsePlaylist } from 'm3u-state'; import { getFilenameFromUrl } from 'm3u-utils'; -import { PLAYLIST_PARSE_BY_URL } from '../../../../shared/ipc-commands'; +import { PLAYLIST_PARSE_BY_URL } from 'shared-interfaces'; +import { DataService } from '../../../../../../../libs/services/src/lib/data.service'; import { FileUploadComponent } from '../../../home/file-upload/file-upload.component'; import { StalkerPortalImportComponent } from '../../../home/stalker-portal-import/stalker-portal-import.component'; import { TextImportComponent } from '../../../home/text-import/text-import.component'; import { UrlUploadComponent } from '../../../home/url-upload/url-upload.component'; import { XtreamCodeImportComponent } from '../../../home/xtream-code-import/xtream-code-import.component'; -import { DataService } from '../../../services/data.service'; -import { parsePlaylist } from '../../../state/actions'; export type PlaylistType = 'xtream' | 'url' | 'text' | 'file' | 'stalker'; @@ -35,16 +35,15 @@ export type PlaylistType = 'xtream' | 'url' | 'text' | 'file' | 'stalker'; templateUrl: './add-playlist-dialog.component.html', }) export class AddPlaylistDialogComponent { - playlistType!: PlaylistType; + private dataService = inject(DataService); + private dialogRef = inject(MatDialogRef); + private store = inject(Store); + private snackBar = inject(MatSnackBar); + private translateService = inject(TranslateService); - constructor( - @Inject(MAT_DIALOG_DATA) data: { type: PlaylistType }, - private dataService: DataService, - private dialogRef: MatDialogRef, - private store: Store, - private snackBar: MatSnackBar, - private translateService: TranslateService - ) { + readonly playlistType!: PlaylistType; + + constructor(@Inject(MAT_DIALOG_DATA) data: { type: PlaylistType }) { this.playlistType = data.type; } diff --git a/apps/web/src/app/shared/components/header/header.component.html b/apps/web/src/app/shared/components/header/header.component.html index 995353abe..8c6938a93 100644 --- a/apps/web/src/app/shared/components/header/header.component.html +++ b/apps/web/src/app/shared/components/header/header.component.html @@ -1,10 +1,14 @@
-
{{ title }}
-
{{ subtitle }}
+
{{ title() }}
+
{{ subtitle() }}
@if (isHome) {