fix(ui): one provider icon per playlist row, imperative Korean remove label

A restored Stalker or Xtream playlist can also carry a URL, and the row's
independent checks then showed the M3U URL icon next to the provider icon.
The row now switches on resolvePlaylistSourceIconKey(), the precedence every
other surface uses, so each source shows exactly one icon.

HOME.PLAYLISTS.REMOVE now names the confirm button and the row's delete
tooltip; in Korean it read "the playlist has been removed". It now says
"remove playlist", like every other locale.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
4grayandClaude Opus 5.5 committed 2026-10-01 18:59:23 +02:00
1 parent 082c56608b
commit fb293ea23d
4 files changed
+98 -57

No files matched your search

+1 -1
View File
@@ -65,7 +65,7 @@
"CHANNELS": "채널",
"ADDED": "추가되었습니다",
"SHOW_DETAILS": "재생 목록 세부 정보 표시",
"REMOVE": "재생 목록이 제거되었습니다",
"REMOVE": "재생 목록 제거",
"SEARCH_PLAYLISTS": "제목으로 재생목록 검색",
"INFO_DIALOG": {
"PLAYLIST_DETAILS": "재생 목록 세부 사항",
@@ -17,69 +17,68 @@
[enabled]="!isBusy()"
/>
}
@if (item.url) {
<div class="icon-container">
<!-- One provider icon per row, by the shared precedence: a restored
Stalker or Xtream playlist can also carry a URL. -->
@switch (sourceIconKey) {
@case ('stalker') {
<mat-icon
class="upload-type-icon"
[matTooltip]="
('HOME.PLAYLISTS.ADDED_VIA_URL' | translate) +
' ' +
item.url
"
>{{ sourceIcons['m3u-url'] }}</mat-icon
[matTooltip]="'HOME.PLAYLISTS.STALKER_PORTAL' | translate"
>{{ sourceIcons.stalker }}</mat-icon
>
@if (item.autoRefresh) {
}
@case ('xtream') {
<div class="icon-container">
<mat-icon
class="auto-refresh-indicator"
class="upload-type-icon"
[matTooltip]="
'HOME.PLAYLISTS.AUTO_REFRESH_ENABLED' | translate
'HOME.PLAYLISTS.XTREAM_PLAYLIST' | translate
"
>autorenew</mat-icon
>{{ sourceIcons.xtream }}</mat-icon
>
}
</div>
}
@if (!item.url && !item.serverUrl && !item.macAddress) {
<div class="icon-container">
<mat-icon
class="upload-type-icon"
[matTooltip]="'HOME.PLAYLISTS.ADDED_VIA_FILE' | translate"
>{{ sourceIcons['m3u-local'] }}</mat-icon
>
@if (item.autoRefresh) {
<mat-icon
class="auto-refresh-indicator"
[matTooltip]="
'HOME.PLAYLISTS.AUTO_REFRESH_ENABLED' | translate
"
>autorenew</mat-icon
>
}
</div>
}
@if (item.serverUrl) {
<div class="icon-container">
<mat-icon
class="upload-type-icon"
[matTooltip]="'HOME.PLAYLISTS.XTREAM_PLAYLIST' | translate"
>{{ sourceIcons.xtream }}</mat-icon
>
@if (!runtime.supportsSourceHealth) {
<span
class="status-dot"
[class]="getStatusClass()"
[matTooltip]="'Portal status: ' + portalStatus"
[attr.aria-label]="'Portal status: ' + portalStatus"
></span>
}
</div>
}
@if (item.macAddress) {
<mat-icon
class="upload-type-icon"
[matTooltip]="'HOME.PLAYLISTS.STALKER_PORTAL' | translate"
>{{ sourceIcons.stalker }}</mat-icon
>
@if (!runtime.supportsSourceHealth) {
<span
class="status-dot"
[class]="getStatusClass()"
[matTooltip]="'Portal status: ' + portalStatus"
[attr.aria-label]="'Portal status: ' + portalStatus"
></span>
}
</div>
}
@default {
<div class="icon-container">
@if (sourceIconKey === 'm3u-url') {
<mat-icon
class="upload-type-icon"
[matTooltip]="
('HOME.PLAYLISTS.ADDED_VIA_URL' | translate) +
' ' +
item.url
"
>{{ sourceIcons['m3u-url'] }}</mat-icon
>
} @else {
<mat-icon
class="upload-type-icon"
[matTooltip]="
'HOME.PLAYLISTS.ADDED_VIA_FILE' | translate
"
>{{ sourceIcons['m3u-local'] }}</mat-icon
>
}
@if (item.autoRefresh) {
<mat-icon
class="auto-refresh-indicator"
[matTooltip]="
'HOME.PLAYLISTS.AUTO_REFRESH_ENABLED'
| translate
"
>autorenew</mat-icon
>
}
</div>
}
}
</div>
<div class="playlist-content">
@@ -221,4 +221,40 @@ describe('PlaylistItemComponent', () => {
).toContain('42%');
expect(nativeElement.querySelector('.cancel-btn')).not.toBeNull();
});
it.each([
[
{ macAddress: '00:1A:79:00:00:01', url: 'http://portal.test' },
'cast',
],
[
{ serverUrl: 'http://xtream.test', url: 'http://xtream.test' },
'cloud',
],
[{ url: 'http://list.test/playlist.m3u' }, 'link'],
[{}, 'description'],
])(
'shows one provider icon for %o, with the shared precedence',
(source, icon) => {
fixture.destroy();
fixture = TestBed.createComponent(PlaylistItemComponent);
fixture.componentInstance.item = {
title: 'Source',
_id: 'source',
count: 10,
importDate: Date.now().toString(),
autoRefresh: false,
...source,
};
fixture.detectChanges();
const icons = Array.from(
(fixture.nativeElement as HTMLElement).querySelectorAll(
'.upload-type-icon'
),
(element) => element.textContent?.trim()
);
expect(icons).toEqual([icon]);
}
);
});
@@ -1,6 +1,8 @@
import { Injector } from '@angular/core';
import { SourceHealthService } from '@iptvnator/portal/shared/data-access';
import {
PlaylistSourceIconKey,
resolvePlaylistSourceIconKey,
SOURCE_TYPE_ICONS,
sourceHealthType,
} from '@iptvnator/shared/interfaces';
@@ -72,6 +74,10 @@ export class PlaylistItemComponent implements OnInit {
private readonly portalStatusService = inject(PortalStatusService);
readonly runtime = inject(RuntimeCapabilitiesService);
readonly sourceIcons = SOURCE_TYPE_ICONS;
get sourceIconKey(): PlaylistSourceIconKey {
return resolvePlaylistSourceIconKey(this.item);
}
private readonly translate = inject(TranslateService);
private readonly languageTick = toSignal(
this.translate.onLangChange.pipe(startWith(null)),