diff --git a/app/config/channels.json b/app/config/channels.json
new file mode 100644
index 0000000..0967ef4
--- /dev/null
+++ b/app/config/channels.json
@@ -0,0 +1 @@
+{}
diff --git a/app/config/config.json b/app/config/config.json
new file mode 100644
index 0000000..ed71cdc
--- /dev/null
+++ b/app/config/config.json
@@ -0,0 +1,8 @@
+{
+ "title": "TVGuide",
+ "timezone": "Europe/Vienna",
+ "language": "de",
+ "theme": "dark",
+ "days": 7,
+ "default_view": "today"
+}
diff --git a/app/public/assets/css/main.css b/app/public/assets/css/main.css
new file mode 100644
index 0000000..678295f
--- /dev/null
+++ b/app/public/assets/css/main.css
@@ -0,0 +1,89 @@
+body {
+ margin: 0;
+ font-family: system-ui, sans-serif;
+ background: #101114;
+ color: #eee;
+}
+
+.topbar {
+ padding: 20px;
+ background: #181a20;
+ border-bottom: 1px solid #2b2f3a;
+ display: flex;
+ align-items: center;
+ gap: 28px;
+}
+
+.topbar h1 {
+ margin: 0;
+}
+
+.topbar nav {
+ display: flex;
+ gap: 14px;
+}
+
+.topbar a {
+ color: #cfd6e6;
+ text-decoration: none;
+}
+
+main {
+ padding: 20px;
+ max-width: 1100px;
+ margin: 0 auto;
+}
+
+.channel {
+ margin-bottom: 28px;
+ background: #181a20;
+ border: 1px solid #2b2f3a;
+ border-radius: 16px;
+ overflow: hidden;
+}
+
+.channel-header {
+ padding: 14px 18px;
+ background: #20232c;
+ font-size: 1.25rem;
+ font-weight: 700;
+}
+
+.programme {
+ padding: 14px 18px;
+ border-top: 1px solid #2b2f3a;
+}
+
+.programme-time {
+ color: #9da7bd;
+ font-size: 0.95rem;
+ margin-bottom: 4px;
+}
+
+.programme-title {
+ font-size: 1.1rem;
+ font-weight: 700;
+}
+
+.programme-desc {
+ color: #c9ced8;
+ margin-top: 6px;
+}
+
+.category {
+ display: inline-block;
+ margin-top: 8px;
+ padding: 3px 8px;
+ border-radius: 999px;
+ background: #303545;
+ color: #cfd6e6;
+ font-size: 0.8rem;
+}
+
+.empty {
+ color: #aaa;
+ background: #181a20;
+ border: 1px solid #2b2f3a;
+ border-radius: 16px;
+ padding: 18px;
+}
diff --git a/app/public/assets/js/app.js b/app/public/assets/js/app.js
new file mode 100644
index 0000000..b0ed84b
--- /dev/null
+++ b/app/public/assets/js/app.js
@@ -0,0 +1 @@
+console.log('TVGuide loaded');
diff --git a/app/public/index.php b/app/public/index.php
index 91f297d..61e5910 100644
--- a/app/public/index.php
+++ b/app/public/index.php
@@ -1,183 +1,27 @@
setTimezone(new DateTimeZone('Europe/Vienna'));
- return $dt;
-}
+$xmltv = new XmlTv(
+ '/var/www/epg/guide.xml',
+ $config->get('timezone', 'Europe/Vienna')
+);
-$channels = [];
-$programmes = [];
+$xmltv = new XmlTv('/var/www/epg/guide.xml', 'Europe/Vienna');
+[$channels, $programmes] = $xmltv->load();
-if (file_exists($xmlFile)) {
- $xml = simplexml_load_file($xmlFile);
+$contentTemplate = '/var/www/templates/home.php';
- foreach ($xml->channel as $channel) {
- $id = (string)$channel['id'];
- $channels[$id] = [
- 'id' => $id,
- 'name' => (string)$channel->{'display-name'},
- 'icon' => isset($channel->icon) ? (string)$channel->icon['src'] : null,
- ];
- }
-
- foreach ($xml->programme as $programme) {
- $channelId = (string)$programme['channel'];
- $start = parseXmltvTime((string)$programme['start']);
- $stop = parseXmltvTime((string)$programme['stop']);
-
- if (!$start || !$stop) {
- continue;
- }
-
- $programmes[$channelId][] = [
- 'title' => (string)$programme->title,
- 'desc' => (string)$programme->desc,
- 'category' => (string)$programme->category,
- 'start' => $start,
- 'stop' => $stop,
- ];
- }
-}
-?>
-
-
-
-
- TVGuide
-
-
-
-
-
-
-
-
-
- Keine Sender gefunden. Prüfe /opt/tvguide-docker/epg/guide.xml.
-
-
-
- $channel): ?>
-
-
-
-
-
-
Keine Sendungen gefunden.
-
-
-
-
-
- = $p['start']->format('H:i') ?> – = $p['stop']->format('H:i') ?>
-
-
- = htmlspecialchars($p['title']) ?>
-
-
-
- = htmlspecialchars($p['desc']) ?>
-
-
-
-
= htmlspecialchars($p['category']) ?>
-
-
-
-
-
-
-
-
-
+View::render('layout', [
+ 'title' => $config->get('title'),
+ 'channels' => $channels,
+ 'programmes' => $programmes,
+ 'contentTemplate' => $contentTemplate,
+]);
diff --git a/app/src/Config.php b/app/src/Config.php
new file mode 100644
index 0000000..e8a04f1
--- /dev/null
+++ b/app/src/Config.php
@@ -0,0 +1,43 @@
+config = json_decode(file_get_contents($configFile), true) ?? [];
+ }
+
+ if (file_exists($channelsFile)) {
+ $this->channels = json_decode(file_get_contents($channelsFile), true) ?? [];
+ }
+ }
+
+ public function get(string $key, mixed $default = null): mixed
+ {
+ return $this->config[$key] ?? $default;
+ }
+
+ public function getChannels(): array
+ {
+ return $this->channels;
+ }
+
+ public function saveChannels(array $channels): void
+ {
+ $this->channels = $channels;
+
+ file_put_contents(
+ '/var/www/config/channels.json',
+ json_encode(
+ $channels,
+ JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE
+ )
+ );
+ }
+}
diff --git a/app/src/View.php b/app/src/View.php
new file mode 100644
index 0000000..26e45d2
--- /dev/null
+++ b/app/src/View.php
@@ -0,0 +1,10 @@
+file = $file;
+ $this->timezone = $timezone;
+ }
+
+ public function load(): array
+ {
+ $channels = [];
+ $programmes = [];
+
+ if (!file_exists($this->file)) {
+ return [$channels, $programmes];
+ }
+
+ $xml = simplexml_load_file($this->file);
+
+ foreach ($xml->channel as $channel) {
+ $id = (string)$channel['id'];
+
+ $channels[$id] = [
+ 'id' => $id,
+ 'name' => (string)$channel->{'display-name'},
+ 'icon' => isset($channel->icon) ? (string)$channel->icon['src'] : null,
+ ];
+ }
+
+ foreach ($xml->programme as $programme) {
+ $channelId = (string)$programme['channel'];
+ $start = $this->parseTime((string)$programme['start']);
+ $stop = $this->parseTime((string)$programme['stop']);
+
+ if (!$start || !$stop) {
+ continue;
+ }
+
+ $programmes[$channelId][] = [
+ 'title' => (string)$programme->title,
+ 'desc' => (string)$programme->desc,
+ 'category' => (string)$programme->category,
+ 'start' => $start,
+ 'stop' => $stop,
+ ];
+ }
+
+ return [$channels, $programmes];
+ }
+
+ private function parseTime(string $value): ?DateTime
+ {
+ if (!preg_match('/^(\d{14})\s+([+-]\d{4})$/', trim($value), $m)) {
+ return null;
+ }
+
+ $dt = DateTime::createFromFormat('YmdHis O', $m[1] . ' ' . $m[2]);
+
+ if (!$dt) {
+ return null;
+ }
+
+ $dt->setTimezone(new DateTimeZone($this->timezone));
+ return $dt;
+ }
+}
diff --git a/app/templates/channel.php b/app/templates/channel.php
new file mode 100644
index 0000000..5445690
--- /dev/null
+++ b/app/templates/channel.php
@@ -0,0 +1,16 @@
+
+
+
+
+
+
Keine Sendungen gefunden.
+
+
+
+ $p]); ?>
+
+
+
+
diff --git a/app/templates/home.php b/app/templates/home.php
new file mode 100644
index 0000000..a7192b4
--- /dev/null
+++ b/app/templates/home.php
@@ -0,0 +1,14 @@
+
+
+ Keine Sender gefunden. Prüfe /opt/tvguide-docker/epg/guide.xml.
+
+
+
+ $channel): ?>
+ $channelId,
+ 'channel' => $channel,
+ 'programmes' => $programmes[$channelId] ?? []
+ ]); ?>
+
+
diff --git a/app/templates/layout.php b/app/templates/layout.php
new file mode 100644
index 0000000..21e7aa1
--- /dev/null
+++ b/app/templates/layout.php
@@ -0,0 +1,25 @@
+
+
+
+
+ = htmlspecialchars($title ?? 'TVGuide') ?>
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/templates/programme.php b/app/templates/programme.php
new file mode 100644
index 0000000..6abe97b
--- /dev/null
+++ b/app/templates/programme.php
@@ -0,0 +1,19 @@
+
+
+ = $p['start']->format('H:i') ?> – = $p['stop']->format('H:i') ?>
+
+
+
+ = htmlspecialchars($p['title']) ?>
+
+
+
+
+ = htmlspecialchars($p['desc']) ?>
+
+
+
+
+
= htmlspecialchars($p['category']) ?>
+
+
diff --git a/docker-compose.yaml b/docker-compose.yaml
index e66b104..3bd5a93 100644
--- a/docker-compose.yaml
+++ b/docker-compose.yaml
@@ -8,6 +8,7 @@ services:
volumes:
- ./app/public:/var/www/html
- ./app/src:/var/www/src
+ - ./app/templates:/var/www/templates
- ./app/config:/var/www/config
- ./app/cache:/var/www/cache
- ./epg:/var/www/epg