refactor(playlist): replace mat-list-item with div for improved layout and styling consistency

Entire-Checkpoint: 271bd0162027
This commit is contained in:
4gray committed 2026-03-29 23:51:21 +02:00
1 parent 94e9a5119c
commit f87a261fb1
8 files changed
+243 -232

No files matched your search

@@ -1,4 +1,5 @@
<mat-list-item
<div
class="playlist-item"
cdkDrag
[cdkDragDisabled]="!isDraggable() || isBusy()"
cdkDragPreviewContainer="parent"
@@ -7,140 +8,149 @@
(click)="onPlaylistClick()"
>
@if (isDraggable()) {
<mat-icon cdkDragHandle class="drag-icon" matListItemIcon
>drag_indicator</mat-icon
>
<mat-icon cdkDragHandle class="drag-icon">drag_indicator</mat-icon>
}
@if (item.url) {
<div class="icon-container" matListItemIcon>
<mat-icon
class="upload-type-icon"
[matTooltip]="
('HOME.PLAYLISTS.ADDED_VIA_URL' | translate) + ' ' + item.url
"
>cloud</mat-icon
>
@if (item.autoRefresh) {
<mat-icon
class="auto-refresh-indicator"
[matTooltip]="'HOME.PLAYLISTS.AUTO_REFRESH_ENABLED' | translate"
>autorenew</mat-icon
>
}
</div>
}
@if (!item.url && !item.serverUrl && !item.macAddress) {
<div class="icon-container" matListItemIcon>
<mat-icon
class="upload-type-icon"
[matTooltip]="'HOME.PLAYLISTS.ADDED_VIA_FILE' | translate"
>folder</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" matListItemIcon>
<mat-icon
class="upload-type-icon"
[matTooltip]="'HOME.PLAYLISTS.XTREAM_PLAYLIST' | translate"
>public</mat-icon
>
<span
class="status-dot"
[class]="getStatusClass()"
[matTooltip]="'Portal status: ' + portalStatus"
[attr.aria-label]="'Portal status: ' + portalStatus"
></span>
</div>
}
@if (item.macAddress) {
<mat-icon
matListItemIcon
class="upload-type-icon"
[matTooltip]="'HOME.PLAYLISTS.STALKER_PORTAL' | translate"
>dashboard</mat-icon
>
}
<div matListItemTitle>
{{ item.title || item.filename }}
</div>
<div matListItemLine class="meta">
@if (!item.serverUrl && !item.portalUrl) {
{{ 'HOME.PLAYLISTS.CHANNELS' | translate }}: {{ item.count }}
@if (item.importDate.length > 3) {
| {{ 'HOME.PLAYLISTS.ADDED' | translate }}:
{{ item.importDate | date }}
}
@if (item.updateDate) {
| {{ 'HOME.PLAYLISTS.UPDATED' | translate }}:
{{ item.updateDate | date: 'MMMM d, yyyy, HH:mm' }}
}
@if (item.updateState === 2) {
<div class="playlist-icon">
@if (item.url) {
<div class="icon-container">
<mat-icon
class="upload-type-icon"
[matTooltip]="
'HOME.PLAYLISTS.INFO_DIALOG.UPDATE_FAILED' | translate
('HOME.PLAYLISTS.ADDED_VIA_URL' | translate) +
' ' +
item.url
"
[inline]="true"
>warning</mat-icon
>cloud</mat-icon
>
}
} @else {
@if (item.macAddress) {
Stalker Portal
} @else {
Xtream Code Playlist
}
@if (item.importDate.length > 3) {
| {{ 'HOME.PLAYLISTS.ADDED' | translate }}:
{{ item.importDate | date }}
}
@if (item.updateDate) {
| {{ 'HOME.PLAYLISTS.UPDATED' | translate }}:
{{ item.updateDate | date: 'MMMM d, yyyy, HH:mm' }}
}
@if (item.autoRefresh) {
<mat-icon
class="auto-refresh-indicator"
[matTooltip]="
'HOME.PLAYLISTS.AUTO_REFRESH_ENABLED' | translate
"
>autorenew</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"
>folder</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"
>public</mat-icon
>
<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"
>dashboard</mat-icon
>
}
</div>
@if (isBusy() && busyMessage()) {
<div matListItemLine class="busy-state">
<span class="busy-state__message">{{ busyMessage() }}</span>
@if (busyProgress() !== null) {
<span class="busy-state__value">{{ busyProgress() }}%</span>
<div class="playlist-content">
<div class="playlist-title">
{{ item.title || item.filename }}
</div>
<div class="meta">
@if (!item.serverUrl && !item.portalUrl) {
{{ 'HOME.PLAYLISTS.CHANNELS' | translate }}: {{ item.count }}
@if (item.importDate.length > 3) {
| {{ 'HOME.PLAYLISTS.ADDED' | translate }}:
{{ item.importDate | date }}
}
@if (item.updateDate) {
| {{ 'HOME.PLAYLISTS.UPDATED' | translate }}:
{{ item.updateDate | date: 'MMMM d, yyyy, HH:mm' }}
}
@if (item.updateState === 2) {
<mat-icon
[matTooltip]="
'HOME.PLAYLISTS.INFO_DIALOG.UPDATE_FAILED'
| translate
"
[inline]="true"
>warning</mat-icon
>
}
} @else {
@if (item.macAddress) {
Stalker
} @else {
Xtream
}
@if (item.importDate.length > 3) {
| {{ 'HOME.PLAYLISTS.ADDED' | translate }}:
{{ item.importDate | date }}
}
@if (item.updateDate) {
| {{ 'HOME.PLAYLISTS.UPDATED' | translate }}:
{{ item.updateDate | date: 'MMMM d, yyyy, HH:mm' }}
}
}
</div>
}
@if (isBusy() && busyMessage() && busyProgress() !== null) {
<div matListItemLine class="busy-state__bar">
<mat-progress-bar
mode="determinate"
[value]="busyProgress() ?? 0"
/>
</div>
}
@if (isBusy() && busyMessage() && busyProgress() === null) {
<div matListItemLine class="busy-state__bar">
<mat-progress-bar mode="indeterminate" />
</div>
}
@if (isBusy() && busyMessage()) {
<div class="busy-state">
<span class="busy-state__message">{{ busyMessage() }}</span>
@if (busyProgress() !== null) {
<span class="busy-state__value">{{ busyProgress() }}%</span>
}
</div>
}
@if (isBusy() && busyMessage() && busyProgress() !== null) {
<div class="busy-state__bar">
<mat-progress-bar
mode="determinate"
[value]="busyProgress() ?? 0"
/>
</div>
}
@if (isBusy() && busyMessage() && busyProgress() === null) {
<div class="busy-state__bar">
<mat-progress-bar mode="indeterminate" />
</div>
}
</div>
@if (showActions()) {
<span matListItemMeta class="action-buttons">
<div class="action-buttons">
@if (canCancelBusyAction()) {
<button
mat-icon-button
color="warn"
class="cancel-btn"
(click)="
$event.stopPropagation();
cancelBusyActionClicked.emit()
$event.stopPropagation(); cancelBusyActionClicked.emit()
"
[matTooltip]="
'WORKSPACE.SHELL.XTREAM_IMPORT_CANCEL' | translate
"
[matTooltip]="'WORKSPACE.SHELL.XTREAM_IMPORT_CANCEL' | translate"
>
<mat-icon>close</mat-icon>
</button>
@@ -149,7 +159,6 @@
<button
mat-icon-button
class="refresh-btn"
color="accent"
(click)="
$event.stopPropagation(); refreshClicked.emit(item)
"
@@ -171,7 +180,6 @@
<button
mat-icon-button
class="refresh-btn"
color="accent"
(click)="
$event.stopPropagation(); refreshClicked.emit(item)
"
@@ -192,7 +200,6 @@
<button
mat-icon-button
color="accent"
class="edit-btn"
(click)="
$event.stopPropagation(); editPlaylistClicked.emit(item)
@@ -205,7 +212,6 @@
<button
mat-icon-button
color="accent"
class="delete-btn"
(click)="$event.stopPropagation(); removeClicked.emit(item._id)"
[disabled]="isBusy()"
@@ -221,6 +227,6 @@
<mat-icon>delete</mat-icon>
}
</button>
</span>
</div>
}
</mat-list-item>
</div>
@@ -1,12 +1,30 @@
mat-list-item {
cursor: pointer;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
border-radius: 12px;
margin: 2px 4px;
:host {
display: block;
width: 100%;
}
.playlist-item {
width: 100%;
min-height: 68px;
display: flex;
align-items: center;
gap: 12px;
padding: 8px 10px 8px 12px;
border: 1px solid transparent;
border-radius: 8px;
background: transparent;
color: inherit;
cursor: pointer;
box-sizing: border-box;
transition:
background 0.15s ease,
border-color 0.15s ease,
box-shadow 0.18s ease,
transform 0.18s ease,
color 0.15s ease;
&:hover {
background-color: rgba(255, 255, 255, 0.03) !important;
background: color-mix(in srgb, currentColor 8%, transparent);
}
&.selected {
@@ -14,9 +32,9 @@ mat-list-item {
135deg,
var(--app-selection-surface-strong),
var(--app-selection-surface)
) !important;
border-color: var(--app-selection-border) !important;
color: var(--app-selection-color) !important;
);
border-color: var(--app-selection-border);
color: var(--app-selection-color);
box-shadow:
inset 0 0 0 1px var(--app-selection-border),
0 12px 20px -18px var(--app-selection-glow);
@@ -27,7 +45,7 @@ mat-list-item {
.drag-icon,
.action-buttons button,
.action-buttons mat-icon {
color: inherit !important;
color: inherit;
}
.status-dot {
@@ -44,21 +62,41 @@ mat-list-item {
}
}
// Light theme hover
@media (prefers-color-scheme: light) {
mat-list-item:hover {
background-color: rgba(0, 0, 0, 0.04) !important;
}
.playlist-icon {
flex-shrink: 0;
display: flex;
align-items: center;
}
.mat-list-base {
padding-top: 0 !important;
.playlist-content {
min-width: 0;
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
gap: 2px;
}
.playlist-title {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: 0.96rem;
font-weight: 600;
letter-spacing: -0.01em;
line-height: 1.2;
}
.meta {
font-size: 12px !important;
color: #666;
margin-top: 2px !important;
font-size: 0.82rem;
color: var(--mat-sys-on-surface-variant);
opacity: 0.84;
line-height: 1.2;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.busy-state {
@@ -66,9 +104,9 @@ mat-list-item {
align-items: center;
justify-content: space-between;
gap: 12px;
font-size: 11px !important;
font-size: 0.7rem;
color: var(--app-selection-border, #8aa4c8);
margin-top: 4px !important;
margin-top: 2px;
}
.busy-state__message {
@@ -82,12 +120,12 @@ mat-list-item {
}
.busy-state__bar {
margin-top: 4px !important;
margin-top: 4px;
}
.cdk-drag-preview {
box-sizing: border-box;
border-radius: 12px;
border-radius: 8px;
box-shadow:
0 8px 24px rgba(0, 0, 0, 0.25),
0 4px 12px rgba(0, 0, 0, 0.15),
@@ -104,56 +142,49 @@ mat-list-item {
}
.drag-icon {
flex-shrink: 0;
cursor: move;
margin-right: 0;
opacity: 0.5;
transition: opacity 0.2s ease;
opacity: 0.46;
transition: opacity 0.15s ease;
&:hover {
opacity: 1;
}
}
::ng-deep {
@media only screen and (max-width: 480px) {
.main-container {
.favorites-icon,
.upload-type-icon,
.refresh-btn,
.edit-btn,
.delete-btn {
display: none;
}
}
.mdc-list-item__content {
padding-left: 20px;
}
}
}
.action-buttons {
align-self: center !important;
flex-shrink: 0;
display: flex;
align-items: center;
gap: 2px;
margin-inline-start: auto;
button {
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
width: 34px;
height: 34px;
border-radius: 10px;
color: inherit;
opacity: 0.76;
transition:
opacity 0.15s ease,
background-color 0.15s ease,
transform 0.15s ease;
&:hover {
background-color: rgba(255, 255, 255, 0.06) !important;
transform: scale(1.05);
mat-icon {
opacity: 1;
}
opacity: 1;
background: color-mix(in srgb, currentColor 10%, transparent);
transform: translateY(-1px);
}
mat-icon {
opacity: 0.85;
transition: opacity 0.3s ease;
width: 20px;
height: 20px;
font-size: 20px;
}
&:disabled {
transform: none;
opacity: 0.38;
}
}
}
@@ -163,23 +194,16 @@ mat-list-item {
margin: 0 auto;
}
// Light theme action buttons
@media (prefers-color-scheme: light) {
.action-buttons button:hover {
background-color: rgba(0, 0, 0, 0.04) !important;
}
}
.icon-container {
position: relative;
display: inline-flex;
.upload-type-icon {
opacity: 0.75;
transition: opacity 0.3s ease;
transition: opacity 0.15s ease;
}
mat-list-item:hover & .upload-type-icon {
.playlist-item:hover & .upload-type-icon {
opacity: 0.95;
}
}
@@ -191,9 +215,9 @@ mat-list-item {
border-radius: 50%;
right: 0;
bottom: 0;
border: 2px solid var(--background-color, #131316);
border: 2px solid var(--mat-sys-surface);
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
transition: all 0.3s ease;
transition: all 0.15s ease;
}
.status-active {
@@ -212,7 +236,8 @@ mat-list-item {
}
.status-unavailable {
background-color: #666666;
background-color: var(--mat-sys-on-surface-variant);
opacity: 0.5;
}
.auto-refresh-indicator {
@@ -223,30 +248,26 @@ mat-list-item {
width: 14px !important;
height: 14px !important;
color: #4caf50;
background-color: var(--background-color, #131316);
background-color: var(--mat-sys-surface);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
transition: all 0.3s ease;
transition: all 0.15s ease;
mat-list-item:hover & {
.playlist-item:hover & {
color: #66bb6a;
transform: rotate(90deg);
}
}
// Light theme auto-refresh indicator
@media (prefers-color-scheme: light) {
.auto-refresh-indicator {
background-color: #ffffff;
}
}
@media only screen and (max-width: 480px) {
.upload-type-icon,
.icon-container {
.icon-container,
.action-buttons .refresh-btn,
.action-buttons .edit-btn,
.action-buttons .delete-btn {
display: none;
}
}
@@ -11,7 +11,6 @@ import {
} from '@angular/core';
import { MatIconButton } from '@angular/material/button';
import { MatIcon } from '@angular/material/icon';
import { MatListModule } from '@angular/material/list';
import { MatProgressBarModule } from '@angular/material/progress-bar';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { MatTooltip } from '@angular/material/tooltip';
@@ -28,7 +27,6 @@ import { PlaylistMeta } from 'shared-interfaces';
DragDropModule,
MatIconButton,
MatIcon,
MatListModule,
MatProgressBarModule,
MatProgressSpinnerModule,
MatTooltip,
@@ -11,7 +11,7 @@
<app-empty-state [type]="'no-results'" />
}
} @else {
<mat-nav-list
<div
class="playlists-list"
cdkDropList
[cdkDropListDisabled]="!isCustomSortActive()"
@@ -39,6 +39,6 @@
(cancelBusyActionClicked)="cancelBusyOperation(item)"
/>
}
</mat-nav-list>
</div>
}
}
@@ -1,35 +1,16 @@
@use '@angular/material' as mat;
:host {
display: flex;
flex-direction: column;
align-items: center;
}
.mat-list-base {
padding-top: 0;
}
.playlists-list {
padding: 0;
padding: 0 6px;
width: 100%;
overflow-x: hidden;
box-sizing: border-box;
}
.meta {
font-size: 12px !important;
color: #666;
margin-top: 2px !important;
}
// Light theme
@media (prefers-color-scheme: light) {
.meta {
color: #757575;
}
}
#skeleton-container {
padding-top: 10px;
width: 100%;
@@ -52,6 +33,6 @@
@media only screen and (max-width: 480px) {
.favorites-icon {
padding-left: 0 !important;
display: none
display: none;
}
}
@@ -7,7 +7,6 @@ import { Component, effect, inject, input, output, signal } from '@angular/core'
import { toSignal } from '@angular/core/rxjs-interop';
import { MatDialog } from '@angular/material/dialog';
import { MatInputModule } from '@angular/material/input';
import { MatListModule } from '@angular/material/list';
import { MatSnackBar } from '@angular/material/snack-bar';
import { Router } from '@angular/router';
import { Store } from '@ngrx/store';
@@ -52,7 +51,6 @@ type PlaylistBusyOperation = {
DragDropModule,
EmptyStateComponent,
MatInputModule,
MatListModule,
PlaylistItemComponent,
],
})