Channel Management, Drag & Drop
This commit is contained in:
+501
-523
File diff suppressed because it is too large
Load Diff
@@ -7,3 +7,96 @@
|
|||||||
@import url("./timeline.css");
|
@import url("./timeline.css");
|
||||||
@import url("./modal.css");
|
@import url("./modal.css");
|
||||||
@import url("./search.css");
|
@import url("./search.css");
|
||||||
|
|
||||||
|
.channels-sticky-header {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 20;
|
||||||
|
background: var(--bg);
|
||||||
|
padding-top: 0.75rem;
|
||||||
|
padding-bottom: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.channel-toolbar {
|
||||||
|
position: sticky;
|
||||||
|
top: 5.5rem;
|
||||||
|
z-index: 19;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.75rem;
|
||||||
|
align-items: center;
|
||||||
|
background: var(--bg);
|
||||||
|
padding: 0.75rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.channel-toolbar input[type="search"] {
|
||||||
|
min-width: 260px;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.channel-filter-buttons,
|
||||||
|
.channel-actions {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.channel-filter-buttons button.active {
|
||||||
|
background: var(--accent);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.channel-count {
|
||||||
|
margin: 0.75rem 0 1rem;
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.channel-settings-card.is-changed {
|
||||||
|
border-color: var(--accent);
|
||||||
|
box-shadow: 0 0 0 1px var(--accent-soft);
|
||||||
|
}
|
||||||
|
|
||||||
|
.number-input.has-duplicate {
|
||||||
|
border-color: #ef4444;
|
||||||
|
background: rgba(239, 68, 68, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.duplicate-warning {
|
||||||
|
margin-left: 1rem;
|
||||||
|
color: #f87171;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 800px) {
|
||||||
|
.channels-sticky-header,
|
||||||
|
.channel-toolbar {
|
||||||
|
position: static;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.drag-handle {
|
||||||
|
cursor: grab;
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 1.2rem;
|
||||||
|
padding-right: 0.75rem;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.channel-settings-main {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.channel-settings-card.is-dragging {
|
||||||
|
opacity: 0.45;
|
||||||
|
border-color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.drag-handle {
|
||||||
|
cursor: grab;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drag-handle:active {
|
||||||
|
cursor: grabbing;
|
||||||
|
}
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ class HomeController
|
|||||||
'programmes' => $programmes,
|
'programmes' => $programmes,
|
||||||
'contentTemplate' => $contentTemplate,
|
'contentTemplate' => $contentTemplate,
|
||||||
'view' => $view,
|
'view' => $view,
|
||||||
'channelConfig' => $this->config->getChannels(),
|
'channelConfig' => $this->channelService->getSortedChannels($this->config),
|
||||||
'saved' => isset($get['saved']),
|
'saved' => isset($get['saved']),
|
||||||
'currentProgrammes' => $currentProgrammes,
|
'currentProgrammes' => $currentProgrammes,
|
||||||
'selectedDate' => $selectedDate,
|
'selectedDate' => $selectedDate,
|
||||||
|
|||||||
@@ -4,34 +4,8 @@ class ChannelService
|
|||||||
{
|
{
|
||||||
public function synchronizeChannels(array $cachedChannels, Config $config): void
|
public function synchronizeChannels(array $cachedChannels, Config $config): void
|
||||||
{
|
{
|
||||||
$storedChannels = $config->getChannels();
|
// Alt-Methode bleibt absichtlich harmlos.
|
||||||
|
// Neue Sender werden jetzt vom SenderSyncService im XmlTvCacheService ergänzt.
|
||||||
$maxNumber = 0;
|
|
||||||
foreach ($storedChannels as $stored) {
|
|
||||||
$maxNumber = max($maxNumber, (int)($stored['number'] ?? 0));
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach ($cachedChannels as $id => $channel) {
|
|
||||||
if (!isset($storedChannels[$id])) {
|
|
||||||
$maxNumber++;
|
|
||||||
|
|
||||||
$storedChannels[$id] = [
|
|
||||||
'uuid' => bin2hex(random_bytes(16)),
|
|
||||||
'name' => $channel['name'] ?? $id,
|
|
||||||
'icon' => $channel['icon'] ?? '',
|
|
||||||
'country' => '',
|
|
||||||
'language' => '',
|
|
||||||
'enabled' => true,
|
|
||||||
'favorite' => false,
|
|
||||||
'number' => $maxNumber,
|
|
||||||
'group' => 'Unknown',
|
|
||||||
];
|
|
||||||
} else {
|
|
||||||
$storedChannels[$id]['name'] = $channel['name'] ?? $storedChannels[$id]['name'];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$config->saveChannels($this->sortChannels($storedChannels));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function saveChannelsFromPost(Config $config, array $post): void
|
public function saveChannelsFromPost(Config $config, array $post): void
|
||||||
@@ -48,12 +22,26 @@ class ChannelService
|
|||||||
unset($channel);
|
unset($channel);
|
||||||
|
|
||||||
$config->saveChannels($this->sortChannels($savedChannels));
|
$config->saveChannels($this->sortChannels($savedChannels));
|
||||||
|
|
||||||
|
@unlink('/var/www/cache/guide.json');
|
||||||
|
@unlink('/var/www/cache/guide.version');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getSortedChannels(Config $config): array
|
||||||
|
{
|
||||||
|
return $this->sortChannels($config->getChannels());
|
||||||
}
|
}
|
||||||
|
|
||||||
private function sortChannels(array $channels): array
|
private function sortChannels(array $channels): array
|
||||||
{
|
{
|
||||||
uasort($channels, function ($a, $b) {
|
uasort($channels, function ($a, $b) {
|
||||||
return ((int)($a['number'] ?? 999999)) <=> ((int)($b['number'] ?? 999999));
|
return [
|
||||||
|
(int)($a['number'] ?? 999999),
|
||||||
|
strtolower($a['name'] ?? '')
|
||||||
|
] <=> [
|
||||||
|
(int)($b['number'] ?? 999999),
|
||||||
|
strtolower($b['name'] ?? '')
|
||||||
|
];
|
||||||
});
|
});
|
||||||
|
|
||||||
return $channels;
|
return $channels;
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
class SenderSyncService
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private string $channelsJsonPath
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function sync(array $xmltvChannels): array
|
||||||
|
{
|
||||||
|
$existing = [];
|
||||||
|
|
||||||
|
if (is_file($this->channelsJsonPath)) {
|
||||||
|
$existing = json_decode(file_get_contents($this->channelsJsonPath), true) ?: [];
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($xmltvChannels as $channel) {
|
||||||
|
$id = $channel['id'] ?? null;
|
||||||
|
|
||||||
|
if (!$id) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($existing[$id])) {
|
||||||
|
continue; // niemals überschreiben
|
||||||
|
}
|
||||||
|
|
||||||
|
$existing[$id] = [
|
||||||
|
'id' => $id,
|
||||||
|
'name' => $channel['name'] ?? $id,
|
||||||
|
'enabled' => false,
|
||||||
|
'favorite' => false,
|
||||||
|
'number' => 999999,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
uasort($existing, function ($a, $b) {
|
||||||
|
return [$a['number'], $a['name']] <=> [$b['number'], $b['name']];
|
||||||
|
});
|
||||||
|
|
||||||
|
file_put_contents(
|
||||||
|
$this->channelsJsonPath,
|
||||||
|
json_encode($existing, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)
|
||||||
|
);
|
||||||
|
|
||||||
|
return $existing;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,19 +1,24 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
require_once __DIR__ . '/SenderSyncService.php';
|
||||||
|
|
||||||
class XmlTvCacheService
|
class XmlTvCacheService
|
||||||
{
|
{
|
||||||
private string $file;
|
private string $file;
|
||||||
private string $cacheFile;
|
private string $cacheFile;
|
||||||
private string $timezone;
|
private string $timezone;
|
||||||
|
private string $channelsFile;
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
string $file,
|
string $file,
|
||||||
string $timezone = 'Europe/Vienna',
|
string $timezone = 'Europe/Vienna',
|
||||||
string $cacheFile = '/var/www/cache/guide.json'
|
string $cacheFile = '/var/www/cache/guide.json',
|
||||||
|
string $channelsFile = '/var/www/config/channels.json'
|
||||||
) {
|
) {
|
||||||
$this->file = $file;
|
$this->file = $file;
|
||||||
$this->timezone = $timezone;
|
$this->timezone = $timezone;
|
||||||
$this->cacheFile = $cacheFile;
|
$this->cacheFile = $cacheFile;
|
||||||
|
$this->channelsFile = $channelsFile;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getCachedData(): array
|
public function getCachedData(): array
|
||||||
@@ -54,15 +59,41 @@ class XmlTvCacheService
|
|||||||
$xml = simplexml_load_file($this->file);
|
$xml = simplexml_load_file($this->file);
|
||||||
$metadataParser = new MetadataParser();
|
$metadataParser = new MetadataParser();
|
||||||
|
|
||||||
|
$xmltvChannels = [];
|
||||||
|
|
||||||
foreach ($xml->channel as $channel) {
|
foreach ($xml->channel as $channel) {
|
||||||
$id = (string)$channel['id'];
|
$id = (string)$channel['id'];
|
||||||
|
|
||||||
$data['channels'][$id] = [
|
$xmltvChannels[] = [
|
||||||
'id' => $id,
|
'id' => $id,
|
||||||
'name' => (string)$channel->{'display-name'},
|
'name' => (string)$channel->{'display-name'},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$senderSync = new SenderSyncService($this->channelsFile);
|
||||||
|
$channelsConfig = $senderSync->sync($xmltvChannels);
|
||||||
|
|
||||||
|
$enabledChannelIds = [];
|
||||||
|
|
||||||
|
foreach ($channelsConfig as $id => $channel) {
|
||||||
|
if (!empty($channel['enabled'])) {
|
||||||
|
$enabledChannelIds[$id] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($xml->channel as $channel) {
|
||||||
|
$id = (string)$channel['id'];
|
||||||
|
|
||||||
|
if (!isset($enabledChannelIds[$id])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$data['channels'][$id] = [
|
||||||
|
'id' => $id,
|
||||||
|
'name' => $channelsConfig[$id]['name'] ?? (string)$channel->{'display-name'},
|
||||||
'icon' => isset($channel->icon) ? (string)$channel->icon['src'] : '',
|
'icon' => isset($channel->icon) ? (string)$channel->icon['src'] : '',
|
||||||
|
'number' => $channelsConfig[$id]['number'] ?? 999999,
|
||||||
|
'favorite' => !empty($channelsConfig[$id]['favorite']),
|
||||||
'programmes' => [],
|
'programmes' => [],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
@@ -70,6 +101,10 @@ class XmlTvCacheService
|
|||||||
foreach ($xml->programme as $programme) {
|
foreach ($xml->programme as $programme) {
|
||||||
$channelId = (string)$programme['channel'];
|
$channelId = (string)$programme['channel'];
|
||||||
|
|
||||||
|
if (!isset($enabledChannelIds[$channelId])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if (!isset($data['channels'][$channelId])) {
|
if (!isset($data['channels'][$channelId])) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -82,18 +117,18 @@ class XmlTvCacheService
|
|||||||
}
|
}
|
||||||
|
|
||||||
$item = [
|
$item = [
|
||||||
'title' => (string)$programme->title,
|
'title' => (string)$programme->title,
|
||||||
'subtitle' => (string)$programme->{'sub-title'},
|
'subtitle' => (string)$programme->{'sub-title'},
|
||||||
'description' => (string)$programme->desc,
|
'description' => (string)$programme->desc,
|
||||||
'category' => (string)$programme->category,
|
'category' => (string)$programme->category,
|
||||||
'start' => $start->getTimestamp(),
|
'start' => $start->getTimestamp(),
|
||||||
'stop' => $stop->getTimestamp(),
|
'stop' => $stop->getTimestamp(),
|
||||||
'duration' => $stop->getTimestamp() - $start->getTimestamp(),
|
'duration' => $stop->getTimestamp() - $start->getTimestamp(),
|
||||||
'year' => (string)$programme->date,
|
'year' => (string)$programme->date,
|
||||||
'rating' => isset($programme->rating->value) ? (string)$programme->rating->value : '',
|
'rating' => isset($programme->rating->value) ? (string)$programme->rating->value : '',
|
||||||
'episode' => isset($programme->{'episode-num'}) ? (string)$programme->{'episode-num'} : '',
|
'episode' => isset($programme->{'episode-num'}) ? (string)$programme->{'episode-num'} : '',
|
||||||
'icon' => isset($programme->icon) ? (string)$programme->icon['src'] : '',
|
'icon' => isset($programme->icon) ? (string)$programme->icon['src'] : '',
|
||||||
'country' => (string)$programme->country,
|
'country' => (string)$programme->country,
|
||||||
];
|
];
|
||||||
|
|
||||||
$item['metadata'] = $metadataParser->parse($item)->toArray();
|
$item['metadata'] = $metadataParser->parse($item)->toArray();
|
||||||
@@ -102,13 +137,15 @@ class XmlTvCacheService
|
|||||||
}
|
}
|
||||||
|
|
||||||
foreach ($data['channels'] as &$channel) {
|
foreach ($data['channels'] as &$channel) {
|
||||||
usort($channel['programmes'], function ($a, $b) {
|
usort($channel['programmes'], fn($a, $b) => $a['start'] <=> $b['start']);
|
||||||
return $a['start'] <=> $b['start'];
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
unset($channel);
|
unset($channel);
|
||||||
|
|
||||||
|
uasort($data['channels'], function ($a, $b) {
|
||||||
|
return [$a['number'], $a['name']] <=> [$b['number'], $b['name']];
|
||||||
|
});
|
||||||
|
|
||||||
$this->writeCache($data);
|
$this->writeCache($data);
|
||||||
return $data;
|
return $data;
|
||||||
}
|
}
|
||||||
|
|||||||
+258
-9
@@ -1,9 +1,9 @@
|
|||||||
<?php if ($saved): ?>
|
<?php if ($saved): ?>
|
||||||
<div class="notice">Sender gespeichert.</div>
|
<div class="notice">Sender gespeichert. EPG wird beim nächsten Laden neu aufgebaut.</div>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
|
||||||
<form method="post" class="settings-form">
|
<form method="post" class="settings-form" id="channelsForm">
|
||||||
<div class="settings-header">
|
<div class="settings-header channels-sticky-header">
|
||||||
<div>
|
<div>
|
||||||
<h2>Senderverwaltung</h2>
|
<h2>Senderverwaltung</h2>
|
||||||
<p class="settings-subtitle">Aktiviere Sender, markiere Favoriten und ändere die Reihenfolge.</p>
|
<p class="settings-subtitle">Aktiviere Sender, markiere Favoriten und ändere die Reihenfolge.</p>
|
||||||
@@ -11,13 +11,54 @@
|
|||||||
<button type="submit">Speichern</button>
|
<button type="submit">Speichern</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="channel-toolbar">
|
||||||
|
<input type="search" id="channelSearch" placeholder="Sender suchen...">
|
||||||
|
|
||||||
|
<div class="channel-filter-buttons">
|
||||||
|
<button type="button" data-filter="all" class="active">Alle</button>
|
||||||
|
<button type="button" data-filter="enabled">Aktiv</button>
|
||||||
|
<button type="button" data-filter="disabled">Deaktiviert</button>
|
||||||
|
<button type="button" data-filter="favorite">Favoriten</button>
|
||||||
|
<button type="button" data-filter="changed">Geändert</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="channel-actions">
|
||||||
|
<button type="button" id="enableVisible">Sichtbare aktivieren</button>
|
||||||
|
<button type="button" id="disableVisible">Sichtbare deaktivieren</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="channel-count">
|
||||||
|
<span id="visibleCount">0</span> von <?= count($channelConfig) ?> Sendern sichtbar
|
||||||
|
<span id="duplicateWarning" class="duplicate-warning" hidden>⚠ Doppelte Sendernummern</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="channel-settings-list">
|
<div class="channel-settings-list">
|
||||||
<?php foreach ($channelConfig as $id => $channel): ?>
|
<?php foreach ($channelConfig as $id => $channel): ?>
|
||||||
<article class="channel-settings-card">
|
<?php
|
||||||
|
$enabled = !empty($channel['enabled']);
|
||||||
|
$favorite = !empty($channel['favorite']);
|
||||||
|
$name = $channel['name'] ?? $id;
|
||||||
|
$number = (int)($channel['number'] ?? 999999);
|
||||||
|
$group = $channel['group'] ?? 'Unknown';
|
||||||
|
?>
|
||||||
|
|
||||||
|
<article
|
||||||
|
class="channel-settings-card"
|
||||||
|
data-name="<?= htmlspecialchars(strtolower($name . ' ' . $id)) ?>"
|
||||||
|
data-enabled="<?= $enabled ? '1' : '0' ?>"
|
||||||
|
data-favorite="<?= $favorite ? '1' : '0' ?>"
|
||||||
|
data-original-enabled="<?= $enabled ? '1' : '0' ?>"
|
||||||
|
data-original-favorite="<?= $favorite ? '1' : '0' ?>"
|
||||||
|
data-original-number="<?= $number ?>"
|
||||||
|
data-original-group="<?= htmlspecialchars($group) ?>"
|
||||||
|
data-changed="0"
|
||||||
|
>
|
||||||
<div class="channel-settings-main">
|
<div class="channel-settings-main">
|
||||||
|
<div class="drag-handle" draggable="true" title="Zum Sortieren ziehen">☰</div>
|
||||||
<label class="switch-row">
|
<label class="switch-row">
|
||||||
<input type="checkbox" name="enabled[<?= htmlspecialchars($id) ?>]" <?= !empty($channel['enabled']) ? 'checked' : '' ?>>
|
<input class="enabled-checkbox" type="checkbox" name="enabled[<?= htmlspecialchars($id) ?>]" <?= $enabled ? 'checked' : '' ?>>
|
||||||
<span class="channel-settings-title"><?= htmlspecialchars($channel['name'] ?? $id) ?></span>
|
<span class="channel-settings-title"><?= htmlspecialchars($name) ?></span>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<div class="channel-id"><?= htmlspecialchars($id) ?></div>
|
<div class="channel-id"><?= htmlspecialchars($id) ?></div>
|
||||||
@@ -26,16 +67,16 @@
|
|||||||
<div class="channel-settings-fields">
|
<div class="channel-settings-fields">
|
||||||
<label>
|
<label>
|
||||||
Nummer
|
Nummer
|
||||||
<input class="number-input" type="number" name="number[<?= htmlspecialchars($id) ?>]" value="<?= (int)($channel['number'] ?? 999999) ?>">
|
<input class="number-input" type="number" name="number[<?= htmlspecialchars($id) ?>]" value="<?= $number ?>">
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<label>
|
<label>
|
||||||
Gruppe
|
Gruppe
|
||||||
<input type="text" name="group[<?= htmlspecialchars($id) ?>]" value="<?= htmlspecialchars($channel['group'] ?? 'Unknown') ?>">
|
<input class="group-input" type="text" name="group[<?= htmlspecialchars($id) ?>]" value="<?= htmlspecialchars($group) ?>">
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<label class="favorite-toggle">
|
<label class="favorite-toggle">
|
||||||
<input type="checkbox" name="favorite[<?= htmlspecialchars($id) ?>]" <?= !empty($channel['favorite']) ? 'checked' : '' ?>>
|
<input class="favorite-checkbox" type="checkbox" name="favorite[<?= htmlspecialchars($id) ?>]" <?= $favorite ? 'checked' : '' ?>>
|
||||||
⭐ Favorit
|
⭐ Favorit
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
@@ -43,3 +84,211 @@
|
|||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
(function () {
|
||||||
|
const search = document.getElementById('channelSearch');
|
||||||
|
const list = document.querySelector('.channel-settings-list');
|
||||||
|
const cards = Array.from(document.querySelectorAll('.channel-settings-card'));
|
||||||
|
const visibleCount = document.getElementById('visibleCount');
|
||||||
|
const duplicateWarning = document.getElementById('duplicateWarning');
|
||||||
|
const filterButtons = Array.from(document.querySelectorAll('[data-filter]'));
|
||||||
|
const enableVisible = document.getElementById('enableVisible');
|
||||||
|
const disableVisible = document.getElementById('disableVisible');
|
||||||
|
|
||||||
|
let currentFilter = 'all';
|
||||||
|
let draggedCard = null;
|
||||||
|
|
||||||
|
function refreshCardState(card) {
|
||||||
|
const enabled = card.querySelector('.enabled-checkbox').checked;
|
||||||
|
const favorite = card.querySelector('.favorite-checkbox').checked;
|
||||||
|
const number = card.querySelector('.number-input').value;
|
||||||
|
const group = card.querySelector('.group-input').value;
|
||||||
|
|
||||||
|
card.dataset.enabled = enabled ? '1' : '0';
|
||||||
|
card.dataset.favorite = favorite ? '1' : '0';
|
||||||
|
|
||||||
|
const changed =
|
||||||
|
card.dataset.enabled !== card.dataset.originalEnabled ||
|
||||||
|
card.dataset.favorite !== card.dataset.originalFavorite ||
|
||||||
|
String(number) !== String(card.dataset.originalNumber) ||
|
||||||
|
String(group) !== String(card.dataset.originalGroup);
|
||||||
|
|
||||||
|
card.dataset.changed = changed ? '1' : '0';
|
||||||
|
card.classList.toggle('is-changed', changed);
|
||||||
|
}
|
||||||
|
|
||||||
|
function markDuplicateNumbers() {
|
||||||
|
const numbers = new Map();
|
||||||
|
|
||||||
|
cards.forEach(card => {
|
||||||
|
const input = card.querySelector('.number-input');
|
||||||
|
input.classList.remove('has-duplicate');
|
||||||
|
|
||||||
|
const number = input.value.trim();
|
||||||
|
if (!number || number === '999999') return;
|
||||||
|
|
||||||
|
if (!numbers.has(number)) numbers.set(number, []);
|
||||||
|
numbers.get(number).push(input);
|
||||||
|
});
|
||||||
|
|
||||||
|
let hasDuplicates = false;
|
||||||
|
|
||||||
|
numbers.forEach(inputs => {
|
||||||
|
if (inputs.length > 1) {
|
||||||
|
hasDuplicates = true;
|
||||||
|
inputs.forEach(input => input.classList.add('has-duplicate'));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
duplicateWarning.hidden = !hasDuplicates;
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyFilter() {
|
||||||
|
const q = search.value.trim().toLowerCase();
|
||||||
|
let count = 0;
|
||||||
|
|
||||||
|
cards.forEach(card => {
|
||||||
|
refreshCardState(card);
|
||||||
|
|
||||||
|
const matchesSearch = !q || card.dataset.name.includes(q);
|
||||||
|
const matchesFilter =
|
||||||
|
currentFilter === 'all' ||
|
||||||
|
(currentFilter === 'enabled' && card.dataset.enabled === '1') ||
|
||||||
|
(currentFilter === 'disabled' && card.dataset.enabled === '0') ||
|
||||||
|
(currentFilter === 'favorite' && card.dataset.favorite === '1') ||
|
||||||
|
(currentFilter === 'changed' && card.dataset.changed === '1');
|
||||||
|
|
||||||
|
const visible = matchesSearch && matchesFilter;
|
||||||
|
card.style.display = visible ? '' : 'none';
|
||||||
|
|
||||||
|
if (visible) count++;
|
||||||
|
});
|
||||||
|
|
||||||
|
visibleCount.textContent = count;
|
||||||
|
markDuplicateNumbers();
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDragAfterElement(container, y) {
|
||||||
|
const visibleCards = Array.from(container.querySelectorAll('.channel-settings-card:not(.is-dragging)'))
|
||||||
|
.filter(card => card.style.display !== 'none');
|
||||||
|
|
||||||
|
let closest = null;
|
||||||
|
let closestOffset = Number.NEGATIVE_INFINITY;
|
||||||
|
|
||||||
|
visibleCards.forEach(card => {
|
||||||
|
const box = card.getBoundingClientRect();
|
||||||
|
const offset = y - box.top - box.height / 2;
|
||||||
|
|
||||||
|
if (offset < 0 && offset > closestOffset) {
|
||||||
|
closestOffset = offset;
|
||||||
|
closest = card;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return closest;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renumberVisible() {
|
||||||
|
let number = 10;
|
||||||
|
|
||||||
|
Array.from(list.querySelectorAll('.channel-settings-card'))
|
||||||
|
.filter(card => card.style.display !== 'none')
|
||||||
|
.forEach(card => {
|
||||||
|
card.querySelector('.number-input').value = number;
|
||||||
|
number += 10;
|
||||||
|
refreshCardState(card);
|
||||||
|
});
|
||||||
|
|
||||||
|
markDuplicateNumbers();
|
||||||
|
}
|
||||||
|
|
||||||
|
cards.forEach(card => {
|
||||||
|
const handle = card.querySelector('.drag-handle');
|
||||||
|
|
||||||
|
handle.setAttribute('draggable', 'true');
|
||||||
|
|
||||||
|
handle.addEventListener('dragstart', event => {
|
||||||
|
draggedCard = card;
|
||||||
|
card.classList.add('is-dragging');
|
||||||
|
event.dataTransfer.effectAllowed = 'move';
|
||||||
|
event.dataTransfer.setData('text/plain', card.dataset.name || 'channel');
|
||||||
|
});
|
||||||
|
|
||||||
|
handle.addEventListener('dragend', () => {
|
||||||
|
if (draggedCard) {
|
||||||
|
draggedCard.classList.remove('is-dragging');
|
||||||
|
}
|
||||||
|
|
||||||
|
draggedCard = null;
|
||||||
|
renumberVisible();
|
||||||
|
applyFilter();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
list.addEventListener('dragover', event => {
|
||||||
|
if (!draggedCard) return;
|
||||||
|
|
||||||
|
event.preventDefault();
|
||||||
|
event.dataTransfer.dropEffect = 'move';
|
||||||
|
|
||||||
|
const afterElement = getDragAfterElement(list, event.clientY);
|
||||||
|
|
||||||
|
if (afterElement == null) {
|
||||||
|
list.appendChild(draggedCard);
|
||||||
|
} else {
|
||||||
|
list.insertBefore(draggedCard, afterElement);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
list.addEventListener('drop', event => {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
if (draggedCard) {
|
||||||
|
draggedCard.classList.remove('is-dragging');
|
||||||
|
}
|
||||||
|
|
||||||
|
draggedCard = null;
|
||||||
|
renumberVisible();
|
||||||
|
applyFilter();
|
||||||
|
});
|
||||||
|
|
||||||
|
filterButtons.forEach(button => {
|
||||||
|
button.addEventListener('click', () => {
|
||||||
|
filterButtons.forEach(b => b.classList.remove('active'));
|
||||||
|
button.classList.add('active');
|
||||||
|
currentFilter = button.dataset.filter;
|
||||||
|
applyFilter();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
search.addEventListener('input', applyFilter);
|
||||||
|
|
||||||
|
cards.forEach(card => {
|
||||||
|
card.querySelectorAll('input').forEach(input => {
|
||||||
|
input.addEventListener('input', applyFilter);
|
||||||
|
input.addEventListener('change', applyFilter);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
enableVisible.addEventListener('click', () => {
|
||||||
|
cards.forEach(card => {
|
||||||
|
if (card.style.display !== 'none') {
|
||||||
|
card.querySelector('.enabled-checkbox').checked = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
applyFilter();
|
||||||
|
});
|
||||||
|
|
||||||
|
disableVisible.addEventListener('click', () => {
|
||||||
|
cards.forEach(card => {
|
||||||
|
if (card.style.display !== 'none') {
|
||||||
|
card.querySelector('.enabled-checkbox').checked = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
applyFilter();
|
||||||
|
});
|
||||||
|
|
||||||
|
applyFilter();
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
|||||||
Reference in New Issue
Block a user