UNPKG

polyv-live-cli

Version:

CLI tool for managing PolyV live streaming services.

858 lines 31.8 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.ChannelStatusPanel = void 0; const blessed_1 = __importDefault(require("blessed")); const blessed_contrib_1 = require("blessed-contrib"); const base_component_1 = require("./base.component"); const channel_details_popup_1 = require("./channel-details.popup"); const batch_operations_dialog_1 = require("./batch-operations.dialog"); class ChannelStatusPanel extends base_component_1.BaseComponent { constructor(config, eventBus) { super(config, eventBus); this.channels = []; this.filteredChannels = []; this.selectedChannels = new Set(); this.currentIndex = 0; this.selectionStartIndex = -1; this.isRangeSelecting = false; this.lastJumpPosition = 0; this.navigationHistory = []; this.sortConfig = { field: config.sortField || 'name', order: config.sortOrder || 'asc' }; this.filterConfig = config.filters || {}; this.lastUpdateTime = new Date(); } createWidget() { this.container = blessed_1.default.box({ label: ' Channel Status Monitor ', border: { type: 'line' }, style: { fg: 'white', bg: 'black', border: { fg: 'cyan' } }, top: this.config.position.y, left: this.config.position.x, width: this.config.position.width, height: this.config.position.height, scrollable: false, mouse: true, keys: true, vi: true, tags: true }); this.headerBox = blessed_1.default.box({ parent: this.container, top: 0, left: 0, width: '100%', height: 2, content: this.getHeaderContent(), style: { fg: 'yellow' }, tags: true }); this.footerBox = blessed_1.default.box({ parent: this.container, bottom: 0, left: 0, width: '100%', height: 2, content: this.getFooterContent(), style: { fg: 'gray' }, tags: true }); const tableConfig = { keys: true, interactive: true, columnSpacing: 2, columnWidth: this.getConfig().columnWidths || [20, 12, 10, 10, 15, 15], style: { fg: 'white', bg: 'black', border: { fg: 'gray' }, header: { fg: 'cyan', bold: true }, cell: { fg: 'white' }, selected: { bg: 'blue', fg: 'white' } } }; this.table = (0, blessed_contrib_1.table)(tableConfig); this.table.parent = this.container; this.table.top = 2; this.table.left = 0; this.table.width = '100%'; this.table.height = 'bottom-2'; this.noDataBox = blessed_1.default.box({ parent: this.container, top: 'center', left: 'center', width: 'shrink', height: 'shrink', content: this.getNoDataMessage(), style: { fg: 'gray' }, tags: true, hidden: true }); this.widget = this.container; this.initializeDetailsPopup(); this.initializeBatchDialog(); this.setupKeyBindings(); this.updateTableDisplay(); } initializeDetailsPopup() { this.detailsPopup = new channel_details_popup_1.ChannelDetailsPopup({ screen: this.widget.screen, eventBus: this.eventBus, showColors: this.getConfig().showColors, refreshInterval: this.getConfig().refreshInterval }); } initializeBatchDialog() { this.batchDialog = new batch_operations_dialog_1.BatchOperationsDialog({ screen: this.widget.screen, eventBus: this.eventBus, showColors: this.getConfig().showColors }); } setupKeyBindings() { this.container.key(['up', 'k'], () => { this.moveSelection(-1); }); this.container.key(['down', 'j'], () => { this.moveSelection(1); }); this.container.key(['pageup'], () => { this.moveSelection(-10); }); this.container.key(['pagedown'], () => { this.moveSelection(10); }); this.container.key(['home'], () => { this.moveToTop(); }); this.container.key(['end'], () => { this.moveToBottom(); }); this.container.key(['space'], () => { this.toggleSelection(); }); this.container.key(['enter'], () => { this.showChannelDetails(); }); this.container.key(['s'], () => { this.cycleSortField(); }); this.container.key(['S'], () => { this.toggleSortOrder(); }); this.container.key(['f'], () => { this.showFilterDialog(); }); this.container.key(['F'], () => { this.showAdvancedFilterDialog(); }); this.container.key(['c'], () => { this.clearFilters(); }); this.container.key(['1'], () => { this.quickFilterByStatus('live'); }); this.container.key(['2'], () => { this.quickFilterByStatus('waiting'); }); this.container.key(['3'], () => { this.quickFilterByStatus('end'); }); this.container.key(['4'], () => { this.quickFilterByStatus('banpush'); }); this.container.key(['r', 'R', 'f5'], () => { this.requestUpdate(); }); this.container.key(['a'], () => { this.selectAll(); }); this.container.key(['A'], () => { this.deselectAll(); }); this.container.key(['d'], () => { this.showBatchOperations(); }); this.container.key(['g'], () => { this.moveToTop(); }); this.container.key(['G'], () => { this.moveToBottom(); }); this.container.key(['ctrl+f'], () => { this.showQuickJumpDialog(); }); this.container.key(['ctrl+g'], () => { this.showGoToLineDialog(); }); this.container.key(['shift+up', 'shift+k'], () => { this.extendSelectionUp(); }); this.container.key(['shift+down', 'shift+j'], () => { this.extendSelectionDown(); }); this.container.key(['ctrl+c'], () => { this.copySelectionInfo(); }); } render() { if (this.isDestroyed || !this.widget) return; try { this.updateHeader(); this.updateFooter(); this.updateTableDisplay(); this.widget.screen?.render(); } catch (error) { this.handleError(error instanceof Error ? error : new Error('Unknown render error')); } } update(data) { if (this.isDestroyed) return; try { let channelsArray; if (Array.isArray(data)) { channelsArray = data; } else if ('channels' in data && Array.isArray(data.channels)) { channelsArray = data.channels; } else { channelsArray = [data]; } this.channels = channelsArray.map(channel => this.convertToStatusInfo(channel)); this.applyFiltersAndSort(); this.lastUpdateTime = new Date(); this.updateState({ lastUpdate: this.lastUpdateTime }); if (this.channels.length === 0) { this.noDataBox.show(); this.table.hide(); } else { this.noDataBox.hide(); this.table.show(); } this.updateStatusColors(); } catch (error) { this.handleError(error instanceof Error ? error : new Error('Failed to update channel status')); } } convertToStatusInfo(channel) { const selectedChannels = this.selectedChannels || new Set(); return { channelId: channel.channelId.toString(), name: channel.name, status: channel.watchStatus, statusText: channel.watchStatusText, viewerCount: channel.pageView || 0, maxViewer: channel.maxViewer || 0, publisher: channel.publisher || '', startTime: channel.startTime || 0, endTime: channel.endTime || 0, createdTime: channel.createdTime || 0, selected: selectedChannels.has(channel.channelId.toString()) }; } compareValues(a, b) { if (a === undefined && b === undefined) return 0; if (a === undefined) return 1; if (b === undefined) return -1; if (typeof a === 'string' && typeof b === 'string') { return a.localeCompare(b); } if (typeof a === 'number' && typeof b === 'number') { return a - b; } return String(a).localeCompare(String(b)); } applyFiltersAndSort() { const channels = this.channels || []; this.filteredChannels = channels.filter(channel => { if (this.filterConfig.status && this.filterConfig.status.length > 0) { if (!this.filterConfig.status.includes(channel.status)) { return false; } } if (this.filterConfig.searchTerm) { const searchTerms = this.filterConfig.searchTerm.toLowerCase().split(' ').filter(term => term.length > 0); const searchableText = `${channel.name} ${channel.channelId} ${channel.publisher} ${channel.statusText}`.toLowerCase(); const allTermsMatch = searchTerms.every(term => searchableText.includes(term)); if (!allTermsMatch) { return false; } } if (this.filterConfig.viewerCountMin !== undefined && channel.viewerCount < this.filterConfig.viewerCountMin) { return false; } if (this.filterConfig.viewerCountMax !== undefined && channel.viewerCount > this.filterConfig.viewerCountMax) { return false; } if (this.filterConfig.publisherFilter) { const publisherFilter = this.filterConfig.publisherFilter.toLowerCase(); if (!channel.publisher.toLowerCase().includes(publisherFilter)) { return false; } } if (this.filterConfig.dateRange) { const channelDate = new Date(channel.createdTime); if (this.filterConfig.dateRange.startDate && channelDate < this.filterConfig.dateRange.startDate) { return false; } if (this.filterConfig.dateRange.endDate && channelDate > this.filterConfig.dateRange.endDate) { return false; } } return true; }); this.filteredChannels.sort((a, b) => { const aVal = a[this.sortConfig.field]; const bVal = b[this.sortConfig.field]; let comparison = this.compareValues(aVal, bVal); if (comparison === 0 && this.sortConfig.secondaryField) { const aSecVal = a[this.sortConfig.secondaryField]; const bSecVal = b[this.sortConfig.secondaryField]; comparison = this.compareValues(aSecVal, bSecVal); if (this.sortConfig.secondaryOrder === 'desc') { comparison = -comparison; } } return this.sortConfig.order === 'asc' ? comparison : -comparison; }); const filteredChannels = this.filteredChannels || []; if (this.currentIndex >= filteredChannels.length) { this.currentIndex = Math.max(0, filteredChannels.length - 1); } } updateTableDisplay() { if (!this.table) return; const headers = ['Channel Name', 'Status', 'Viewers', 'Max', 'Publisher', 'Created']; const data = []; const channels = this.filteredChannels || []; const selectedChannels = this.selectedChannels || new Set(); for (let i = 0; i < channels.length; i++) { const channel = channels[i]; if (!channel) continue; const isSelected = selectedChannels.has(channel.channelId); const isCurrent = i === this.currentIndex; const row = [ this.formatCellValue(channel.name, isSelected, isCurrent), this.formatStatusCell(channel.status, channel.statusText, isSelected, isCurrent), this.formatCellValue(channel.viewerCount.toString(), isSelected, isCurrent), this.formatCellValue(channel.maxViewer.toString(), isSelected, isCurrent), this.formatCellValue(channel.publisher, isSelected, isCurrent), this.formatCellValue(this.formatDate(new Date(channel.createdTime)), isSelected, isCurrent) ]; data.push(row); } this.table.setData({ headers, data }); } formatCellValue(value, isSelected, isCurrent) { if (isCurrent) { return `{inverse}${isSelected ? '✓ ' : ''}${value}{/inverse}`; } return isSelected ? `{cyan-fg}✓ ${value}{/cyan-fg}` : value; } formatStatusCell(status, statusText, isSelected, isCurrent) { const colors = this.getStatusColors(status); const safeColor = this.getTerminalSafeColor(colors.fg); const prefix = isCurrent ? '{inverse}' : ''; const suffix = isCurrent ? '{/inverse}' : ''; const checkmark = isSelected ? '✓ ' : ''; const icon = colors.icon || ''; const healthLevel = this.getStatusHealthLevel(status); let statusDisplay = statusText; switch (healthLevel) { case 'healthy': statusDisplay = `${icon} ${statusText}`; break; case 'warning': statusDisplay = `${icon} ${statusText}`; break; case 'error': statusDisplay = `${icon} ${statusText}`; break; } return `${prefix}{${safeColor}-fg}${checkmark}${statusDisplay}{/${safeColor}-fg}${suffix}`; } updateStatusColors() { this.throttledRender(); this.emit('component:statusChanged', { componentId: this.state.id, healthSummary: this.getChannelHealthSummary(), timestamp: new Date() }); } getChannelHealthSummary() { const channels = this.filteredChannels || []; const summary = { healthy: 0, warning: 0, error: 0 }; channels.forEach(channel => { const healthLevel = this.getStatusHealthLevel(channel.status); summary[healthLevel]++; }); return summary; } getStatusColors(status) { switch (status) { case 'live': return { fg: 'green', icon: '●' }; case 'waiting': return { fg: 'yellow', icon: '◯' }; case 'end': return { fg: 'gray', icon: '◦' }; case 'unStart': return { fg: 'gray', icon: '◦' }; case 'banpush': return { fg: 'red', icon: '✖' }; case 'playback': return { fg: 'blue', icon: '▶' }; default: return { fg: 'white', icon: '?' }; } } getTerminalSafeColor(color) { const config = this.getConfig(); if (!config.showColors) { return 'white'; } const colorMapping = { 'green': 'green', 'yellow': 'yellow', 'red': 'red', 'blue': 'blue', 'gray': 'gray', 'white': 'white' }; return colorMapping[color] || 'white'; } getStatusHealthLevel(status) { switch (status) { case 'live': return 'healthy'; case 'waiting': case 'playback': return 'warning'; case 'banpush': return 'error'; case 'end': case 'unStart': default: return 'warning'; } } formatDate(date) { return date.toLocaleDateString(); } updateHeader() { this.headerBox.setContent(this.getHeaderContent()); } updateFooter() { this.footerBox.setContent(this.getFooterContent()); } getHeaderContent() { const total = this.channels?.length || 0; const filtered = this.filteredChannels?.length || 0; const selected = this.selectedChannels?.size || 0; const lastUpdate = this.lastUpdateTime?.toLocaleTimeString() || 'Never'; const health = this.getChannelHealthSummary(); const healthDisplay = `{green-fg}●${health.healthy}{/green-fg} {yellow-fg}◯${health.warning}{/yellow-fg} {red-fg}✖${health.error}{/red-fg}`; return `Total: {bold}${total}{/bold} | Displayed: {bold}${filtered}{/bold} | Selected: {cyan-fg}${selected}{/cyan-fg} | Health: ${healthDisplay} | Updated: {gray-fg}${lastUpdate}{/gray-fg}`; } getFooterContent() { const activeFilters = this.filterConfig ? Object.keys(this.filterConfig).length : 0; const filterInfo = activeFilters > 0 ? `{yellow-fg}[${activeFilters} filters]{/yellow-fg}` : ''; const sortInfo = this.sortConfig?.secondaryField ? `{cyan-fg}[Multi-sort]{/cyan-fg}` : ''; return `{gray-fg}↑↓: Navigate | Space: Select | Enter: Details | S: Sort | Shift+S: Order | F: Filter | Shift+F: Advanced | 1-4: Quick Filter | C: Clear | R: Refresh{/gray-fg} ${filterInfo} ${sortInfo}`; } getNoDataMessage() { return '{center}{gray-fg}No channel data available{/gray-fg}\n{center}{gray-fg}Waiting for channel information...{/gray-fg}{/center}'; } moveSelection(delta) { const channels = this.filteredChannels || []; if (channels.length === 0) return; this.currentIndex = Math.max(0, Math.min(channels.length - 1, this.currentIndex + delta)); this.throttledRender(); } moveToTop() { this.saveNavigationHistory(); this.currentIndex = 0; this.isRangeSelecting = false; this.throttledRender(); this.emitNavigationEvent('moveToTop'); } moveToBottom() { const channels = this.filteredChannels || []; this.saveNavigationHistory(); this.currentIndex = Math.max(0, channels.length - 1); this.isRangeSelecting = false; this.throttledRender(); this.emitNavigationEvent('moveToBottom'); } showQuickJumpDialog() { this.emit('component:quickJumpDialog', { componentId: this.state.id, currentIndex: this.currentIndex, totalItems: this.filteredChannels?.length || 0, timestamp: new Date() }); } showGoToLineDialog() { this.emit('component:goToLineDialog', { componentId: this.state.id, currentLine: this.currentIndex + 1, totalLines: this.filteredChannels?.length || 0, timestamp: new Date() }); } extendSelectionUp() { const channels = this.filteredChannels || []; if (channels.length === 0) return; if (!this.isRangeSelecting) { this.selectionStartIndex = this.currentIndex; this.isRangeSelecting = true; } const newIndex = Math.max(0, this.currentIndex - 1); this.updateRangeSelection(newIndex); } extendSelectionDown() { const channels = this.filteredChannels || []; if (channels.length === 0) return; if (!this.isRangeSelecting) { this.selectionStartIndex = this.currentIndex; this.isRangeSelecting = true; } const newIndex = Math.min(channels.length - 1, this.currentIndex + 1); this.updateRangeSelection(newIndex); } updateRangeSelection(newIndex) { const channels = this.filteredChannels || []; const selectedChannels = this.selectedChannels || new Set(); if (this.selectionStartIndex === -1) return; this.clearRangeSelection(); const startIndex = Math.min(this.selectionStartIndex, newIndex); const endIndex = Math.max(this.selectionStartIndex, newIndex); for (let i = startIndex; i <= endIndex; i++) { const channel = channels[i]; if (channel) { selectedChannels.add(channel.channelId); } } this.currentIndex = newIndex; this.throttledRender(); } clearRangeSelection() { } copySelectionInfo() { const selectedChannels = this.selectedChannels || new Set(); const channels = this.filteredChannels || []; if (selectedChannels.size === 0 && this.currentIndex < channels.length) { const currentChannel = channels[this.currentIndex]; if (currentChannel) { this.copyChannelInfo(currentChannel); } } else { const selectedChannelData = channels.filter(c => selectedChannels.has(c.channelId)); this.copyMultipleChannelsInfo(selectedChannelData); } } copyChannelInfo(channel) { const info = `${channel.name} (${channel.channelId}) - ${channel.statusText} - ${channel.viewerCount} viewers`; this.emit('component:copyToClipboard', { componentId: this.state.id, content: info, type: 'singleChannel', channel, timestamp: new Date() }); } copyMultipleChannelsInfo(channels) { const info = channels.map(c => `${c.name} (${c.channelId}) - ${c.statusText} - ${c.viewerCount} viewers`).join('\n'); this.emit('component:copyToClipboard', { componentId: this.state.id, content: info, type: 'multipleChannels', channels, count: channels.length, timestamp: new Date() }); } saveNavigationHistory() { if (this.currentIndex !== this.lastJumpPosition) { this.navigationHistory.push(this.lastJumpPosition); if (this.navigationHistory.length > 10) { this.navigationHistory.shift(); } this.lastJumpPosition = this.currentIndex; } } emitNavigationEvent(action) { this.emit('component:navigation', { componentId: this.state.id, action, currentIndex: this.currentIndex, totalItems: this.filteredChannels?.length || 0, timestamp: new Date() }); } jumpToLine(lineNumber) { const channels = this.filteredChannels || []; const targetIndex = lineNumber - 1; if (targetIndex >= 0 && targetIndex < channels.length) { this.saveNavigationHistory(); this.currentIndex = targetIndex; this.isRangeSelecting = false; this.throttledRender(); this.emitNavigationEvent('jumpToLine'); return true; } return false; } jumpToChannel(searchTerm) { const channels = this.filteredChannels || []; const lowerSearchTerm = searchTerm.toLowerCase(); const targetIndex = channels.findIndex(channel => channel.channelId.toLowerCase().includes(lowerSearchTerm) || channel.name.toLowerCase().includes(lowerSearchTerm)); if (targetIndex !== -1) { this.saveNavigationHistory(); this.currentIndex = targetIndex; this.isRangeSelecting = false; this.throttledRender(); this.emitNavigationEvent('jumpToChannel'); return true; } return false; } toggleSelection() { const channels = this.filteredChannels || []; const selectedChannels = this.selectedChannels || new Set(); if (channels.length === 0) return; const currentChannel = channels[this.currentIndex]; if (!currentChannel) return; if (selectedChannels.has(currentChannel.channelId)) { selectedChannels.delete(currentChannel.channelId); } else { selectedChannels.add(currentChannel.channelId); } this.throttledRender(); } selectAll() { const channels = this.filteredChannels || []; const selectedChannels = this.selectedChannels || new Set(); channels.forEach(channel => { selectedChannels.add(channel.channelId); }); this.throttledRender(); } deselectAll() { const selectedChannels = this.selectedChannels || new Set(); selectedChannels.clear(); this.throttledRender(); } cycleSortField() { const fields = ['name', 'status', 'viewerCount', 'maxViewer', 'publisher', 'createdTime']; const currentIndex = fields.indexOf(this.sortConfig.field); const nextIndex = (currentIndex + 1) % fields.length; const nextField = fields[nextIndex]; if (nextField) { if (this.sortConfig.secondaryField) { this.sortConfig.field = this.sortConfig.secondaryField; this.sortConfig.order = this.sortConfig.secondaryOrder || 'asc'; delete this.sortConfig.secondaryField; delete this.sortConfig.secondaryOrder; } else { this.sortConfig.field = nextField; } } this.applyFiltersAndSort(); this.throttledRender(); this.emit('component:sortChanged', { componentId: this.state.id, sortConfig: this.sortConfig, timestamp: new Date() }); } toggleSortOrder() { this.sortConfig.order = this.sortConfig.order === 'asc' ? 'desc' : 'asc'; this.applyFiltersAndSort(); this.throttledRender(); this.emit('component:sortChanged', { componentId: this.state.id, sortConfig: this.sortConfig, timestamp: new Date() }); } setMultiFieldSort(primary, primaryOrder, secondary, secondaryOrder) { const newConfig = { field: primary, order: primaryOrder }; if (secondary !== undefined) { newConfig.secondaryField = secondary; } if (secondaryOrder !== undefined) { newConfig.secondaryOrder = secondaryOrder; } this.sortConfig = newConfig; this.applyFiltersAndSort(); this.throttledRender(); this.emit('component:sortChanged', { componentId: this.state.id, sortConfig: this.sortConfig, changeType: 'multiField', timestamp: new Date() }); } showFilterDialog() { this.emit('component:filterDialog', { componentId: this.state.id, currentFilter: this.filterConfig, type: 'basic', timestamp: new Date() }); } showAdvancedFilterDialog() { this.emit('component:advancedFilterDialog', { componentId: this.state.id, currentFilter: this.filterConfig, availableFields: ['viewerCount', 'publisher', 'createdTime'], timestamp: new Date() }); } quickFilterByStatus(status) { if (this.filterConfig.status?.includes(status)) { this.filterConfig.status = this.filterConfig.status.filter(s => s !== status); if (this.filterConfig.status.length === 0) { delete this.filterConfig.status; } } else { if (!this.filterConfig.status) { this.filterConfig.status = []; } this.filterConfig.status.push(status); } this.applyFiltersAndSort(); this.throttledRender(); this.emit('component:filterChanged', { componentId: this.state.id, filter: this.filterConfig, changeType: 'quickFilter', status, timestamp: new Date() }); } clearFilters() { const hadFilters = Object.keys(this.filterConfig).length > 0; this.filterConfig = {}; this.applyFiltersAndSort(); this.throttledRender(); if (hadFilters) { this.emit('component:filtersCleared', { componentId: this.state.id, timestamp: new Date() }); } } setAdvancedFilter(filter) { this.filterConfig = { ...this.filterConfig, ...filter }; this.applyFiltersAndSort(); this.throttledRender(); this.emit('component:filterChanged', { componentId: this.state.id, filter: this.filterConfig, changeType: 'advanced', timestamp: new Date() }); } showChannelDetails() { const channels = this.filteredChannels || []; if (channels.length === 0) return; const currentChannel = channels[this.currentIndex]; if (!currentChannel) return; this.detailsPopup.show(currentChannel); this.emit('component:showDetails', { componentId: this.state.id, channelId: currentChannel.channelId, channel: currentChannel, timestamp: new Date() }); } showBatchOperations() { const selectedChannels = this.selectedChannels || new Set(); if (selectedChannels.size === 0) return; this.batchDialog.show(Array.from(selectedChannels)); this.emit('component:batchOperations', { componentId: this.state.id, selectedChannels: Array.from(selectedChannels), timestamp: new Date() }); } getConfig() { return this.config; } getSelectedChannels() { return Array.from(this.selectedChannels || new Set()); } getCurrentChannel() { const channels = this.filteredChannels || []; return channels[this.currentIndex] || null; } setFilter(filter) { this.filterConfig = filter; this.applyFiltersAndSort(); this.throttledRender(); } setSort(sort) { this.sortConfig = sort; this.applyFiltersAndSort(); this.throttledRender(); } destroy() { this.stopRefresh(); this.channels = []; this.filteredChannels = []; if (this.selectedChannels) { this.selectedChannels.clear(); } if (this.detailsPopup) { this.detailsPopup.destroy(); } if (this.batchDialog) { this.batchDialog.destroy(); } super.destroy(); } } exports.ChannelStatusPanel = ChannelStatusPanel; //# sourceMappingURL=channel-status.panel.js.map