restyle #9
@@ -21,6 +21,30 @@ use Psr\Http\Message\ServerRequestInterface;
|
||||
*/
|
||||
class ApiController extends BasicController
|
||||
{
|
||||
/**
|
||||
* Возвращает информацию о плейлисте
|
||||
*
|
||||
* @param ServerRequestInterface $request
|
||||
* @param ResponseInterface $response
|
||||
* @return ResponseInterface
|
||||
* @throws Exception
|
||||
*/
|
||||
public function getOne(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface
|
||||
{
|
||||
try {
|
||||
$code = $request->getAttributes()['code'] ?? null;
|
||||
empty($code) && throw new PlaylistNotFoundException('');
|
||||
|
||||
$playlist = ini()->getPlaylist($code);
|
||||
if ($playlist['isOnline'] === true) {
|
||||
unset($playlist['content']);
|
||||
}
|
||||
return $this->responseJson($response, 200, $playlist);
|
||||
} catch (PlaylistNotFoundException $e) {
|
||||
return $this->responseJsonError($response, 404, $e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Возвращает информацию о каналов плейлиста
|
||||
*
|
||||
@@ -56,24 +80,4 @@ class ApiController extends BasicController
|
||||
return $response->withStatus(200)
|
||||
->withHeader('Content-Type', $mime);
|
||||
}
|
||||
|
||||
/**
|
||||
* Возвращает информацию о плейлисте
|
||||
*
|
||||
* @param ServerRequestInterface $request
|
||||
* @param ResponseInterface $response
|
||||
* @return ResponseInterface
|
||||
* @throws Exception
|
||||
*/
|
||||
public function json(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface
|
||||
{
|
||||
$code = $request->getAttributes()['code'];
|
||||
|
||||
try {
|
||||
$playlist = ini()->getPlaylist($code);
|
||||
return $this->responseJson($response, 200, $playlist);
|
||||
} catch (PlaylistNotFoundException $e) {
|
||||
return $this->responseJsonError($response, 404, $e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,8 +34,9 @@ class BasicController
|
||||
*/
|
||||
public function notFound(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface
|
||||
{
|
||||
$code = $request->getAttributes()['code'] ?? '';
|
||||
$response->withStatus(404);
|
||||
$this->view($request, $response, 'notfound.twig');
|
||||
$this->view($request, $response, 'notfound.twig', ['code' => $code]);
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
@@ -63,21 +63,6 @@ class WebController extends BasicController
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Возвращает страницу FAQ
|
||||
*
|
||||
* @param ServerRequestInterface $request
|
||||
* @param ResponseInterface $response
|
||||
* @return ResponseInterface
|
||||
* @throws LoaderError
|
||||
* @throws RuntimeError
|
||||
* @throws SyntaxError
|
||||
*/
|
||||
public function faq(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface
|
||||
{
|
||||
return $this->view($request, $response, 'faq.twig');
|
||||
}
|
||||
|
||||
/**
|
||||
* Переадресует запрос на прямую ссылку плейлиста
|
||||
*
|
||||
|
||||
@@ -41,7 +41,11 @@ class IniFile
|
||||
|
||||
// сохраняем порядок
|
||||
foreach (array_keys($ini) as $code) {
|
||||
$data = redis()->get($code);
|
||||
try {
|
||||
$data = @redis()->get($code);
|
||||
} catch (Throwable) {
|
||||
$data = false;
|
||||
}
|
||||
if ($data === false) {
|
||||
$raw = $ini[$code];
|
||||
$data = [
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
*/
|
||||
|
||||
use App\Controllers\ApiController;
|
||||
use App\Controllers\BasicController;
|
||||
use App\Controllers\BotController;
|
||||
use App\Controllers\WebController;
|
||||
|
||||
@@ -35,12 +36,6 @@ return [
|
||||
'handler' => [WebController::class, 'home'],
|
||||
'name' => 'home',
|
||||
],
|
||||
[
|
||||
'method' => 'GET',
|
||||
'path' => '/faq',
|
||||
'handler' => [WebController::class, 'faq'],
|
||||
'name' => 'faq',
|
||||
],
|
||||
[
|
||||
'method' => 'GET',
|
||||
'path' => '/{code:[0-9a-zA-Z]+}[.m3u[8]]',
|
||||
@@ -62,13 +57,13 @@ return [
|
||||
|
||||
[
|
||||
'method' => 'GET',
|
||||
'path' => '/{code:[0-9a-zA-Z]+}/json',
|
||||
'handler' => [ApiController::class, 'json'],
|
||||
'name' => 'json',
|
||||
'path' => '/api/playlists/{code:[0-9a-zA-Z]+}',
|
||||
'handler' => [ApiController::class, 'getOne'],
|
||||
'name' => 'api::getOne',
|
||||
],
|
||||
[
|
||||
'method' => 'GET',
|
||||
'path' => '/{code:[0-9a-zA-Z]+}/qrcode',
|
||||
'path' => '/api/playlists/{code:[0-9a-zA-Z]+}/qrcode',
|
||||
'handler' => [ApiController::class, 'makeQrCode'],
|
||||
'name' => 'api::makeQrCode',
|
||||
],
|
||||
|
||||
@@ -175,7 +175,7 @@
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body text-center">
|
||||
<img src="/{{ playlist.code }}/qrcode" alt="">
|
||||
<img src="/api/playlists/{{ playlist.code }}/qrcode" alt="">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
197
views/list.twig
197
views/list.twig
@@ -10,119 +10,128 @@
|
||||
|
||||
{% block metakeywords %}самообновляемые,бесплатные,iptv-плейлисты,iptv,плейлисты{% endblock %}
|
||||
|
||||
{% block head %}
|
||||
<style>
|
||||
.card {transition: box-shadow .2s, transform .2s}
|
||||
.card.hover-success:hover {transform: translateY(-7px); box-shadow: rgba(var(--bs-success-rgb), 1) 0 5px 20px -5px}
|
||||
.card.hover-danger:hover {transform: translateY(-7px); box-shadow: rgba(var(--bs-danger-rgb), 1) 0 5px 20px -5px}
|
||||
.card.hover-secondary:hover {transform: translateY(-7px); box-shadow: rgba(var(--bs-secondary-rgb), 1) 0 5px 20px -5px}
|
||||
</style>
|
||||
<script>
|
||||
function setDefaultLogo(imgtag) {
|
||||
imgtag.onerror = null
|
||||
imgtag.src = '/no-tvg-logo.png'
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
{% block header %}
|
||||
<div class="row text-muted small">
|
||||
<div class="col-md">
|
||||
Список изменён: {{ updatedAt }} МСК<br/>
|
||||
Плейлистов в списке: <strong>{{ count }}</strong>
|
||||
<div class="d-flex flex-wrap justify-content-between align-items-center mb-4">
|
||||
<div class="mb-2">
|
||||
<h2 class="mb-0">Список плейлистов ({{ count }})</h2>
|
||||
<div class="text-muted small">Изменён {{ updatedAt }} МСК</div>
|
||||
</div>
|
||||
<div class="col-md">
|
||||
Состояние проверки:<br />
|
||||
<span class="me-1">
|
||||
<span class="badge me-1 bg-success text-dark">online</span>{{ onlineCount }}
|
||||
</span>
|
||||
<span class="me-1">
|
||||
<span class="badge me-1 bg-danger text-dark">offline</span>{{ offlineCount }}
|
||||
</span>
|
||||
<span class="me-1">
|
||||
<span class="badge me-1 bg-secondary text-dark" title="В очереди на проверку">unknown</span>{{ uncheckedCount }}
|
||||
</span>
|
||||
<div class="d-flex flex-wrap gap-2 mb-2">
|
||||
<span class="badge bg-success">online: {{ onlineCount }}</span>
|
||||
<span class="badge bg-danger">offline: {{ offlineCount }}</span>
|
||||
<span class="badge bg-secondary">unknown: {{ uncheckedCount }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<hr/>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="table-responsive">
|
||||
<table class="table table-responsive table-dark table-hover small">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="col-1 text-center">Код</th>
|
||||
<th class="col-8">Информация о плейлисте</th>
|
||||
<th class="col-1 text-center">Каналов</th>
|
||||
<th class="col-2 d-none d-sm-table-cell">Ссылка для ТВ</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<div class="row g-4">
|
||||
{% for code, playlist in playlists %}
|
||||
<tr class="pls" data-playlist-code="{{ code }}">
|
||||
<td class="text-center font-monospace code">{{ code }}</td>
|
||||
<td class="info">
|
||||
{% if playlist.isOnline is same as(true) %}
|
||||
<span class="badge small bg-success text-dark">online</span>
|
||||
{% elseif playlist.isOnline is same as(false) %}
|
||||
<span class="badge small bg-danger text-dark">offline</span>
|
||||
{% elseif playlist.isOnline is same as(null) %}
|
||||
<span class="badge small bg-secondary text-dark" title="В очереди на проверку">unknown</span>
|
||||
{% endif %}
|
||||
{% if "adult" in playlist.tags %}
|
||||
<span class="badge small bg-warning text-dark" title="Есть каналы для взрослых!">18+</span>
|
||||
{% endif %}
|
||||
<a href="/{{ code }}/details" class="text-light fw-bold text-decoration-none">{{ playlist.name }}</a>
|
||||
<div class="small mt-2">
|
||||
<p class="my-1 d-none d-lg-block">
|
||||
{% set statusClass = 'secondary' %}
|
||||
{% if playlist.isOnline is same as(true) %}
|
||||
{% set statusClass = 'success' %}
|
||||
{% elseif playlist.isOnline is same as(false) %}
|
||||
{% set statusClass = 'danger' %}
|
||||
{% endif %}
|
||||
|
||||
<div class="col-md-6 col-lg-4">
|
||||
<div class="card bg-dark text-light h-100 border border-{{ statusClass }} hover-{{ statusClass }} position-relative">
|
||||
<a href="/{{ code }}/details" class="text-decoration-none">
|
||||
<div class="card-header d-flex align-items-center gap-2">
|
||||
<span class="font-monospace text-{{ statusClass }}">{{ code }}</span>
|
||||
<span class="badge bg-{{ statusClass }} ms-auto">
|
||||
{% if playlist.isOnline is same as(true) %}online
|
||||
{% elseif playlist.isOnline is same as(false) %}offline
|
||||
{% elseif playlist.isOnline is same as(null) %}unknown
|
||||
{% endif %}
|
||||
</span>
|
||||
{% if "adult" in playlist.tags %}
|
||||
<span class="badge bg-warning text-dark" title="Есть каналы для взрослых!">18+</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<div class="card-body position-relative z-2">
|
||||
<a href="/{{ code }}/details" class="text-decoration-none">
|
||||
<h5 class="card-title text-light">{{ playlist.name }}</h5>
|
||||
</a>
|
||||
{% if playlist.description is not same as(null) %}
|
||||
<p class="card-text small text-secondary d-none d-md-block">{{ playlist.description }}</p>
|
||||
{% endif %}
|
||||
<div class="d-flex flex-wrap gap-2 mb-1">
|
||||
{% if playlist.isOnline is not same as(null) %}
|
||||
<span class="badge border border-secondary">
|
||||
<ion-icon name="videocam-outline" class="me-1"></ion-icon> {{ playlist.channels|length }}<span class="d-none d-xl-inline-block"> каналов</span>
|
||||
</span>
|
||||
{% endif %}
|
||||
{% if playlist.groups|length > 0 %}
|
||||
<ion-icon name="folder-open-outline" title="Каналы разбиты на группы"></ion-icon>
|
||||
<span class="badge border border-secondary">
|
||||
<ion-icon name="folder-open-outline" class="me-1"></ion-icon> {{ playlist.groups|length }}<span class="d-none d-xl-inline-block"> групп</span>
|
||||
</span>
|
||||
{% endif %}
|
||||
{% if playlist.hasTvg %}
|
||||
<ion-icon name="newspaper-outline" title="Есть программа передач"></ion-icon>
|
||||
<span class="badge border border-secondary">
|
||||
<ion-icon name="newspaper-outline" class="me-1"></ion-icon><span class="d-none d-xl-inline-block"> ТВ-программа</span>
|
||||
</span>
|
||||
{% endif %}
|
||||
{% if playlist.hasCatchup %}
|
||||
<ion-icon name="play-back-outline" title="Есть перемотка (архив)"></ion-icon>
|
||||
<span class="badge border border-secondary">
|
||||
<ion-icon name="play-back-outline" class="me-1"></ion-icon><span class="d-none d-xl-inline-block"> Архив</span>
|
||||
</span>
|
||||
{% endif %}
|
||||
{{ playlist.description }}
|
||||
</p>
|
||||
{% if playlist.tags|length > 0 %}
|
||||
<p class="my-1 d-none d-lg-block text-muted" title="Теги, присвоенные каналам при проверке">
|
||||
<ion-icon name="pricetag-outline" class="me-1"></ion-icon>
|
||||
{% for tag in playlist.tags %}
|
||||
<span class="chtag">#{{ tag }}</span>
|
||||
{% endfor %}
|
||||
</p>
|
||||
{% endif %}
|
||||
<a href="/{{ code }}/details" class="text-light">Подробнее...</a>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
{% if (playlist.isOnline is not same as(null)) %}
|
||||
{{ playlist.channels|length }}
|
||||
{% else %}
|
||||
?
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="d-none d-sm-table-cell">
|
||||
<span onclick="prompt('Скопируй адрес плейлиста. Если не работает, добавь \'.m3u\' в конец.', '{{ mirror_url(playlist.code) }}')"
|
||||
title="Нажми на ссылку, чтобы скопировать её в буфер обмена"
|
||||
class="font-monospace cursor-pointer"
|
||||
data-bs-toggle="tooltip"
|
||||
data-bs-placement="top"
|
||||
title="Нажми на ссылку, чтобы скопировать её в буфер обмена"
|
||||
|
||||
<div class="card-footer cursor-pointer"
|
||||
onclick="prompt('Скопируй адрес плейлиста. Если не работает, добавь \'.m3u\' в конец.', '{{ mirror_url(playlist.code) }}')"
|
||||
title="Нажми чтобы скопировать"
|
||||
>
|
||||
{{ mirror_url(playlist.code) }}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
<div class="d-flex justify-content-between align-items-center small">
|
||||
<span class="font-monospace text-truncate">
|
||||
{{ mirror_url(playlist.code) }}
|
||||
</span>
|
||||
<ion-icon name="copy-outline"></ion-icon>
|
||||
</div>
|
||||
</div>
|
||||
<a href="/{{ code }}/details" class="text-decoration-none">
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% if pageCount > 1 %}
|
||||
<div aria-label="pages">
|
||||
<ul class="pagination justify-content-center">
|
||||
{% for page in range(1, pageCount) %}
|
||||
{% if page == pageCurrent %}
|
||||
<li class="page-item active" aria-current="page">
|
||||
<span class="page-link">{{ page }}</span>
|
||||
</li>
|
||||
{% else %}
|
||||
<li class="page-item">
|
||||
<a class="page-link bg-dark border-secondary text-light" href="page/{{ page }}">{{ page }}</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{% if pageCount > 1 %}
|
||||
<nav class="mt-4">
|
||||
<ul class="pagination justify-content-center">
|
||||
{% for page in range(1, pageCount) %}
|
||||
{% if page == pageCurrent %}
|
||||
<li class="page-item active" aria-current="page">
|
||||
<span class="page-link">{{ page }}</span>
|
||||
</li>
|
||||
{% else %}
|
||||
<li class="page-item">
|
||||
<a class="page-link bg-dark text-light" href="page/{{ page }}">{{ page }}</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</nav>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block footer %}
|
||||
|
||||
@@ -6,19 +6,26 @@
|
||||
|
||||
{% extends "template.twig" %}
|
||||
|
||||
{% block header %}
|
||||
<h2>Плейлист не найден</h2>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<p>
|
||||
Плейлист {{ id }} не найден
|
||||
</p>
|
||||
<a class="btn btn-outline-light" href="{{ base_url() }}" title="На главную">
|
||||
Перейти к списку
|
||||
</a>
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-8 col-lg-6 text-center">
|
||||
<div class="card bg-dark border-secondary">
|
||||
<div class="card-body">
|
||||
<ion-icon name="warning-outline" class="display-1 text-warning mb-3"></ion-icon>
|
||||
<h2 class="card-title">Плейлист <code>{{ code }}</code> не найден</h2>
|
||||
<p class="card-text">
|
||||
Возможно, его здесь никогда не было, либо он уже был удалён.
|
||||
</p>
|
||||
<p class="text-muted small">
|
||||
Если хочешь, чтобы здесь был плейлист, предложи его к добавлению.
|
||||
<br />
|
||||
<a href="https://iptv.axenov.dev/docs/support.html#participate">Как это сделать?</a>
|
||||
</p>
|
||||
<a class="btn btn-outline-light" href="/" title="На главную">
|
||||
<ion-icon name="list-outline" class="me-1"></ion-icon>Перейти к списку плейлистов
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
###########################################################################}
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<html lang="ru" class="h-100">
|
||||
<head>
|
||||
<title>{% block title %}{{ config('app.title') }}{% endblock %}</title>
|
||||
<meta charset="utf-8">
|
||||
@@ -30,67 +30,107 @@
|
||||
<meta name="theme-color" content="#212529">
|
||||
{% block head %}{% endblock %}
|
||||
</head>
|
||||
<body class="bg-dark text-light">
|
||||
<div class="container col-lg-10 mx-auto">
|
||||
<header>
|
||||
<nav class="navbar navbar-expand-lg navbar-dark">
|
||||
<a class="navbar-brand" href="/" title="На главную">
|
||||
<img src="/favicon/favicon-32x32.png" class="d-inline-block px-lg-1" alt="Логотип проекта - emoji телевизора"/>
|
||||
{{ config('app.title') }}
|
||||
</a>
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="collapse navbar-collapse" id="navbarNav">
|
||||
<ul class="navbar-nav">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" target="_blank" href="/docs">Документация</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" target="_blank" href="/docs/support.html">Помочь проекту</a>
|
||||
</li>
|
||||
<li class="nav-item dropdown">
|
||||
<a class="nav-link dropdown-toggle"
|
||||
href="#"
|
||||
role="button"
|
||||
data-bs-toggle="dropdown"
|
||||
aria-expanded="false"
|
||||
>
|
||||
Telegram
|
||||
</a>
|
||||
<ul class="dropdown-menu dropdown-menu-dark">
|
||||
<li><a class="dropdown-item" target="_blank" href="https://t.me/iptv_aggregator">Канал @iptv_aggregator</a></li>
|
||||
<li><a class="dropdown-item" target="_blank" href="https://t.me/iptv_aggregator_chat">Чат @iptv_aggregator_chat</a></li>
|
||||
<li><a class="dropdown-item" target="_blank" href="https://t.me/iptv_aggregator_bot">Бот @iptv_aggregator_bot</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
</header>
|
||||
<body class="d-flex flex-column h-100 bg-dark text-light">
|
||||
<header class="sticky-top bg-dark border-bottom border-secondary">
|
||||
<nav class="navbar navbar-expand-lg navbar-dark container px-2">
|
||||
<a class="navbar-brand d-flex align-items-center gap-2" href="/" title="На главную">
|
||||
<img src="/favicon/favicon-32x32.png" alt="Логотип проекта" class="d-inline-block">
|
||||
<span>{{ config('app.title') }}</span>
|
||||
</a>
|
||||
<button class="navbar-toggler"
|
||||
type="button"
|
||||
data-bs-toggle="collapse"
|
||||
data-bs-target="#navbarNav"
|
||||
aria-controls="navbarNav"
|
||||
>
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="collapse navbar-collapse" id="navbarNav">
|
||||
<ul class="navbar-nav ms-auto">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" target="_blank" href="/docs">
|
||||
<ion-icon name="document-text-outline" class="me-1"></ion-icon> Документация
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" target="_blank" href="/docs/support.html">
|
||||
<ion-icon name="heart-outline" class="me-1"></ion-icon> Помочь проекту
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item dropdown">
|
||||
<a class="nav-link dropdown-toggle d-flex align-items-center"
|
||||
href="#"
|
||||
role="button"
|
||||
data-bs-toggle="dropdown"
|
||||
aria-expanded="false"
|
||||
>
|
||||
<ion-icon name="paper-plane-outline" class="me-1"></ion-icon> Telegram
|
||||
</a>
|
||||
<ul class="dropdown-menu dropdown-menu-dark">
|
||||
<li>
|
||||
<a class="dropdown-item d-flex align-items-center gap-2" target="_blank" href="https://t.me/iptv_aggregator">
|
||||
<ion-icon name="megaphone-outline"></ion-icon> Канал @iptv_aggregator
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a class="dropdown-item d-flex align-items-center gap-2" target="_blank" href="https://t.me/iptv_aggregator_chat">
|
||||
<ion-icon name="chatbubbles-outline"></ion-icon> Чат @iptv_aggregator_chat
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a class="dropdown-item d-flex align-items-center gap-2" target="_blank" href="https://t.me/iptv_aggregator_bot">
|
||||
<ion-icon name="chatbox-ellipses-outline"></ion-icon> Бот @iptv_aggregator_bot
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<section class="container h-100 pt-lg-3 px-0 pb-0">
|
||||
{% block header %}{% endblock %}
|
||||
<main class="flex-grow-1 container py-4">
|
||||
{% block header %}{% endblock %}
|
||||
<div class="content-wrapper">
|
||||
{% block content %}{% endblock %}
|
||||
</section>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<footer class="py-4 text-center">
|
||||
<script src="/js/bootstrap.bundle.min.js"></script>
|
||||
{% block footer %}{% endblock %}
|
||||
<a target="_blank" href="/docs">Документация</a> | <a
|
||||
target="_blank" href="https://git.axenov.dev/IPTV">Исходники</a> | <a
|
||||
target="_blank" href="https://axenov.dev">axenov.dev</a> | <a
|
||||
target="_blank" href="https://t.me/iptv_aggregator">Канал</a> | <a
|
||||
target="_blank" href="https://t.me/iptv_aggregator_chat">Чат</a> | <a
|
||||
target="_blank" href="https://t.me/iptv_aggregator_bot">Бот</a>
|
||||
<footer class="bg-dark border-top border-secondary py-4">
|
||||
<div class="container text-center">
|
||||
<div class="d-flex flex-wrap justify-content-center gap-3 mb-3">
|
||||
<a target="_blank" href="/docs" class="text-light text-decoration-none d-flex align-items-center gap-1">
|
||||
<ion-icon name="document-text-outline"></ion-icon>Документация
|
||||
</a>
|
||||
<a target="_blank" href="https://git.axenov.dev/IPTV" class="text-light text-decoration-none d-flex align-items-center gap-1">
|
||||
<ion-icon name="code-slash-outline"></ion-icon>Исходники
|
||||
</a>
|
||||
<a target="_blank" href="https://axenov.dev" class="text-light text-decoration-none d-flex align-items-center gap-1">
|
||||
<ion-icon name="person-outline"></ion-icon>axenov.dev
|
||||
</a>
|
||||
<a target="_blank" href="https://t.me/iptv_aggregator" class="text-light text-decoration-none d-flex align-items-center gap-1">
|
||||
<ion-icon name="megaphone-outline"></ion-icon>Канал
|
||||
</a>
|
||||
<a target="_blank" href="https://t.me/iptv_aggregator_chat" class="text-light text-decoration-none d-flex align-items-center gap-1">
|
||||
<ion-icon name="chatbubbles-outline"></ion-icon>Чат
|
||||
</a>
|
||||
<a target="_blank" href="https://t.me/iptv_aggregator_bot" class="text-light text-decoration-none d-flex align-items-center gap-1">
|
||||
<ion-icon name="chatbox-ellipses-outline"></ion-icon>Бот
|
||||
</a>
|
||||
</div>
|
||||
<div>
|
||||
<a class="small text-secondary d-inline-flex align-items-center gap-1"
|
||||
href="https://git.axenov.dev/IPTV/web/releases/tag/v{{ version() }}"
|
||||
target="_blank"
|
||||
>
|
||||
<ion-icon name="pricetag-outline"></ion-icon>v{{ version() }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<br>
|
||||
<a class="small text-secondary"
|
||||
href="https://git.axenov.dev/IPTV/web/releases/tag/v{{ version() }}"
|
||||
target="_blank"
|
||||
>v{{ version() }}</a>
|
||||
</footer>
|
||||
</div>
|
||||
<script src="/js/bootstrap.bundle.min.js"></script>
|
||||
{% block footer %}{% endblock %}
|
||||
|
||||
{% include("custom.twig") ignore missing %}
|
||||
</body>
|
||||
|
||||
Reference in New Issue
Block a user