End of Day

This commit is contained in:
Alexander Schiemann 2026-01-17 00:55:52 +01:00
parent 43047ec499
commit 4dfd98da93
16 changed files with 795 additions and 988 deletions

View File

@ -157,7 +157,7 @@ final class CApp {
if (!str_starts_with($path, '/assets') && $path !== '/favicon.ico') {
$lang = isset($_COOKIE['scmedia_lang']) ? (string)$_COOKIE['scmedia_lang'] : '';
$token = (string)($_COOKIE['access_token'] ?? '');
$token = (string)($_COOKIE['scmedia_access_token'] ?? '');
if ($lang === '' && $token !== '' && isset($this->container->all()['auth'])) {
/** @var \ScMedia\Services\AuthService $auth */
$auth = $this->container->all()['auth'];

View File

@ -65,10 +65,14 @@ final class AuthController extends BaseController {
}
$tokens = $res['tokens'] ?? [];
if (!empty($tokens['access_token'])) {
setcookie('access_token', $tokens['access_token'], [
setcookie('scmedia_access_token', $tokens['access_token'], [
'expires' => (int)($tokens['access_expires_at'] ?? 0),
'path' => '/',
'httponly' => true,
'samesite' => 'Lax',
]);
setcookie('access_token', '', [
'expires' => time() - 3600,
'path' => '/',
'samesite' => 'Lax',
]);
}
@ -96,10 +100,14 @@ final class AuthController extends BaseController {
}
$tokens = $res['tokens'] ?? [];
if (!empty($tokens['access_token'])) {
setcookie('access_token', $tokens['access_token'], [
setcookie('scmedia_access_token', $tokens['access_token'], [
'expires' => (int)($tokens['access_expires_at'] ?? 0),
'path' => '/',
'httponly' => true,
'samesite' => 'Lax',
]);
setcookie('access_token', '', [
'expires' => time() - 3600,
'path' => '/',
'samesite' => 'Lax',
]);
}
@ -120,10 +128,14 @@ final class AuthController extends BaseController {
}
$tokens = $res['tokens'] ?? [];
if (!empty($tokens['access_token'])) {
setcookie('access_token', $tokens['access_token'], [
setcookie('scmedia_access_token', $tokens['access_token'], [
'expires' => (int)($tokens['access_expires_at'] ?? 0),
'path' => '/',
'httponly' => true,
'samesite' => 'Lax',
]);
setcookie('access_token', '', [
'expires' => time() - 3600,
'path' => '/',
'samesite' => 'Lax',
]);
}
@ -138,16 +150,20 @@ final class AuthController extends BaseController {
if ($refresh !== '') {
$auth->logout($refresh);
} else {
$token = (string)($_COOKIE['access_token'] ?? '');
$token = (string)($_COOKIE['scmedia_access_token'] ?? '');
$auth->logoutByAccessToken($token);
}
setcookie('scmedia_access_token', '', [
'expires' => time() - 3600,
'path' => '/',
'samesite' => 'Lax',
]);
setcookie('access_token', '', [
'expires' => time() - 3600,
'path' => '/',
'httponly' => true,
'samesite' => 'Lax',
]);
setcookie('sse_key', '', [
setcookie('scmedia_sse_key', '', [
'expires' => time() - 3600,
'path' => '/',
'httponly' => true,
@ -225,7 +241,7 @@ final class AuthController extends BaseController {
$user = $auth->requireAuth();
$key = $auth->issueSseKey((int)$user['id']);
$ttl = (int)($key['expires_in'] ?? 60);
setcookie('sse_key', $key['key'], [
setcookie('scmedia_sse_key', $key['key'], [
'expires' => time() + max(10, $ttl),
'path' => '/',
'httponly' => true,

View File

@ -107,7 +107,7 @@ final class EventsController extends BaseController {
// Read and validate SSE key.
private function readUserId(): ?int {
$key = isset($_COOKIE['sse_key']) ? (string)$_COOKIE['sse_key'] : '';
$key = isset($_COOKIE['scmedia_sse_key']) ? (string)$_COOKIE['scmedia_sse_key'] : '';
$key = trim($key, "\"'");
$key = urldecode($key);
$userId = $this->auth->validateSseKey($key);

View File

@ -988,7 +988,7 @@ final class AuthService {
if (preg_match('/^Bearer\s+(.+)$/i', $header, $m)) {
return trim($m[1]);
}
$cookie = $_COOKIE['access_token'] ?? '';
$cookie = $_COOKIE['scmedia_access_token'] ?? '';
if (is_string($cookie) && $cookie !== '') {
return $cookie;
}

View File

@ -47,6 +47,7 @@ final class SettingsService {
'layout' => $map['layout'] ?? [],
'media_rules' => $map['media_rules'] ?? [],
'rules' => $map['rules'] ?? [],
'templates' => $map['templates'] ?? [],
'sources' => $map['sources'] ?? [],
'metadata' => $map['metadata'] ?? [],
'exports' => $map['exports'] ?? [],
@ -67,7 +68,7 @@ final class SettingsService {
throw new Exception("Settings revision mismatch");
}
$allowed = ['general', 'scanner_defaults', 'paths', 'tools', 'logs', 'layout', 'media_rules', 'rules', 'sources', 'metadata', 'exports', 'ui', 'background', 'safety', 'tasks', 'pending_tasks'];
$allowed = ['general', 'scanner_defaults', 'paths', 'tools', 'logs', 'layout', 'media_rules', 'rules', 'templates', 'sources', 'metadata', 'exports', 'ui', 'background', 'safety', 'tasks', 'pending_tasks'];
$toSave = [];
foreach ($allowed as $k) {
if (array_key_exists($k, $payload)) {

View File

@ -27,17 +27,16 @@ $toolbarRightHtml = '';
<main class="layout">
<nav class="tabs">
<button class="tab active" data-tab="scan" data-i18n="settings.tabs.scan_profiles"><?php echo htmlspecialchars($t('settings.tabs.scan_profiles', 'Scan Profiles'), ENT_QUOTES); ?></button>
<button class="tab" data-tab="layout" data-i18n="settings.tabs.library_layout"><?php echo htmlspecialchars($t('settings.tabs.library_layout', 'Library'), ENT_QUOTES); ?></button>
<button class="tab active" data-tab="server" data-i18n="settings.tabs.server"><?php echo htmlspecialchars($t('settings.tabs.server', 'Server'), ENT_QUOTES); ?></button>
<button class="tab" data-tab="plugins" data-i18n="settings.tabs.plugins"><?php echo htmlspecialchars($t('settings.tabs.plugins', 'Plugins'), ENT_QUOTES); ?></button>
<button class="tab" data-tab="tasks" data-i18n="settings.tabs.tasks"><?php echo htmlspecialchars($t('settings.tabs.tasks', 'Tasks'), ENT_QUOTES); ?></button>
<button class="tab" data-tab="sources" data-i18n="settings.tabs.sources"><?php echo htmlspecialchars($t('settings.tabs.sources', 'Sources'), ENT_QUOTES); ?></button>
<button class="tab" data-tab="rules" data-i18n="settings.tabs.rules"><?php echo htmlspecialchars($t('settings.tabs.rules', 'Rules'), ENT_QUOTES); ?></button>
<button class="tab" data-tab="server" data-i18n="settings.tabs.server"><?php echo htmlspecialchars($t('settings.tabs.server', 'Server'), ENT_QUOTES); ?></button>
<button class="tab" data-tab="templates" data-i18n="settings.tabs.templates"><?php echo htmlspecialchars($t('settings.tabs.templates', 'Templates'), ENT_QUOTES); ?></button>
<button class="tab" data-tab="tasks" data-i18n="settings.tabs.tasks"><?php echo htmlspecialchars($t('settings.tabs.tasks', 'Tasks'), ENT_QUOTES); ?></button>
<button class="tab" data-tab="tools" data-i18n="settings.tabs.tools"><?php echo htmlspecialchars($t('settings.tabs.tools', 'Programs'), ENT_QUOTES); ?></button>
<button class="tab" data-tab="logs" data-i18n="settings.tabs.logs"><?php echo htmlspecialchars($t('settings.tabs.logs', 'Logs'), ENT_QUOTES); ?></button>
<button class="tab" data-tab="admin" data-i18n="admin.title"><?php echo htmlspecialchars($t('admin.title', 'Admin'), ENT_QUOTES); ?></button>
<button class="tab" data-tab="debug" id="tabDebug"<?php echo empty($debugToolsEnabled) ? ' style="display:none;"' : ''; ?> data-i18n="settings.tabs.debug"><?php echo htmlspecialchars($t('settings.tabs.debug', 'Debug'), ENT_QUOTES); ?></button>
<button class="tab" data-tab="about" data-i18n="settings.tabs.about"><?php echo htmlspecialchars($t('settings.tabs.about', 'About'), ENT_QUOTES); ?></button>
<?php
$statusHidden = false;
$statusClass = $statusHidden ? 'status hidden' : 'status';
@ -52,86 +51,11 @@ $toolbarRightHtml = '';
</nav>
<section class="panel">
<!-- ===================== SCAN TAB ===================== -->
<div class="tabpane active" id="pane-scan">
<!-- ===================== SOURCES TAB ===================== -->
<div class="tabpane" id="pane-sources">
<div class="pane-header">
<h2 data-i18n="settings.scan_profiles.title"><?php echo htmlspecialchars($t('settings.scan_profiles.title', 'Scan Profiles'), ENT_QUOTES); ?></h2>
<button id="btnAddProfile" class="btn primary" data-i18n="settings.scan_profiles.add"><?php echo htmlspecialchars($t('settings.scan_profiles.add', 'Add profile'), ENT_QUOTES); ?></button>
</div>
<div class="card">
<template id="tplProfileRow">
<tr>
<td><button class="drag-handle" type="button" data-action="drag" data-i18n-aria="settings.scan_profiles.move">
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false">
<path fill="currentColor" d="M7 5h10v2H7V5zm0 6h10v2H7v-2zm0 6h10v2H7v-2z"/>
</svg>
</button></td>
<td><input type="checkbox" data-field="enabled"></td>
<td data-field="profile_type"></td>
<td data-field="name"></td>
<td><code data-field="root_path"></code></td>
<td data-field="max_depth"></td>
<td data-field="exclude"></td>
<td data-field="ext"></td>
<td data-field="last_scan"></td>
<td data-field="last_result"></td>
<td>
<button class="btn" data-action="edit" data-i18n="common.edit"><?php echo htmlspecialchars($t('common.edit', 'Edit'), ENT_QUOTES); ?></button>
<button class="btn" data-action="del" data-i18n="common.delete"><?php echo htmlspecialchars($t('common.delete', 'Delete'), ENT_QUOTES); ?></button>
</td>
</tr>
</template>
<?php
$id = 'profilesTable';
$tableClass = 'data-table';
$headers = [
['label' => '', 'i18n' => '', 'key' => '', 'filter' => null],
['label' => $t('settings.scan_profiles.th_on', 'On'), 'i18n' => 'settings.scan_profiles.th_on', 'key' => 'enabled', 'filter' => ['type' => 'boolean']],
['label' => $t('settings.scan_profiles.th_type', 'Type'), 'i18n' => 'settings.scan_profiles.th_type', 'key' => 'profile_type', 'filter' => ['type' => 'text', 'ops' => ['like','eq']]],
['label' => $t('settings.scan_profiles.th_name', 'Name'), 'i18n' => 'settings.scan_profiles.th_name', 'sort' => 'name', 'key' => 'name', 'filter' => ['type' => 'text', 'ops' => ['like','eq','empty']]],
['label' => $t('settings.scan_profiles.th_root', 'Root path'), 'i18n' => 'settings.scan_profiles.th_root', 'key' => 'root_path', 'filter' => ['type' => 'text', 'ops' => ['like','eq','empty']]],
['label' => $t('settings.scan_profiles.th_depth', 'Depth'), 'i18n' => 'settings.scan_profiles.th_depth', 'key' => 'max_depth', 'filter' => ['type' => 'number', 'ops' => ['eq','between','gt','lt']]],
['label' => $t('settings.scan_profiles.th_excludes', 'Excludes'), 'i18n' => 'settings.scan_profiles.th_excludes', 'key' => 'exclude', 'filter' => ['type' => 'text', 'ops' => ['like','eq','empty']]],
['label' => $t('settings.scan_profiles.th_ext', 'Ext'), 'i18n' => 'settings.scan_profiles.th_ext', 'key' => 'ext', 'filter' => ['type' => 'text', 'ops' => ['like','eq','empty']]],
['label' => $t('settings.scan_profiles.th_last_scan', 'Last scan'), 'i18n' => 'settings.scan_profiles.th_last_scan', 'key' => 'last_scan', 'filter' => ['type' => 'date', 'ops' => ['eq','between']]],
['label' => $t('settings.scan_profiles.th_result', 'Result'), 'i18n' => 'settings.scan_profiles.th_result', 'key' => 'last_result', 'filter' => ['type' => 'text', 'ops' => ['like','eq','empty']]],
['label' => '', 'i18n' => '', 'key' => '', 'filter' => null],
];
$attrs = ['data-table' => 'profiles'];
require __DIR__ . '/../partials/table.php';
?>
</div>
<div class="card">
<h3 data-i18n="settings.scanner_defaults.title"><?php echo htmlspecialchars($t('settings.scanner_defaults.title', 'Global scanner defaults'), ENT_QUOTES); ?></h3>
<div class="grid">
<label>
<div class="lbl" data-i18n="settings.scanner_defaults.video_ext"><?php echo htmlspecialchars($t('settings.scanner_defaults.video_ext', 'Video extensions (comma)'), ENT_QUOTES); ?></div>
<input id="videoExt" type="text" placeholder="mkv,mp4,avi" data-i18n-placeholder="settings.scanner_defaults.video_ext_ph">
</label>
<label>
<div class="lbl" data-i18n="settings.scanner_defaults.max_depth"><?php echo htmlspecialchars($t('settings.scanner_defaults.max_depth', 'Max depth default'), ENT_QUOTES); ?></div>
<input id="maxDepthDefault" type="number" min="1" max="10">
</label>
<label>
<div class="lbl" data-i18n="settings.scanner_defaults.max_files"><?php echo htmlspecialchars($t('settings.scanner_defaults.max_files', 'Max files per item'), ENT_QUOTES); ?></div>
<input id="maxFilesPerItem" type="number" min="100" max="200000">
</label>
<label>
<div class="lbl" data-i18n="settings.scanner_defaults.max_items"><?php echo htmlspecialchars($t('settings.scanner_defaults.max_items', 'Max items per scan (0 = no limit)'), ENT_QUOTES); ?></div>
<input id="maxItemsPerScan" type="number" min="0" max="1000000">
</label>
</div>
</div>
</div>
<!-- ===================== LAYOUT TAB ===================== -->
<div class="tabpane" id="pane-layout">
<div class="pane-header">
<h2 data-i18n="settings.library_layout.title"><?php echo htmlspecialchars($t('settings.library_layout.title', 'Library Layout'), ENT_QUOTES); ?></h2>
<button id="btnAddRoot" class="btn primary" data-i18n="settings.library_layout.add_root"><?php echo htmlspecialchars($t('settings.library_layout.add_root', 'Add root'), ENT_QUOTES); ?></button>
<h2 data-i18n="settings.sources.title"><?php echo htmlspecialchars($t('settings.sources.title', 'Sources'), ENT_QUOTES); ?></h2>
<button id="btnAddRoot" class="btn primary" data-i18n="settings.sources.add_root"><?php echo htmlspecialchars($t('settings.sources.add_root', 'Add source'), ENT_QUOTES); ?></button>
</div>
<div class="card">
@ -139,9 +63,9 @@ $toolbarRightHtml = '';
$id = 'rootsTable';
$tableClass = 'data-table';
$headers = [
['label' => $t('settings.library_layout.th_type', 'Type'), 'i18n' => 'settings.library_layout.th_type', 'key' => 'type', 'filter' => ['type' => 'text', 'ops' => ['like','eq']]],
['label' => $t('settings.library_layout.th_path', 'Path'), 'i18n' => 'settings.library_layout.th_path', 'sort' => 'path', 'key' => 'path', 'filter' => ['type' => 'text', 'ops' => ['like','eq','empty']]],
['label' => $t('settings.library_layout.th_status', 'Status'), 'i18n' => 'settings.library_layout.th_status', 'key' => 'status', 'filter' => ['type' => 'text', 'ops' => ['like','eq']]],
['label' => $t('settings.sources.th_type', 'Type'), 'i18n' => 'settings.sources.th_type', 'sort' => 'type', 'key' => 'type', 'filter' => ['type' => 'text', 'ops' => ['like','eq']]],
['label' => $t('settings.sources.th_path', 'Path'), 'i18n' => 'settings.sources.th_path', 'sort' => 'path', 'key' => 'path', 'filter' => ['type' => 'text', 'ops' => ['like','eq','empty']]],
['label' => $t('settings.sources.th_status', 'Status'), 'i18n' => 'settings.sources.th_status', 'sort' => 'status', 'key' => 'status', 'filter' => ['type' => 'text', 'ops' => ['like','eq']]],
['label' => '', 'i18n' => '', 'key' => '', 'filter' => null],
];
$attrs = ['data-table' => 'roots'];
@ -192,7 +116,8 @@ $toolbarRightHtml = '';
$headers = [
['label' => $t('settings.tasks.th_name', 'Task'), 'i18n' => 'settings.tasks.th_name', 'sort' => 'name', 'key' => 'name', 'filter' => ['type' => 'text', 'ops' => ['like','eq','empty']]],
['label' => $t('settings.tasks.th_sources', 'Sources'), 'i18n' => 'settings.tasks.th_sources', 'key' => 'sources', 'filter' => ['type' => 'text', 'ops' => ['like','eq','empty']]],
['label' => $t('settings.tasks.th_actions', 'Actions'), 'i18n' => 'settings.tasks.th_actions', 'key' => 'actions', 'filter' => ['type' => 'text', 'ops' => ['like','eq','empty']]],
['label' => $t('settings.tasks.th_rules', 'Rules'), 'i18n' => 'settings.tasks.th_rules', 'key' => 'rules', 'filter' => ['type' => 'text', 'ops' => ['like','eq','empty']]],
['label' => $t('settings.tasks.th_template', 'Template'), 'i18n' => 'settings.tasks.th_template', 'key' => 'template', 'filter' => ['type' => 'text', 'ops' => ['like','eq','empty']]],
['label' => $t('settings.tasks.th_status', 'Status'), 'i18n' => 'settings.tasks.th_status', 'key' => 'status', 'filter' => ['type' => 'text', 'ops' => ['like','eq']]],
['label' => '', 'i18n' => '', 'key' => '', 'filter' => null],
];
@ -207,26 +132,7 @@ $toolbarRightHtml = '';
<div class="tabpane" id="pane-rules">
<div class="pane-header">
<h2 data-i18n="settings.rules.title"><?php echo htmlspecialchars($t('settings.rules.title', 'Rules'), ENT_QUOTES); ?></h2>
<div class="row">
<div class="dropdown">
<button class="btn primary" id="btnAddRule" data-i18n="settings.rules.add_rule"><?php echo htmlspecialchars($t('settings.rules.add_rule', 'Add rule'), ENT_QUOTES); ?></button>
<div class="dropdown-menu" id="ruleTypeMenu">
<button type="button" data-rule-type="name_map" data-i18n="rules.type.name_map"><?php echo htmlspecialchars($t('rules.type.name_map', 'Name mapping'), ENT_QUOTES); ?></button>
<button type="button" data-rule-type="delete_track" data-i18n="rules.type.delete_track"><?php echo htmlspecialchars($t('rules.type.delete_track', 'Delete tracks'), ENT_QUOTES); ?></button>
<button type="button" data-rule-type="priorities" data-i18n="rules.type.priorities"><?php echo htmlspecialchars($t('rules.type.priorities', 'Priorities'), ENT_QUOTES); ?></button>
<button type="button" data-rule-type="lang_fix" data-i18n="rules.type.lang_fix"><?php echo htmlspecialchars($t('rules.type.lang_fix', 'Language fix'), ENT_QUOTES); ?></button>
<button type="button" data-rule-type="source_filter" data-i18n="rules.type.source_filter"><?php echo htmlspecialchars($t('rules.type.source_filter', 'Source filter'), ENT_QUOTES); ?></button>
</div>
</div>
<label class="rule-sort">
<span class="lbl" data-i18n="rules.sort_by"><?php echo htmlspecialchars($t('rules.sort_by', 'Sort by'), ENT_QUOTES); ?></span>
<select id="rulesSortField">
<option value="name" data-i18n="rules.sort.name"><?php echo htmlspecialchars($t('rules.sort.name', 'Name'), ENT_QUOTES); ?></option>
<option value="type" data-i18n="rules.sort.type"><?php echo htmlspecialchars($t('rules.sort.type', 'Type'), ENT_QUOTES); ?></option>
</select>
<button class="btn" id="rulesSortDir" type="button"></button>
</label>
</div>
<button class="btn primary" id="btnAddRule" data-i18n="settings.rules.add_rule"><?php echo htmlspecialchars($t('settings.rules.add_rule', 'Add rule'), ENT_QUOTES); ?></button>
</div>
<div class="card">
@ -235,8 +141,8 @@ $toolbarRightHtml = '';
$tableClass = 'data-table';
$headers = [
['label' => $t('rules.th.name', 'Name'), 'i18n' => 'rules.th.name', 'sort' => 'name', 'key' => 'name', 'filter' => ['type' => 'text', 'ops' => ['like','eq','empty']]],
['label' => $t('rules.th.type', 'Type'), 'i18n' => 'rules.th.type', 'sort' => 'type', 'key' => 'type', 'filter' => ['type' => 'text', 'ops' => ['like','eq']]],
['label' => $t('rules.th.summary', 'Summary'), 'i18n' => 'rules.th.summary', 'key' => 'summary', 'filter' => ['type' => 'text', 'ops' => ['like','eq','empty']]],
['label' => $t('rules.th.tags', 'Tags'), 'i18n' => 'rules.th.tags', 'key' => 'tags', 'filter' => ['type' => 'text', 'ops' => ['like','eq','empty']]],
['label' => $t('rules.th.conditions', 'Conditions'), 'i18n' => 'rules.th.conditions', 'key' => 'conditions', 'filter' => ['type' => 'text', 'ops' => ['like','eq','empty']]],
['label' => $t('rules.th.status', 'Status'), 'i18n' => 'rules.th.status', 'key' => 'status', 'filter' => ['type' => 'text', 'ops' => ['like','eq']]],
['label' => '', 'i18n' => '', 'key' => '', 'filter' => null],
];
@ -246,8 +152,31 @@ $toolbarRightHtml = '';
</div>
</div>
<!-- ===================== TEMPLATES TAB ===================== -->
<div class="tabpane" id="pane-templates">
<div class="pane-header">
<h2 data-i18n="settings.templates.title"><?php echo htmlspecialchars($t('settings.templates.title', 'Templates'), ENT_QUOTES); ?></h2>
<button id="btnAddTemplate" class="btn primary" data-i18n="settings.templates.add"><?php echo htmlspecialchars($t('settings.templates.add', 'Add template'), ENT_QUOTES); ?></button>
</div>
<div class="card">
<?php
$id = 'templatesTable';
$tableClass = 'data-table';
$headers = [
['label' => $t('settings.templates.th_name', 'Template'), 'i18n' => 'settings.templates.th_name', 'sort' => 'name', 'key' => 'name', 'filter' => ['type' => 'text', 'ops' => ['like','eq','empty']]],
['label' => $t('settings.templates.th_action', 'Action'), 'i18n' => 'settings.templates.th_action', 'key' => 'action', 'filter' => ['type' => 'text', 'ops' => ['like','eq','empty']]],
['label' => $t('settings.templates.th_status', 'Status'), 'i18n' => 'settings.templates.th_status', 'key' => 'status', 'filter' => ['type' => 'text', 'ops' => ['like','eq']]],
['label' => '', 'i18n' => '', 'key' => '', 'filter' => null],
];
$attrs = ['data-table' => 'templates'];
require __DIR__ . '/../partials/table.php';
?>
<div class="hint" id="templatesHint"></div>
</div>
</div>
<!-- ===================== SERVER TAB ===================== -->
<div class="tabpane" id="pane-server">
<div class="tabpane active" id="pane-server">
<div class="pane-header">
<h2 data-i18n="settings.server.title"><?php echo htmlspecialchars($t('settings.server.title', 'Server'), ENT_QUOTES); ?></h2>
</div>
@ -391,6 +320,41 @@ $toolbarRightHtml = '';
<div class="card">
<h3 data-i18n="settings.server.danger"><?php echo htmlspecialchars($t('settings.server.danger', 'Danger zone'), ENT_QUOTES); ?></h3>
</div>
<div class="card">
<h3 data-i18n="settings.about.title"><?php echo htmlspecialchars($t('settings.about.title', 'About'), ENT_QUOTES); ?></h3>
<div class="about-card" data-version>
<div class="about-row">
<img class="about-logo" src="/assets/icons/scmedia.png" alt="scMedia">
<div class="about-body">
<div class="about-title">
<a class="about-link" href="https://safe-cap.com/software/scMedia">scMedia</a>
<span class="version">v0.0.0</span>
</div>
<div class="about-meta">
<span class="about-company">SAFE-CAP</span>
<a class="about-link muted" href="https://safe-cap.com/">safe-cap.com</a>
</div>
</div>
</div>
</div>
</div>
<div class="card">
<?php
$id = 'aboutPluginsTable';
$tableClass = 'table';
$headers = [
['label' => $t('settings.about.table_name', 'Name'), 'i18n' => 'settings.about.table_name'],
['label' => $t('settings.about.table_type', 'Type'), 'i18n' => 'settings.about.table_type'],
['label' => $t('settings.about.table_author', 'Author'), 'i18n' => 'settings.about.table_author'],
['label' => $t('settings.about.table_version', 'Version'), 'i18n' => 'settings.about.table_version'],
['label' => $t('settings.about.table_update', 'Update'), 'i18n' => 'settings.about.table_update'],
];
$footer = false;
require __DIR__ . '/../partials/table.php';
?>
</div>
</div>
<!-- ===================== TOOLS TAB ===================== -->
@ -657,102 +621,9 @@ $toolbarRightHtml = '';
</div>
</div>
<!-- ===================== ABOUT TAB ===================== -->
<div class="tabpane" id="pane-about">
<div class="pane-header">
<h2 data-i18n="settings.about.title"><?php echo htmlspecialchars($t('settings.about.title', 'About'), ENT_QUOTES); ?></h2>
</div>
<div class="card about-card" data-version>
<div class="about-row">
<img class="about-logo" src="/assets/icons/scmedia.png" alt="scMedia">
<div class="about-body">
<div class="about-title">
<a class="about-link" href="https://safe-cap.com/software/scMedia">scMedia</a>
<span class="version">v0.0.0</span>
</div>
<div class="about-meta">
<span class="about-company">SAFE-CAP</span>
<a class="about-link muted" href="https://safe-cap.com/">safe-cap.com</a>
</div>
</div>
</div>
</div>
<div class="card">
<?php
$id = 'aboutPluginsTable';
$tableClass = 'table';
$headers = [
['label' => $t('settings.about.table_name', 'Name'), 'i18n' => 'settings.about.table_name'],
['label' => $t('settings.about.table_type', 'Type'), 'i18n' => 'settings.about.table_type'],
['label' => $t('settings.about.table_author', 'Author'), 'i18n' => 'settings.about.table_author'],
['label' => $t('settings.about.table_version', 'Version'), 'i18n' => 'settings.about.table_version'],
['label' => $t('settings.about.table_update', 'Update'), 'i18n' => 'settings.about.table_update'],
];
$footer = false;
require __DIR__ . '/../partials/table.php';
?>
</div>
</div>
</section>
</main>
<div id="modal" class="modal" style="display:none;">
<div class="modal-card">
<div class="modal-head">
<div class="modal-title" id="modalTitle"><?php echo htmlspecialchars($t('settings.scan_profiles.modal_edit', 'Edit profile'), ENT_QUOTES); ?></div>
<button id="modalClose" class="btn" data-i18n="common.close"><?php echo htmlspecialchars($t('common.close', 'Close'), ENT_QUOTES); ?></button>
</div>
<div class="modal-body">
<div class="grid">
<label>
<div class="lbl" data-i18n="settings.scan_profiles.modal_name"><?php echo htmlspecialchars($t('settings.scan_profiles.modal_name', 'Name'), ENT_QUOTES); ?></div>
<input id="pName" type="text">
</label>
<label>
<div class="lbl" data-i18n="settings.scan_profiles.modal_root"><?php echo htmlspecialchars($t('settings.scan_profiles.modal_root', 'Root path'), ENT_QUOTES); ?></div>
<input id="pRoot" type="text">
</label>
<label>
<div class="lbl" data-i18n="settings.scan_profiles.modal_depth"><?php echo htmlspecialchars($t('settings.scan_profiles.modal_depth', 'Max depth'), ENT_QUOTES); ?></div>
<input id="pDepth" type="number" min="1" max="10">
</label>
<label>
<div class="lbl" data-i18n="settings.scan_profiles.modal_type"><?php echo htmlspecialchars($t('settings.scan_profiles.modal_type', 'Profile type'), ENT_QUOTES); ?></div>
<select id="pType">
<option value="scan" data-i18n="settings.scan_profiles.type_scan"><?php echo htmlspecialchars($t('settings.scan_profiles.type_scan', 'Scan'), ENT_QUOTES); ?></option>
<option value="analyze" data-i18n="settings.scan_profiles.type_analyze"><?php echo htmlspecialchars($t('settings.scan_profiles.type_analyze', 'Analyze'), ENT_QUOTES); ?></option>
</select>
</label>
<label>
<div class="lbl" data-i18n="settings.scan_profiles.modal_enabled"><?php echo htmlspecialchars($t('settings.scan_profiles.modal_enabled', 'Enabled'), ENT_QUOTES); ?></div>
<select id="pEnabled">
<option value="1" data-i18n="common.yes"><?php echo htmlspecialchars($t('common.yes', 'Yes'), ENT_QUOTES); ?></option>
<option value="0" data-i18n="common.no"><?php echo htmlspecialchars($t('common.no', 'No'), ENT_QUOTES); ?></option>
</select>
</label>
<label>
<div class="lbl" data-i18n="settings.scan_profiles.modal_excludes"><?php echo htmlspecialchars($t('settings.scan_profiles.modal_excludes', 'Exclude patterns'), ENT_QUOTES); ?></div>
<input id="pExcludes" type="text">
</label>
<label>
<div class="lbl" data-i18n="settings.scan_profiles.modal_ext_mode"><?php echo htmlspecialchars($t('settings.scan_profiles.modal_ext_mode', 'Include ext mode'), ENT_QUOTES); ?></div>
<select id="pExtMode">
<option value="default" data-i18n="settings.scan_profiles.ext_default"><?php echo htmlspecialchars($t('settings.scan_profiles.ext_default', 'default'), ENT_QUOTES); ?></option>
<option value="custom" data-i18n="settings.scan_profiles.ext_custom"><?php echo htmlspecialchars($t('settings.scan_profiles.ext_custom', 'custom'), ENT_QUOTES); ?></option>
</select>
</label>
<label id="pExtCustomWrap" style="display:none;">
<div class="lbl" data-i18n="settings.scan_profiles.modal_ext_custom"><?php echo htmlspecialchars($t('settings.scan_profiles.modal_ext_custom', 'Custom extensions'), ENT_QUOTES); ?></div>
<input id="pExtCustom" type="text">
</label>
</div>
</div>
<div class="modal-foot">
<button id="modalSave" class="btn primary" data-i18n="common.save"><?php echo htmlspecialchars($t('common.save', 'Save'), ENT_QUOTES); ?></button>
</div>
</div>
</div>
<div id="ruleModal" class="modal" style="display:none;">
<div class="modal-card">
<div class="modal-head">

View File

@ -204,6 +204,8 @@ input, select {
z-index: 20;
display: none;
min-width: 200px;
max-height: 60vh;
overflow-y: auto;
margin-top: 6px;
padding: 6px;
border: 1px solid var(--border);
@ -448,6 +450,9 @@ input, select {
border: 1px solid var(--border);
border-radius: 16px;
overflow: hidden;
display: flex;
flex-direction: column;
max-height: 90vh;
}
.modal-head, .modal-foot {
@ -470,6 +475,21 @@ input, select {
.modal-body {
padding: 12px;
overflow: auto;
overflow-x: hidden;
}
.root-row {
display: grid;
grid-template-columns: 140px minmax(240px, 1fr) 140px;
gap: 10px;
align-items: end;
}
.root-row label {
display: flex;
flex-direction: column;
gap: 6px;
}
@media (max-width: 980px) {

View File

@ -78,11 +78,16 @@
"settings.tasks.modal_title": "Aufgabe",
"settings.tasks.field_name": "Name",
"settings.tasks.field_sources": "Quellen",
"settings.tasks.field_actions": "Aktionen",
"settings.tasks.field_rules": "Regeln",
"settings.tasks.field_template": "Vorlage",
"settings.tasks.field_enabled": "Aktiv",
"settings.tasks.source.library": "Bibliothek",
"settings.tasks.hint": "Quellen und Regeln bestimmen die Daten, die Vorlage definiert die Aktion.",
"settings.tasks.source.movie": "Filme",
"settings.tasks.source.series": "Serien",
"settings.tasks.source.transmission": "Transmission",
"settings.tasks.source.staging": "Staging",
"settings.tasks.rules_empty": "Noch keine Regeln",
"settings.tasks.template_none": "Keine Vorlage",
"settings.tasks.action.analyze": "Analyse",
"settings.tasks.action.identify": "Identifizieren",
"settings.tasks.action.normalize": "Normalisieren",
@ -146,8 +151,10 @@
"settings.title": "Einstellungen",
"settings.back": "Zurück",
"settings.tabs.scan_profiles": "Scan-Profile",
"settings.tabs.library_layout": "Bibliothek",
"settings.tabs.library": "Bibliothek",
"settings.tabs.sources": "Quellen",
"settings.tabs.plugins": "Plugins",
"settings.tabs.templates": "Vorlagen",
"settings.tabs.tasks": "Aufgaben",
"settings.tabs.rules": "Regeln",
"settings.tabs.tools": "Programme",
@ -162,15 +169,34 @@
"settings.scan_profiles.confirm_delete": "Profil löschen?",
"settings.scan_profiles.ext_default": "Standard",
"settings.scanner_defaults.title": "Globale Scanner-Defaults",
"settings.library_layout.title": "Library",
"settings.library_layout.preview": "Vorschau",
"settings.library_layout.roots": "Ordner",
"settings.library_layout.add_root": "Hinzufuegen",
"settings.library_layout.th_type": "Typ",
"settings.library_layout.th_path": "Pfad",
"settings.library_layout.th_status": "Status",
"settings.library_layout.modal_title": "Ordner",
"settings.library_layout.confirm_delete": "Ordner löschen?",
"settings.library.title": "Library",
"settings.library.preview": "Vorschau",
"settings.library.roots": "Ordner",
"settings.library.add_root": "Hinzufuegen",
"settings.library.th_type": "Typ",
"settings.library.th_path": "Pfad",
"settings.library.th_status": "Status",
"settings.library.modal_title": "Ordner",
"settings.library.confirm_delete": "Ordner löschen?",
"settings.sources.title": "Quellen",
"settings.sources.add_root": "Quelle hinzufügen",
"settings.sources.th_type": "Typ",
"settings.sources.th_path": "Pfad",
"settings.sources.th_status": "Status",
"settings.sources.modal_title": "Quelle",
"settings.sources.confirm_delete": "Quelle löschen?",
"settings.templates.title": "Vorlagen",
"settings.templates.add": "Vorlage hinzufügen",
"settings.templates.th_name": "Vorlage",
"settings.templates.th_action": "Aktion",
"settings.templates.th_status": "Status",
"settings.templates.modal_title": "Vorlage",
"settings.templates.field_name": "Name",
"settings.templates.field_action": "Aktion",
"settings.templates.field_enabled": "Aktiv",
"settings.templates.hint": "Beschreibe die Aktion (Mapping, Konvertieren, Normalisieren, Export).",
"settings.templates.confirm_delete": "Vorlage löschen?",
"settings.templates.unnamed": "Ohne Namen",
"root.type.movie": "Filme",
"root.type.series": "Serien",
"root.type.staging": "Staging",
@ -332,21 +358,21 @@
"settings.scanner_defaults.max_files": "Max. Dateien pro Element",
"settings.scanner_defaults.max_items": "Max. Elemente pro Scan (0 = ohne Limit)",
"settings.library_layout.movies_root": "Filme-Ordner",
"settings.library_layout.movies_root_ph": "/mnt/media/library/movies",
"settings.library_layout.series_root": "Serien-Ordner",
"settings.library_layout.series_root_ph": "/mnt/media/library/series",
"settings.library_layout.staging_root": "Staging (optional)",
"settings.library_layout.staging_root_ph": "/mnt/media/.staging",
"settings.library.movies_root": "Filme-Ordner",
"settings.library.movies_root_ph": "/mnt/media/library/movies",
"settings.library.series_root": "Serien-Ordner",
"settings.library.series_root_ph": "/mnt/media/library/series",
"settings.library.staging_root": "Staging (optional)",
"settings.library.staging_root_ph": "/mnt/media/.staging",
"settings.library_layout.movies_strategy_title": "Filme-Strategie",
"settings.library_layout.series_strategy_title": "Serien-Strategie",
"settings.library_layout.strategy": "Strategie",
"settings.library_layout.season_naming": "Staffelbezeichnung",
"settings.library_layout.normalization_title": "Normalisierung",
"settings.library_layout.collision_title": "Kollisionsregel",
"settings.library_layout.preview_title": "Vorschau",
"settings.library_layout.preview_hint": "Klicke auf „Vorschau erzeugen“. ",
"settings.library.movies_strategy_title": "Filme-Strategie",
"settings.library.series_strategy_title": "Serien-Strategie",
"settings.library.strategy": "Strategie",
"settings.library.season_naming": "Staffelbezeichnung",
"settings.library.normalization_title": "Normalisierung",
"settings.library.collision_title": "Kollisionsregel",
"settings.library.preview_title": "Vorschau",
"settings.library.preview_hint": "Klicke auf „Vorschau erzeugen“. ",
"settings.strategy.flat": "Flach",
"settings.strategy.first_letter": "Nach erstem Buchstaben",
@ -511,7 +537,8 @@
"settings.tasks.add": "Aufgabe hinzufugen",
"settings.tasks.th_name": "Aufgabe",
"settings.tasks.th_sources": "Quellen",
"settings.tasks.th_actions": "Aktionen",
"settings.tasks.th_rules": "Regeln",
"settings.tasks.th_template": "Vorlage",
"settings.tasks.th_status": "Status",
"auth.page_title": "scMedia / Login",

View File

@ -78,11 +78,16 @@
"settings.tasks.modal_title": "Task",
"settings.tasks.field_name": "Name",
"settings.tasks.field_sources": "Sources",
"settings.tasks.field_actions": "Actions",
"settings.tasks.field_rules": "Rules",
"settings.tasks.field_template": "Template",
"settings.tasks.field_enabled": "Enabled",
"settings.tasks.source.library": "Library",
"settings.tasks.hint": "Sources + rules choose what to process. Template defines the action pipeline.",
"settings.tasks.source.movie": "Movies",
"settings.tasks.source.series": "Series",
"settings.tasks.source.transmission": "Transmission",
"settings.tasks.source.staging": "Staging",
"settings.tasks.rules_empty": "No rules yet",
"settings.tasks.template_none": "No template",
"settings.tasks.action.analyze": "Analyze",
"settings.tasks.action.identify": "Identify",
"settings.tasks.action.normalize": "Normalize",
@ -146,8 +151,10 @@
"settings.title": "Settings",
"settings.back": "Back",
"settings.tabs.scan_profiles": "Scan Profiles",
"settings.tabs.library_layout": "Library",
"settings.tabs.library": "Library",
"settings.tabs.sources": "Sources",
"settings.tabs.plugins": "Plugins",
"settings.tabs.templates": "Templates",
"settings.tabs.tasks": "Tasks",
"settings.tabs.rules": "Rules",
"settings.tabs.tools": "Programs",
@ -162,15 +169,34 @@
"settings.scan_profiles.confirm_delete": "Delete profile?",
"settings.scan_profiles.ext_default": "default",
"settings.scanner_defaults.title": "Global scanner defaults",
"settings.library_layout.title": "Library",
"settings.library_layout.preview": "Preview",
"settings.library_layout.roots": "Folders",
"settings.library_layout.add_root": "Add",
"settings.library_layout.th_type": "Type",
"settings.library_layout.th_path": "Path",
"settings.library_layout.th_status": "Status",
"settings.library_layout.modal_title": "Root",
"settings.library_layout.confirm_delete": "Delete root?",
"settings.library.title": "Library",
"settings.library.preview": "Preview",
"settings.library.roots": "Folders",
"settings.library.add_root": "Add",
"settings.library.th_type": "Type",
"settings.library.th_path": "Path",
"settings.library.th_status": "Status",
"settings.library.modal_title": "Root",
"settings.library.confirm_delete": "Delete root?",
"settings.sources.title": "Sources",
"settings.sources.add_root": "Add source",
"settings.sources.th_type": "Type",
"settings.sources.th_path": "Path",
"settings.sources.th_status": "Status",
"settings.sources.modal_title": "Source",
"settings.sources.confirm_delete": "Delete source?",
"settings.templates.title": "Templates",
"settings.templates.add": "Add template",
"settings.templates.th_name": "Template",
"settings.templates.th_action": "Action",
"settings.templates.th_status": "Status",
"settings.templates.modal_title": "Template",
"settings.templates.field_name": "Name",
"settings.templates.field_action": "Action",
"settings.templates.field_enabled": "Enabled",
"settings.templates.hint": "Describe the action pipeline (mapping, convert, normalize, export).",
"settings.templates.confirm_delete": "Delete template?",
"settings.templates.unnamed": "Untitled",
"root.type.movie": "Movie",
"root.type.series": "Series",
"root.type.staging": "Staging",
@ -332,21 +358,21 @@
"settings.scanner_defaults.max_files": "Max files per item",
"settings.scanner_defaults.max_items": "Max items per scan (0 = no limit)",
"settings.library_layout.movies_root": "Movies root",
"settings.library_layout.movies_root_ph": "/mnt/media/library/movies",
"settings.library_layout.series_root": "Series root",
"settings.library_layout.series_root_ph": "/mnt/media/library/series",
"settings.library_layout.staging_root": "Staging root (optional)",
"settings.library_layout.staging_root_ph": "/mnt/media/.staging",
"settings.library.movies_root": "Movies root",
"settings.library.movies_root_ph": "/mnt/media/library/movies",
"settings.library.series_root": "Series root",
"settings.library.series_root_ph": "/mnt/media/library/series",
"settings.library.staging_root": "Staging root (optional)",
"settings.library.staging_root_ph": "/mnt/media/.staging",
"settings.library_layout.movies_strategy_title": "Movies strategy",
"settings.library_layout.series_strategy_title": "Series strategy",
"settings.library_layout.strategy": "Strategy",
"settings.library_layout.season_naming": "Season naming",
"settings.library_layout.normalization_title": "Normalization",
"settings.library_layout.collision_title": "Collision policy",
"settings.library_layout.preview_title": "Preview",
"settings.library_layout.preview_hint": "Click \"Generate preview\".",
"settings.library.movies_strategy_title": "Movies strategy",
"settings.library.series_strategy_title": "Series strategy",
"settings.library.strategy": "Strategy",
"settings.library.season_naming": "Season naming",
"settings.library.normalization_title": "Normalization",
"settings.library.collision_title": "Collision policy",
"settings.library.preview_title": "Preview",
"settings.library.preview_hint": "Click \"Generate preview\".",
"settings.strategy.flat": "Flat",
"settings.strategy.first_letter": "By first letter",
@ -511,7 +537,8 @@
"settings.tasks.add": "Add task",
"settings.tasks.th_name": "Task",
"settings.tasks.th_sources": "Sources",
"settings.tasks.th_actions": "Actions",
"settings.tasks.th_rules": "Rules",
"settings.tasks.th_template": "Template",
"settings.tasks.th_status": "Status",
"auth.page_title": "scMedia / Login",

View File

@ -78,11 +78,16 @@
"settings.tasks.modal_title": "Задача",
"settings.tasks.field_name": "Название",
"settings.tasks.field_sources": "Источники",
"settings.tasks.field_actions": "Действия",
"settings.tasks.field_rules": "Правила",
"settings.tasks.field_template": "Шаблон",
"settings.tasks.field_enabled": "Включена",
"settings.tasks.source.library": "Библиотека",
"settings.tasks.hint": "Источники и правила выбирают данные, шаблон задает цепочку действий.",
"settings.tasks.source.movie": "Фильмы",
"settings.tasks.source.series": "Сериалы",
"settings.tasks.source.transmission": "Transmission",
"settings.tasks.source.staging": "Staging",
"settings.tasks.rules_empty": "Правил пока нет",
"settings.tasks.template_none": "Без шаблона",
"settings.tasks.action.analyze": "Анализ",
"settings.tasks.action.identify": "Поиск",
"settings.tasks.action.normalize": "Нормализация",
@ -146,8 +151,10 @@
"settings.title": "Настройки",
"settings.back": "Назад",
"settings.tabs.scan_profiles": "Профили сканирования",
"settings.tabs.library_layout": "Библиотека",
"settings.tabs.library": "Библиотека",
"settings.tabs.sources": "Источники",
"settings.tabs.plugins": "Плагины",
"settings.tabs.templates": "Шаблоны",
"settings.tabs.tasks": "Задачи",
"settings.tabs.rules": "Правила",
"settings.tabs.tools": "Программы",
@ -162,15 +169,34 @@
"settings.scan_profiles.confirm_delete": "Удалить профиль?",
"settings.scan_profiles.ext_default": "по умолчанию",
"settings.scanner_defaults.title": "Глобальные настройки сканера",
"settings.library_layout.title": "Библиотека",
"settings.library_layout.preview": "Превью",
"settings.library_layout.roots": "Папки",
"settings.library_layout.add_root": "Добавить",
"settings.library_layout.th_type": "Тип",
"settings.library_layout.th_path": "Путь",
"settings.library_layout.th_status": "Статус",
"settings.library_layout.modal_title": "Папка",
"settings.library_layout.confirm_delete": "Удалить папку?",
"settings.library.title": "Библиотека",
"settings.library.preview": "Превью",
"settings.library.roots": "Папки",
"settings.library.add_root": "Добавить",
"settings.library.th_type": "Тип",
"settings.library.th_path": "Путь",
"settings.library.th_status": "Статус",
"settings.library.modal_title": "Папка",
"settings.library.confirm_delete": "Удалить папку?",
"settings.sources.title": "Источники",
"settings.sources.add_root": "Добавить источник",
"settings.sources.th_type": "Тип",
"settings.sources.th_path": "Путь",
"settings.sources.th_status": "Статус",
"settings.sources.modal_title": "Источник",
"settings.sources.confirm_delete": "Удалить источник?",
"settings.templates.title": "Шаблоны",
"settings.templates.add": "Добавить шаблон",
"settings.templates.th_name": "Шаблон",
"settings.templates.th_action": "Действие",
"settings.templates.th_status": "Статус",
"settings.templates.modal_title": "Шаблон",
"settings.templates.field_name": "Название",
"settings.templates.field_action": "Действие",
"settings.templates.field_enabled": "Включен",
"settings.templates.hint": "Опиши цепочку действий (маппинг, конвертация, нормализация, экспорт).",
"settings.templates.confirm_delete": "Удалить шаблон?",
"settings.templates.unnamed": "Без названия",
"root.type.movie": "Фильмы",
"root.type.series": "Сериалы",
"root.type.staging": "Стейджинг",
@ -334,21 +360,21 @@
"settings.scanner_defaults.max_files": "Макс. файлов на элемент",
"settings.scanner_defaults.max_items": "Макс. элементов за скан (0 = без лимита)",
"settings.library_layout.movies_root": "Папка фильмов",
"settings.library_layout.movies_root_ph": "/mnt/media/library/movies",
"settings.library_layout.series_root": "Папка сериалов",
"settings.library_layout.series_root_ph": "/mnt/media/library/series",
"settings.library_layout.staging_root": "Staging (опционально)",
"settings.library_layout.staging_root_ph": "/mnt/media/.staging",
"settings.library.movies_root": "Папка фильмов",
"settings.library.movies_root_ph": "/mnt/media/library/movies",
"settings.library.series_root": "Папка сериалов",
"settings.library.series_root_ph": "/mnt/media/library/series",
"settings.library.staging_root": "Staging (опционально)",
"settings.library.staging_root_ph": "/mnt/media/.staging",
"settings.library_layout.movies_strategy_title": "Стратегия фильмов",
"settings.library_layout.series_strategy_title": "Стратегия сериалов",
"settings.library_layout.strategy": "Стратегия",
"settings.library_layout.season_naming": "Название сезона",
"settings.library_layout.normalization_title": "Нормализация",
"settings.library_layout.collision_title": "Политика коллизий",
"settings.library_layout.preview_title": "Превью",
"settings.library_layout.preview_hint": "Нажмите «Сгенерировать превью».",
"settings.library.movies_strategy_title": "Стратегия фильмов",
"settings.library.series_strategy_title": "Стратегия сериалов",
"settings.library.strategy": "Стратегия",
"settings.library.season_naming": "Название сезона",
"settings.library.normalization_title": "Нормализация",
"settings.library.collision_title": "Политика коллизий",
"settings.library.preview_title": "Превью",
"settings.library.preview_hint": "Нажмите «Сгенерировать превью».",
"settings.strategy.flat": "Плоско",
"settings.strategy.first_letter": "По первой букве",
@ -513,7 +539,8 @@
"settings.tasks.add": "Добавить задачу",
"settings.tasks.th_name": "Задача",
"settings.tasks.th_sources": "Источники",
"settings.tasks.th_actions": "Действия",
"settings.tasks.th_rules": "Правила",
"settings.tasks.th_template": "Шаблон",
"settings.tasks.th_status": "Статус",
"auth.page_title": "scMedia / Вход",

View File

@ -90,9 +90,6 @@
if (ui.theme && window.UI?.setTheme) {
window.UI.setTheme(ui.theme);
}
if (ui.language && ui.language !== (window.APP_LANG || 'en')) {
localStorage.setItem('scmedia_lang', ui.language);
}
}
async function uploadAvatar() {

View File

@ -1369,7 +1369,7 @@ function initEventsPipe() {
initSourcesPolling();
return;
}
document.cookie = `sse_key=${encodeURIComponent(key)}; path=/; max-age=${Math.max(10, ttl)}`;
document.cookie = `scmedia_sse_key=${encodeURIComponent(key)}; path=/; max-age=${Math.max(10, ttl)}`;
await new Promise((r) => setTimeout(r, 50));
const es = new EventSource('/api/events');
state.eventsSource = es;
@ -1643,9 +1643,6 @@ function init() {
if (prefs?.theme && window.UI?.setTheme) {
window.UI.setTheme(prefs.theme);
}
if (prefs?.language && prefs.language !== state.lang) {
localStorage.setItem('scmedia_lang', prefs.language);
}
}).catch(() => {});
}
UI.initThemeToggle();

View File

@ -2,14 +2,24 @@
/* English comments: auth token helpers and login UI */
(function () {
const LS_ACCESS = 'scmedia_access_token';
const LS_REFRESH = 'scmedia_refresh_token';
const LS_CLIENT = 'scmedia_client_type';
const ACCESS_COOKIE = 'scmedia_access_token';
let refreshPromise = null;
let refreshTimer = null;
function getAccessToken() {
return localStorage.getItem(LS_ACCESS) || sessionStorage.getItem(LS_ACCESS) || '';
const parts = document.cookie.split(';').map((p) => p.trim());
for (const part of parts) {
if (!part) continue;
const idx = part.indexOf('=');
if (idx === -1) continue;
const key = part.slice(0, idx);
if (key === ACCESS_COOKIE) {
return decodeURIComponent(part.slice(idx + 1));
}
}
return '';
}
function getRefreshToken() {
@ -22,22 +32,22 @@
function setTokens(tokens, clientType = 'web', remember = true) {
const storage = remember ? localStorage : sessionStorage;
if (tokens?.access_token) storage.setItem(LS_ACCESS, tokens.access_token);
if (tokens?.refresh_token) storage.setItem(LS_REFRESH, tokens.refresh_token);
storage.setItem(LS_CLIENT, clientType);
const other = remember ? sessionStorage : localStorage;
other.removeItem(LS_ACCESS);
other.removeItem(LS_REFRESH);
other.removeItem(LS_CLIENT);
localStorage.removeItem('scmedia_access_token');
sessionStorage.removeItem('scmedia_access_token');
}
function clearTokens() {
localStorage.removeItem(LS_ACCESS);
localStorage.removeItem(LS_REFRESH);
localStorage.removeItem(LS_CLIENT);
sessionStorage.removeItem(LS_ACCESS);
sessionStorage.removeItem(LS_REFRESH);
sessionStorage.removeItem(LS_CLIENT);
localStorage.removeItem('scmedia_access_token');
sessionStorage.removeItem('scmedia_access_token');
}
function parseJwt(token) {

File diff suppressed because it is too large Load Diff

View File

@ -202,7 +202,7 @@
localStorage.removeItem(SSE_LAST_TYPE);
localStorage.removeItem(SSE_LEASE_KEY);
sessionStorage.removeItem(SSE_KEY_EXP);
document.cookie = 'sse_key=; path=/; max-age=0';
document.cookie = 'scmedia_sse_key=; path=/; max-age=0';
}
window.Sse = { start, stop, on };

View File

@ -2,7 +2,7 @@
/* English comments: shared DOM + UI helpers */
(function () {
const LS_THEME = 'scmedia_theme';
const THEME_COOKIE = 'scmedia_theme';
let themeToggleReady = false;
let queueState = { active: [], recent: [] };
let sseOpening = false;
@ -21,16 +21,33 @@
return Array.from(document.querySelectorAll(selector));
}
function getCookie(name) {
const parts = document.cookie.split(';').map((p) => p.trim());
for (const part of parts) {
if (!part) continue;
const idx = part.indexOf('=');
if (idx === -1) continue;
const key = part.slice(0, idx);
if (key === name) return decodeURIComponent(part.slice(idx + 1));
}
return '';
}
function setCookie(name, value, days = 365) {
const expires = new Date(Date.now() + days * 86400000).toUTCString();
document.cookie = `${name}=${encodeURIComponent(value)}; path=/; expires=${expires}; SameSite=Lax`;
}
function setTheme(theme) {
const mode = theme === 'light' ? 'light' : 'dark';
document.documentElement.setAttribute('data-theme', mode);
localStorage.setItem(LS_THEME, mode);
setCookie(THEME_COOKIE, mode);
}
function initThemeToggle() {
const btn = qs('themeToggle');
const saved = localStorage.getItem(LS_THEME) || 'dark';
const saved = getCookie(THEME_COOKIE) || 'dark';
setTheme(saved);
if (!btn || themeToggleReady) return;
btn.addEventListener('click', () => {