mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-16 14:16:16 -08:00
style: enhance UI components with improved styles, search functionality, and error handling
This commit is contained in:
1 parent
aa6a753aa5
commit
09efead104
15 files changed
+482
-156
No files matched your search
@@ -88,8 +88,8 @@ mat-card:hover {
|
||||
position: absolute;
|
||||
top: 6px;
|
||||
right: 6px;
|
||||
// Use a semi-transparent dark pill, not hardcoded green
|
||||
background: rgba(0, 0, 0, 0.65);
|
||||
// Use a solid or highly opaque dark pill to avoid expensive backdrop-filter
|
||||
background: rgba(0, 0, 0, 0.85);
|
||||
color: #4ade80; // green-400, readable across themes
|
||||
font-size: 0.72rem;
|
||||
font-weight: 600;
|
||||
@@ -99,7 +99,6 @@ mat-card:hover {
|
||||
align-items: center;
|
||||
padding: 2px 5px;
|
||||
gap: 2px;
|
||||
backdrop-filter: blur(4px);
|
||||
|
||||
mat-icon {
|
||||
font-size: 13px;
|
||||
|
||||
@@ -60,9 +60,7 @@
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
backdrop-filter: blur(8px);
|
||||
-webkit-backdrop-filter: blur(8px);
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
background: rgba(0, 0, 0, 0.85);
|
||||
color: #fff;
|
||||
|
||||
&.live {
|
||||
|
||||
@@ -131,7 +131,7 @@
|
||||
<mat-icon>{{ action.icon }}</mat-icon>
|
||||
</button>
|
||||
}
|
||||
<button
|
||||
<!-- <button
|
||||
type="button"
|
||||
class="header-shortcut"
|
||||
mat-icon-button
|
||||
@@ -142,7 +142,7 @@
|
||||
aria-label="Toggle favorites only"
|
||||
>
|
||||
<mat-icon>favorite</mat-icon>
|
||||
</button>
|
||||
</button> -->
|
||||
@if (canSort()) {
|
||||
<button
|
||||
type="button"
|
||||
|
||||
+31
-12
@@ -12,11 +12,13 @@
|
||||
>
|
||||
<div class="sidebar-header">
|
||||
@if (selectedCategoryId()) {
|
||||
<button mat-icon-button (click)="backToCategories()">
|
||||
<!-- <button mat-icon-button (click)="backToCategories()">
|
||||
<mat-icon>chevron_left</mat-icon>
|
||||
</button>
|
||||
</button> -->
|
||||
<h2 class="category-title">
|
||||
{{ selectedCategoryInfo()?.name }} ({{ selectedCategoryInfo()?.count }})
|
||||
{{ selectedCategoryInfo()?.name }} ({{
|
||||
selectedCategoryInfo()?.count
|
||||
}})
|
||||
</h2>
|
||||
<button
|
||||
mat-icon-button
|
||||
@@ -27,19 +29,28 @@
|
||||
<mat-icon>sort_by_alpha</mat-icon>
|
||||
</button>
|
||||
<mat-menu #channelSortMenu="matMenu">
|
||||
<button mat-menu-item (click)="setLiveChannelSortMode('server')">
|
||||
<button
|
||||
mat-menu-item
|
||||
(click)="setLiveChannelSortMode('server')"
|
||||
>
|
||||
<span>Server Order</span>
|
||||
@if (liveChannelSortMode() === 'server') {
|
||||
<mat-icon>check</mat-icon>
|
||||
}
|
||||
</button>
|
||||
<button mat-menu-item (click)="setLiveChannelSortMode('name-asc')">
|
||||
<button
|
||||
mat-menu-item
|
||||
(click)="setLiveChannelSortMode('name-asc')"
|
||||
>
|
||||
<span>Name A-Z</span>
|
||||
@if (liveChannelSortMode() === 'name-asc') {
|
||||
<mat-icon>check</mat-icon>
|
||||
}
|
||||
</button>
|
||||
<button mat-menu-item (click)="setLiveChannelSortMode('name-desc')">
|
||||
<button
|
||||
mat-menu-item
|
||||
(click)="setLiveChannelSortMode('name-desc')"
|
||||
>
|
||||
<span>Name Z-A</span>
|
||||
@if (liveChannelSortMode() === 'name-desc') {
|
||||
<mat-icon>check</mat-icon>
|
||||
@@ -50,7 +61,11 @@
|
||||
@if (!isWorkspaceLayout) {
|
||||
<app-playlist-switcher
|
||||
class="switcher"
|
||||
[currentTitle]="currentPlaylist()?.name || currentPlaylist()?.title || 'Playlist'"
|
||||
[currentTitle]="
|
||||
currentPlaylist()?.name ||
|
||||
currentPlaylist()?.title ||
|
||||
'Playlist'
|
||||
"
|
||||
[subtitle]="'Xtream Code'"
|
||||
/>
|
||||
}
|
||||
@@ -58,7 +73,9 @@
|
||||
mat-icon-button
|
||||
class="manage-categories-btn"
|
||||
(click)="openCategoryManagement()"
|
||||
[matTooltip]="'XTREAM.CATEGORY_MANAGEMENT.TITLE' | translate"
|
||||
[matTooltip]="
|
||||
'XTREAM.CATEGORY_MANAGEMENT.TITLE' | translate
|
||||
"
|
||||
>
|
||||
<mat-icon>tune</mat-icon>
|
||||
</button>
|
||||
@@ -95,10 +112,12 @@
|
||||
</div>
|
||||
} @else {
|
||||
<div class="no-channel-selected">
|
||||
<button mat-icon-button disabled>
|
||||
<mat-icon>category</mat-icon>
|
||||
</button>
|
||||
{{ 'PORTALS.SELECT_CATEGORY' | translate }}
|
||||
@if (!selectedCategoryId()) {
|
||||
<button mat-icon-button disabled>
|
||||
<mat-icon>category</mat-icon>
|
||||
</button>
|
||||
{{ 'PORTALS.SELECT_CATEGORY' | translate }}
|
||||
}
|
||||
</div>
|
||||
}
|
||||
<!-- <app-mpv-player-bar /> -->
|
||||
|
||||
+96
-69
@@ -1,84 +1,111 @@
|
||||
<div class="search-panel">
|
||||
<mat-form-field class="search-bar" subscriptSizing="dynamic">
|
||||
<mat-icon matPrefix>search</mat-icon>
|
||||
<label class="search-field">
|
||||
<mat-icon>search</mat-icon>
|
||||
<input
|
||||
matInput
|
||||
spellcheck="false"
|
||||
type="search"
|
||||
spellcheck="false"
|
||||
[placeholder]="'CHANNELS.SEARCH_CHANNEL' | translate"
|
||||
[(ngModel)]="searchString"
|
||||
/>
|
||||
</mat-form-field>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="channels-list">
|
||||
<cdk-virtual-scroll-viewport
|
||||
[itemSize]="68"
|
||||
class="scroll-viewport-portals"
|
||||
>
|
||||
<div
|
||||
*cdkVirtualFor="
|
||||
let item of sortedChannels() | filterBy: searchString() : 'title';
|
||||
trackBy: trackBy;
|
||||
templateCacheSize: 0
|
||||
"
|
||||
class="channel-item"
|
||||
[class.active]="xtreamStore.selectedItem()?.xtream_id === item.xtream_id"
|
||||
(click)="playClicked.emit(item)"
|
||||
>
|
||||
<img
|
||||
class="stream-icon"
|
||||
[src]="item.stream_icon ?? item.poster_url"
|
||||
(error)="$event.target.src = './assets/images/default-poster.png'"
|
||||
alt="channel icon"
|
||||
/>
|
||||
<div class="channel-info">
|
||||
<div class="title">{{ item.title }}</div>
|
||||
@if (currentPrograms.get(item.xtream_id)) {
|
||||
<div class="program-info">
|
||||
<div class="current-program">
|
||||
{{ currentPrograms.get(item.xtream_id) }}
|
||||
</div>
|
||||
@if (currentProgramsProgress.has(item.xtream_id)) {
|
||||
<div class="progress-container">
|
||||
<span>{{
|
||||
programTimings.get(item.xtream_id)?.start
|
||||
| date: 'HH:mm'
|
||||
}}</span>
|
||||
<div class="progress-bar">
|
||||
<div
|
||||
class="progress"
|
||||
[style.width.%]="
|
||||
currentProgramsProgress.get(item.xtream_id)
|
||||
"
|
||||
></div>
|
||||
@if (
|
||||
sortedChannels() | filterBy: searchString() : 'title';
|
||||
as filteredChannels
|
||||
) {
|
||||
@if (filteredChannels.length > 0) {
|
||||
<cdk-virtual-scroll-viewport
|
||||
[itemSize]="68"
|
||||
class="scroll-viewport-portals"
|
||||
>
|
||||
<div
|
||||
*cdkVirtualFor="
|
||||
let item of filteredChannels;
|
||||
trackBy: trackBy;
|
||||
templateCacheSize: 0
|
||||
"
|
||||
class="channel-item"
|
||||
[class.active]="
|
||||
xtreamStore.selectedItem()?.xtream_id === item.xtream_id
|
||||
"
|
||||
(click)="playClicked.emit(item)"
|
||||
>
|
||||
<img
|
||||
class="stream-icon"
|
||||
[src]="item.stream_icon ?? item.poster_url"
|
||||
(error)="
|
||||
$event.target.src =
|
||||
'./assets/images/default-poster.png'
|
||||
"
|
||||
alt="channel icon"
|
||||
/>
|
||||
<div class="channel-info">
|
||||
<div class="title">{{ item.title }}</div>
|
||||
@if (currentPrograms.get(item.xtream_id)) {
|
||||
<div class="program-info">
|
||||
<div class="current-program">
|
||||
{{ currentPrograms.get(item.xtream_id) }}
|
||||
</div>
|
||||
@if (
|
||||
currentProgramsProgress.has(item.xtream_id)
|
||||
) {
|
||||
<div class="progress-container">
|
||||
<span>{{
|
||||
programTimings.get(item.xtream_id)
|
||||
?.start | date: 'HH:mm'
|
||||
}}</span>
|
||||
<div class="progress-bar">
|
||||
<div
|
||||
class="progress"
|
||||
[style.width.%]="
|
||||
currentProgramsProgress.get(
|
||||
item.xtream_id
|
||||
)
|
||||
"
|
||||
></div>
|
||||
</div>
|
||||
<span>{{
|
||||
programTimings.get(item.xtream_id)
|
||||
?.end | date: 'HH:mm'
|
||||
}}</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
} @else {
|
||||
<div class="program-info no-epg-placeholder">
|
||||
<div class="placeholder-bar"></div>
|
||||
<div class="progress-container">
|
||||
<span>--:--</span>
|
||||
<div class="progress-bar"></div>
|
||||
<span>--:--</span>
|
||||
</div>
|
||||
<span>{{
|
||||
programTimings.get(item.xtream_id)?.end
|
||||
| date: 'HH:mm'
|
||||
}}</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
} @else {
|
||||
<div class="program-info no-epg-placeholder">
|
||||
<div class="placeholder-bar"></div>
|
||||
<div class="progress-container">
|
||||
<span>--:--</span>
|
||||
<div class="progress-bar"></div>
|
||||
<span>--:--</span>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
<button
|
||||
class="favorite-btn"
|
||||
(click)="toggleFavorite($event, item)"
|
||||
>
|
||||
<mat-icon>
|
||||
{{
|
||||
favorites.get(item.xtream_id)
|
||||
? 'star'
|
||||
: 'star_outline'
|
||||
}}
|
||||
</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
</cdk-virtual-scroll-viewport>
|
||||
} @else {
|
||||
<div class="empty-search-state">
|
||||
<div class="empty-icon-wrapper">
|
||||
<mat-icon>search_off</mat-icon>
|
||||
</div>
|
||||
<h3>{{ 'CHANNELS.NO_CHANNELS_FOUND' | translate }}</h3>
|
||||
<p>We couldn't find anything matching "{{ searchString() }}"</p>
|
||||
</div>
|
||||
<button
|
||||
class="favorite-btn"
|
||||
(click)="toggleFavorite($event, item)"
|
||||
>
|
||||
<mat-icon>
|
||||
{{ favorites.get(item.xtream_id) ? 'star' : 'star_outline' }}
|
||||
</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
</cdk-virtual-scroll-viewport>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
+157
-22
@@ -32,32 +32,68 @@
|
||||
.channel-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 8px;
|
||||
position: relative;
|
||||
padding: 8px 8px 8px 12px;
|
||||
margin: 2px 4px;
|
||||
border-radius: 4px;
|
||||
border-radius: 10px;
|
||||
border: 1px solid transparent;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s ease;
|
||||
background: transparent;
|
||||
color: var(--mat-sys-on-surface-variant);
|
||||
height: 64px;
|
||||
box-sizing: border-box;
|
||||
gap: 8px;
|
||||
// Critical: use margin-based calc and ensure it doesn't overflow
|
||||
width: calc(100% - 8px);
|
||||
min-width: 0; // Allow shrinking below content size
|
||||
overflow: hidden;
|
||||
min-width: 0;
|
||||
transition:
|
||||
background 0.15s ease,
|
||||
color 0.15s ease,
|
||||
border-color 0.15s ease,
|
||||
box-shadow 0.18s ease,
|
||||
transform 0.18s ease;
|
||||
|
||||
&:hover {
|
||||
background-color: color-mix(in srgb, var(--mat-sys-on-surface) 8%, transparent);
|
||||
background: var(--mat-sys-surface-container);
|
||||
color: var(--mat-sys-on-surface);
|
||||
|
||||
.title {
|
||||
color: var(--mat-sys-primary);
|
||||
color: var(--mat-sys-on-surface);
|
||||
}
|
||||
}
|
||||
|
||||
&.active {
|
||||
background-color: color-mix(in srgb, var(--mat-sys-primary) 16%, transparent);
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
var(--app-selection-surface-strong),
|
||||
var(--app-selection-surface)
|
||||
) !important;
|
||||
color: var(--app-selection-color) !important;
|
||||
border-color: var(--app-selection-border) !important;
|
||||
font-weight: 600;
|
||||
box-shadow:
|
||||
inset 0 0 0 1px var(--app-selection-border),
|
||||
0 12px 20px -18px var(--app-selection-glow);
|
||||
transform: translateY(-1px);
|
||||
|
||||
.title {
|
||||
color: var(--mat-sys-primary);
|
||||
* {
|
||||
color: var(--app-selection-color) !important;
|
||||
}
|
||||
|
||||
.current-program,
|
||||
.progress-container {
|
||||
opacity: 0.92;
|
||||
}
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset-inline-start: 6px;
|
||||
top: 7px;
|
||||
bottom: 7px;
|
||||
width: 3px;
|
||||
border-radius: 999px;
|
||||
background: var(--app-selection-color);
|
||||
box-shadow: 0 0 10px var(--app-selection-glow);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -162,26 +198,63 @@
|
||||
}
|
||||
|
||||
.search-panel {
|
||||
padding: 8px 12px;
|
||||
padding: 10px 12px;
|
||||
flex-shrink: 0;
|
||||
height: 56px;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.search-bar {
|
||||
.search-field {
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
@include mat.form-field-density(-5);
|
||||
}
|
||||
height: 36px;
|
||||
border-radius: 10px;
|
||||
border: 1px solid var(--app-search-border, var(--mat-sys-outline-variant));
|
||||
background: var(--app-search-bg, var(--mat-sys-surface-container));
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 0 12px;
|
||||
box-sizing: border-box;
|
||||
cursor: text;
|
||||
transition:
|
||||
border-color 0.15s ease,
|
||||
background-color 0.15s ease,
|
||||
box-shadow 0.15s ease;
|
||||
|
||||
::ng-deep {
|
||||
.search-bar {
|
||||
div {
|
||||
border-radius: 20px;
|
||||
}
|
||||
&:focus-within {
|
||||
border-color: var(--mat-sys-primary);
|
||||
box-shadow: 0 0 0 3px rgba(var(--mat-sys-primary), 0.12);
|
||||
}
|
||||
|
||||
div[matformfieldlineripple] {
|
||||
display: none;
|
||||
mat-icon {
|
||||
font-size: 18px;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
color: var(--mat-sys-on-surface-variant);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
input {
|
||||
flex: 1;
|
||||
border: none;
|
||||
outline: none;
|
||||
background: transparent;
|
||||
color: var(--mat-sys-on-surface);
|
||||
font: inherit;
|
||||
font-size: 0.875rem;
|
||||
|
||||
&:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
&::placeholder {
|
||||
color: var(--mat-sys-on-surface-variant);
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -236,3 +309,65 @@
|
||||
height: 48px;
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Empty State ──────────────────────────────────────────────────────────────
|
||||
.empty-search-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
padding: 32px 24px;
|
||||
text-align: center;
|
||||
color: var(--mat-sys-on-surface-variant);
|
||||
animation: fadeIn 0.3s ease-out;
|
||||
|
||||
.empty-icon-wrapper {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
border-radius: 20px;
|
||||
background: var(--mat-sys-surface-container);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 20px;
|
||||
box-shadow:
|
||||
0 8px 24px -8px rgba(0, 0, 0, 0.1),
|
||||
inset 0 1px 1px rgba(255, 255, 255, 0.05);
|
||||
|
||||
mat-icon {
|
||||
font-size: 32px;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
color: var(--mat-sys-on-surface-variant);
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 1.125rem;
|
||||
font-weight: 600;
|
||||
margin: 0 0 8px 0;
|
||||
color: var(--mat-sys-on-surface);
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 0.875rem;
|
||||
margin: 0;
|
||||
opacity: 0.7;
|
||||
line-height: 1.4;
|
||||
max-width: 260px;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
@@ -30,7 +30,7 @@
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
background: rgba(0, 0, 0, 0.85);
|
||||
padding: 6px 8px;
|
||||
font-size: 12px;
|
||||
color: white;
|
||||
@@ -39,7 +39,6 @@
|
||||
text-overflow: ellipsis;
|
||||
border-top-left-radius: inherit;
|
||||
border-top-right-radius: inherit;
|
||||
backdrop-filter: blur(5px);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
|
||||
@@ -324,13 +324,12 @@
|
||||
position: absolute;
|
||||
bottom: 8px;
|
||||
left: 8px;
|
||||
background: rgba(0, 0, 0, 0.75);
|
||||
background: rgba(0, 0, 0, 0.85);
|
||||
padding: 2px 6px;
|
||||
border-radius: 3px;
|
||||
font-size: 0.7rem;
|
||||
font-weight: 600;
|
||||
color: white;
|
||||
backdrop-filter: blur(4px);
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
@@ -440,13 +439,12 @@
|
||||
position: absolute;
|
||||
bottom: 6px;
|
||||
right: 6px;
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
background: rgba(0, 0, 0, 0.85);
|
||||
padding: 2px 6px;
|
||||
border-radius: 3px;
|
||||
font-size: 0.7rem;
|
||||
font-weight: 600;
|
||||
color: white;
|
||||
backdrop-filter: blur(4px);
|
||||
}
|
||||
|
||||
&__info {
|
||||
|
||||
@@ -236,6 +236,7 @@
|
||||
"LOADING": "Loading channels...",
|
||||
"LOADING_MORE": "Loading more...",
|
||||
"NO_SEARCH_RESULTS": "No channels found",
|
||||
"NO_CHANNELS_FOUND": "No channels found",
|
||||
"TRY_DIFFERENT_SEARCH": "Try a different search term"
|
||||
},
|
||||
"TOP_MENU": {
|
||||
|
||||
@@ -47,6 +47,7 @@
|
||||
text-overflow: ellipsis;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
.nav-item-meta {
|
||||
|
||||
Reference in new issue
Block a user