mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-11 11:06:16 -08:00
refactor(playlist-switcher): add context actions for playlist and account info
Entire-Checkpoint: 0e4488af6c76
This commit is contained in:
1 parent
e579c21486
commit
eaaf634b17
6 files changed
+149
-60
No files matched your search
@@ -137,4 +137,32 @@
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@if (showPlaylistInfo() || showAccountInfo()) {
|
||||
<mat-divider />
|
||||
<div class="context-actions-section">
|
||||
@if (showPlaylistInfo()) {
|
||||
<button
|
||||
class="context-action-item"
|
||||
(click)="playlistInfoRequested.emit()"
|
||||
>
|
||||
<mat-icon>playlist_play</mat-icon>
|
||||
<span>{{
|
||||
'WORKSPACE.SHELL.PLAYLIST_INFO' | translate
|
||||
}}</span>
|
||||
</button>
|
||||
}
|
||||
@if (showAccountInfo()) {
|
||||
<button
|
||||
class="context-action-item"
|
||||
(click)="accountInfoRequested.emit()"
|
||||
>
|
||||
<mat-icon>person</mat-icon>
|
||||
<span>{{
|
||||
'WORKSPACE.SHELL.ACCOUNT_INFO' | translate
|
||||
}}</span>
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</mat-menu>
|
||||
@@ -371,6 +371,40 @@
|
||||
}
|
||||
}
|
||||
|
||||
.context-actions-section {
|
||||
padding: 6px 8px 8px;
|
||||
|
||||
.context-action-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
width: 100%;
|
||||
padding: 8px 10px;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
background: transparent;
|
||||
color: var(--mat-sys-on-surface-variant);
|
||||
font: inherit;
|
||||
font-size: 0.86em;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition:
|
||||
background 0.15s ease,
|
||||
color 0.15s ease;
|
||||
|
||||
&:hover {
|
||||
background: var(--mat-sys-surface-container);
|
||||
color: var(--mat-sys-on-surface);
|
||||
}
|
||||
|
||||
mat-icon {
|
||||
font-size: 18px;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 480px) {
|
||||
.playlist-info {
|
||||
max-width: 140px;
|
||||
|
||||
@@ -15,6 +15,7 @@ import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { MatIconButton } from '@angular/material/button';
|
||||
import { MatRippleModule } from '@angular/material/core';
|
||||
import { MatDivider } from '@angular/material/divider';
|
||||
import { MatIcon } from '@angular/material/icon';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { MatMenu, MatMenuModule, MatMenuTrigger } from '@angular/material/menu';
|
||||
@@ -92,6 +93,7 @@ const M3U_SECTIONS = ['all', 'groups', 'favorites', 'recent'] as const;
|
||||
imports: [
|
||||
DatePipe,
|
||||
FormsModule,
|
||||
MatDivider,
|
||||
MatIcon,
|
||||
MatIconButton,
|
||||
MatInputModule,
|
||||
@@ -112,8 +114,16 @@ export class PlaylistSwitcherComponent {
|
||||
readonly currentTitle = input.required<string>();
|
||||
/** Subtitle to display (e.g., "123 Channels" or "Xtream Code") */
|
||||
readonly subtitle = input<string>('');
|
||||
/** Whether the "Playlist Info" action is available */
|
||||
readonly showPlaylistInfo = input(false);
|
||||
/** Whether the "Account Info" action is available */
|
||||
readonly showAccountInfo = input(false);
|
||||
/** Emitted when a different playlist is selected */
|
||||
readonly playlistSelected = output<string>();
|
||||
/** Emitted when user clicks "Playlist Info" */
|
||||
readonly playlistInfoRequested = output<void>();
|
||||
/** Emitted when user clicks "Account Info" */
|
||||
readonly accountInfoRequested = output<void>();
|
||||
|
||||
readonly menuTrigger = viewChild.required<MatMenuTrigger>('menuTrigger');
|
||||
readonly playlistMenu = viewChild.required<MatMenu>('playlistMenu');
|
||||
|
||||
+18
-58
@@ -21,6 +21,13 @@
|
||||
|
||||
@if (primaryContextLinks().length > 0) {
|
||||
<div class="rail-divider"></div>
|
||||
@if (railPlaylistLabel(); as label) {
|
||||
<span
|
||||
class="rail-context-label"
|
||||
[matTooltip]="label"
|
||||
[matTooltipPosition]="'right'"
|
||||
>{{ label }}</span>
|
||||
}
|
||||
<app-portal-rail-links
|
||||
variant="rail"
|
||||
activeClass="is-active"
|
||||
@@ -58,6 +65,10 @@
|
||||
class="playlist-switcher"
|
||||
[currentTitle]="playlistTitle()"
|
||||
[subtitle]="playlistSubtitle()"
|
||||
[showPlaylistInfo]="canOpenPlaylistInfo()"
|
||||
[showAccountInfo]="canOpenAccountInfo()"
|
||||
(playlistInfoRequested)="openPlaylistInfo()"
|
||||
(accountInfoRequested)="openAccountInfo()"
|
||||
/>
|
||||
|
||||
<label class="search-field">
|
||||
@@ -103,7 +114,7 @@
|
||||
/>
|
||||
<span class="header-actions-divider" aria-hidden="true"></span>
|
||||
<a
|
||||
class="header-shortcut"
|
||||
class="header-shortcut header-shortcut--global"
|
||||
mat-icon-button
|
||||
routerLink="/workspace/global-favorites"
|
||||
[class.is-active]="isGlobalFavoritesRoute()"
|
||||
@@ -141,71 +152,20 @@
|
||||
<mat-icon>download</mat-icon>
|
||||
</button>
|
||||
}
|
||||
@if (hasContextActions()) {
|
||||
@if (headerBulkAction(); as action) {
|
||||
<span class="header-actions-divider" aria-hidden="true"></span>
|
||||
<button
|
||||
type="button"
|
||||
mat-icon-button
|
||||
[matMenuTriggerFor]="contextActionsMenu"
|
||||
[matTooltip]="
|
||||
'WORKSPACE.SHELL.RAIL_CONTEXT_ACTIONS' | translate
|
||||
"
|
||||
[attr.aria-label]="
|
||||
'WORKSPACE.SHELL.OPEN_CONTEXT_ACTIONS' | translate
|
||||
"
|
||||
(click)="runHeaderBulkAction()"
|
||||
[disabled]="action.disabled"
|
||||
[matTooltip]="action.tooltip"
|
||||
[attr.aria-label]="action.ariaLabel"
|
||||
>
|
||||
<mat-icon>more_horiz</mat-icon>
|
||||
<mat-icon>{{ action.icon }}</mat-icon>
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
|
||||
<mat-menu #contextActionsMenu="matMenu">
|
||||
@if (!hasContextActions()) {
|
||||
<button mat-menu-item disabled>
|
||||
<span>{{
|
||||
'WORKSPACE.SHELL.NO_SECTION_ACTIONS' | translate
|
||||
}}</span>
|
||||
</button>
|
||||
} @else {
|
||||
@if (contextActionGroups().hasPlaylistActions) {
|
||||
@if (canOpenPlaylistInfo()) {
|
||||
<button mat-menu-item (click)="openPlaylistInfo()">
|
||||
<mat-icon>playlist_play</mat-icon>
|
||||
<span>{{
|
||||
'WORKSPACE.SHELL.PLAYLIST_INFO' | translate
|
||||
}}</span>
|
||||
</button>
|
||||
}
|
||||
@if (canOpenAccountInfo()) {
|
||||
<button mat-menu-item (click)="openAccountInfo()">
|
||||
<mat-icon>person</mat-icon>
|
||||
<span>{{
|
||||
'WORKSPACE.SHELL.ACCOUNT_INFO' | translate
|
||||
}}</span>
|
||||
</button>
|
||||
}
|
||||
}
|
||||
|
||||
@if (
|
||||
contextActionGroups().hasCleanupActions &&
|
||||
contextActionGroups().hasPlaylistActions
|
||||
) {
|
||||
<mat-divider />
|
||||
}
|
||||
|
||||
@if (headerBulkAction(); as action) {
|
||||
<button
|
||||
mat-menu-item
|
||||
(click)="runHeaderBulkAction()"
|
||||
[disabled]="action.disabled"
|
||||
[attr.aria-label]="action.ariaLabel"
|
||||
>
|
||||
<mat-icon>{{ action.icon }}</mat-icon>
|
||||
<span>{{ action.tooltip }}</span>
|
||||
</button>
|
||||
}
|
||||
}
|
||||
</mat-menu>
|
||||
</header>
|
||||
|
||||
<div class="workspace-body" [class.single-pane]="!showContextPanel()">
|
||||
|
||||
@@ -137,6 +137,37 @@
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
// ─── Rail Context Label ──────────────────────────────────────────────────────
|
||||
.rail-context-label {
|
||||
font-size: 9px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.04em;
|
||||
text-transform: uppercase;
|
||||
color: var(--mat-sys-on-surface-variant);
|
||||
opacity: 0.55;
|
||||
max-width: 44px;
|
||||
text-align: center;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
padding: 0 2px;
|
||||
margin-bottom: 2px;
|
||||
cursor: default;
|
||||
app-region: no-drag;
|
||||
animation: rail-label-enter 0.2s ease-out both;
|
||||
}
|
||||
|
||||
@keyframes rail-label-enter {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-4px);
|
||||
}
|
||||
to {
|
||||
opacity: 0.55;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Rail Divider ─────────────────────────────────────────────────────────────
|
||||
.rail-divider {
|
||||
width: 44px;
|
||||
@@ -323,6 +354,24 @@
|
||||
}
|
||||
}
|
||||
|
||||
.header-shortcut--global {
|
||||
position: relative;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: 5px;
|
||||
right: 5px;
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
border-radius: 50%;
|
||||
background: var(--mat-sys-tertiary);
|
||||
border: 1.5px solid
|
||||
var(--app-header-bg, var(--mat-sys-surface-container-low));
|
||||
z-index: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.header-actions-divider {
|
||||
width: 1px;
|
||||
height: 20px;
|
||||
@@ -533,6 +582,10 @@
|
||||
margin: 0 4px;
|
||||
}
|
||||
|
||||
.rail-context-label {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.rail-footer {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
@@ -13,7 +13,6 @@ import {
|
||||
} from '@angular/core/rxjs-interop';
|
||||
import { MatIconButton } from '@angular/material/button';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { MatDividerModule } from '@angular/material/divider';
|
||||
import { MatIcon } from '@angular/material/icon';
|
||||
import { MatMenuModule } from '@angular/material/menu';
|
||||
import { MatTooltip } from '@angular/material/tooltip';
|
||||
@@ -98,7 +97,6 @@ const SEARCH_INPUT_DEBOUNCE_MS = 350;
|
||||
imports: [
|
||||
MatIcon,
|
||||
MatIconButton,
|
||||
MatDividerModule,
|
||||
MatMenuModule,
|
||||
MatTooltip,
|
||||
AddPlaylistMenuComponent,
|
||||
@@ -365,6 +363,12 @@ export class WorkspaceShellComponent {
|
||||
|
||||
return 'WORKSPACE.SHELL.SEARCH_PLAYLIST_PLACEHOLDER';
|
||||
});
|
||||
readonly railPlaylistLabel = computed<string | null>(() => {
|
||||
const context = this.railContext();
|
||||
if (!context) return null;
|
||||
const active = this.activePlaylist();
|
||||
return active?.title || active?.filename || null;
|
||||
});
|
||||
readonly primaryContextLinks = computed<PortalRailLink[]>(() => {
|
||||
this.languageTick();
|
||||
|
||||
|
||||
Reference in new issue
Block a user