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') { if (!str_starts_with($path, '/assets') && $path !== '/favicon.ico') {
$lang = isset($_COOKIE['scmedia_lang']) ? (string)$_COOKIE['scmedia_lang'] : ''; $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'])) { if ($lang === '' && $token !== '' && isset($this->container->all()['auth'])) {
/** @var \ScMedia\Services\AuthService $auth */ /** @var \ScMedia\Services\AuthService $auth */
$auth = $this->container->all()['auth']; $auth = $this->container->all()['auth'];

View File

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

View File

@ -107,7 +107,7 @@ final class EventsController extends BaseController {
// Read and validate SSE key. // Read and validate SSE key.
private function readUserId(): ?int { 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 = trim($key, "\"'");
$key = urldecode($key); $key = urldecode($key);
$userId = $this->auth->validateSseKey($key); $userId = $this->auth->validateSseKey($key);

View File

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

View File

@ -47,6 +47,7 @@ final class SettingsService {
'layout' => $map['layout'] ?? [], 'layout' => $map['layout'] ?? [],
'media_rules' => $map['media_rules'] ?? [], 'media_rules' => $map['media_rules'] ?? [],
'rules' => $map['rules'] ?? [], 'rules' => $map['rules'] ?? [],
'templates' => $map['templates'] ?? [],
'sources' => $map['sources'] ?? [], 'sources' => $map['sources'] ?? [],
'metadata' => $map['metadata'] ?? [], 'metadata' => $map['metadata'] ?? [],
'exports' => $map['exports'] ?? [], 'exports' => $map['exports'] ?? [],
@ -67,7 +68,7 @@ final class SettingsService {
throw new Exception("Settings revision mismatch"); 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 = []; $toSave = [];
foreach ($allowed as $k) { foreach ($allowed as $k) {
if (array_key_exists($k, $payload)) { if (array_key_exists($k, $payload)) {

View File

@ -27,17 +27,16 @@ $toolbarRightHtml = '';
<main class="layout"> <main class="layout">
<nav class="tabs"> <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 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="layout" data-i18n="settings.tabs.library_layout"><?php echo htmlspecialchars($t('settings.tabs.library_layout', 'Library'), 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="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="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="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="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="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="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 <?php
$statusHidden = false; $statusHidden = false;
$statusClass = $statusHidden ? 'status hidden' : 'status'; $statusClass = $statusHidden ? 'status hidden' : 'status';
@ -52,86 +51,11 @@ $toolbarRightHtml = '';
</nav> </nav>
<section class="panel"> <section class="panel">
<!-- ===================== SCAN TAB ===================== --> <!-- ===================== SOURCES TAB ===================== -->
<div class="tabpane active" id="pane-scan"> <div class="tabpane" id="pane-sources">
<div class="pane-header"> <div class="pane-header">
<h2 data-i18n="settings.scan_profiles.title"><?php echo htmlspecialchars($t('settings.scan_profiles.title', 'Scan Profiles'), ENT_QUOTES); ?></h2> <h2 data-i18n="settings.sources.title"><?php echo htmlspecialchars($t('settings.sources.title', 'Sources'), 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> <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">
<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>
</div> </div>
<div class="card"> <div class="card">
@ -139,9 +63,9 @@ $toolbarRightHtml = '';
$id = 'rootsTable'; $id = 'rootsTable';
$tableClass = 'data-table'; $tableClass = 'data-table';
$headers = [ $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.sources.th_type', 'Type'), 'i18n' => 'settings.sources.th_type', 'sort' => '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.sources.th_path', 'Path'), 'i18n' => 'settings.sources.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_status', 'Status'), 'i18n' => 'settings.sources.th_status', 'sort' => 'status', 'key' => 'status', 'filter' => ['type' => 'text', 'ops' => ['like','eq']]],
['label' => '', 'i18n' => '', 'key' => '', 'filter' => null], ['label' => '', 'i18n' => '', 'key' => '', 'filter' => null],
]; ];
$attrs = ['data-table' => 'roots']; $attrs = ['data-table' => 'roots'];
@ -192,7 +116,8 @@ $toolbarRightHtml = '';
$headers = [ $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_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_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' => $t('settings.tasks.th_status', 'Status'), 'i18n' => 'settings.tasks.th_status', 'key' => 'status', 'filter' => ['type' => 'text', 'ops' => ['like','eq']]],
['label' => '', 'i18n' => '', 'key' => '', 'filter' => null], ['label' => '', 'i18n' => '', 'key' => '', 'filter' => null],
]; ];
@ -207,26 +132,7 @@ $toolbarRightHtml = '';
<div class="tabpane" id="pane-rules"> <div class="tabpane" id="pane-rules">
<div class="pane-header"> <div class="pane-header">
<h2 data-i18n="settings.rules.title"><?php echo htmlspecialchars($t('settings.rules.title', 'Rules'), ENT_QUOTES); ?></h2> <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> <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>
</div> </div>
<div class="card"> <div class="card">
@ -235,8 +141,8 @@ $toolbarRightHtml = '';
$tableClass = 'data-table'; $tableClass = 'data-table';
$headers = [ $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.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.tags', 'Tags'), 'i18n' => 'rules.th.tags', 'key' => 'tags', 'filter' => ['type' => 'text', 'ops' => ['like','eq','empty']]],
['label' => $t('rules.th.summary', 'Summary'), 'i18n' => 'rules.th.summary', 'key' => 'summary', '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' => $t('rules.th.status', 'Status'), 'i18n' => 'rules.th.status', 'key' => 'status', 'filter' => ['type' => 'text', 'ops' => ['like','eq']]],
['label' => '', 'i18n' => '', 'key' => '', 'filter' => null], ['label' => '', 'i18n' => '', 'key' => '', 'filter' => null],
]; ];
@ -246,8 +152,31 @@ $toolbarRightHtml = '';
</div> </div>
</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 ===================== --> <!-- ===================== SERVER TAB ===================== -->
<div class="tabpane" id="pane-server"> <div class="tabpane active" id="pane-server">
<div class="pane-header"> <div class="pane-header">
<h2 data-i18n="settings.server.title"><?php echo htmlspecialchars($t('settings.server.title', 'Server'), ENT_QUOTES); ?></h2> <h2 data-i18n="settings.server.title"><?php echo htmlspecialchars($t('settings.server.title', 'Server'), ENT_QUOTES); ?></h2>
</div> </div>
@ -391,6 +320,41 @@ $toolbarRightHtml = '';
<div class="card"> <div class="card">
<h3 data-i18n="settings.server.danger"><?php echo htmlspecialchars($t('settings.server.danger', 'Danger zone'), ENT_QUOTES); ?></h3> <h3 data-i18n="settings.server.danger"><?php echo htmlspecialchars($t('settings.server.danger', 'Danger zone'), ENT_QUOTES); ?></h3>
</div> </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> </div>
<!-- ===================== TOOLS TAB ===================== --> <!-- ===================== TOOLS TAB ===================== -->
@ -657,102 +621,9 @@ $toolbarRightHtml = '';
</div> </div>
</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> </section>
</main> </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 id="ruleModal" class="modal" style="display:none;">
<div class="modal-card"> <div class="modal-card">
<div class="modal-head"> <div class="modal-head">

View File

@ -204,6 +204,8 @@ input, select {
z-index: 20; z-index: 20;
display: none; display: none;
min-width: 200px; min-width: 200px;
max-height: 60vh;
overflow-y: auto;
margin-top: 6px; margin-top: 6px;
padding: 6px; padding: 6px;
border: 1px solid var(--border); border: 1px solid var(--border);
@ -448,6 +450,9 @@ input, select {
border: 1px solid var(--border); border: 1px solid var(--border);
border-radius: 16px; border-radius: 16px;
overflow: hidden; overflow: hidden;
display: flex;
flex-direction: column;
max-height: 90vh;
} }
.modal-head, .modal-foot { .modal-head, .modal-foot {
@ -470,6 +475,21 @@ input, select {
.modal-body { .modal-body {
padding: 12px; 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) { @media (max-width: 980px) {

View File

@ -78,11 +78,16 @@
"settings.tasks.modal_title": "Aufgabe", "settings.tasks.modal_title": "Aufgabe",
"settings.tasks.field_name": "Name", "settings.tasks.field_name": "Name",
"settings.tasks.field_sources": "Quellen", "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.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.transmission": "Transmission",
"settings.tasks.source.staging": "Staging", "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.analyze": "Analyse",
"settings.tasks.action.identify": "Identifizieren", "settings.tasks.action.identify": "Identifizieren",
"settings.tasks.action.normalize": "Normalisieren", "settings.tasks.action.normalize": "Normalisieren",
@ -146,8 +151,10 @@
"settings.title": "Einstellungen", "settings.title": "Einstellungen",
"settings.back": "Zurück", "settings.back": "Zurück",
"settings.tabs.scan_profiles": "Scan-Profile", "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.plugins": "Plugins",
"settings.tabs.templates": "Vorlagen",
"settings.tabs.tasks": "Aufgaben", "settings.tabs.tasks": "Aufgaben",
"settings.tabs.rules": "Regeln", "settings.tabs.rules": "Regeln",
"settings.tabs.tools": "Programme", "settings.tabs.tools": "Programme",
@ -162,15 +169,34 @@
"settings.scan_profiles.confirm_delete": "Profil löschen?", "settings.scan_profiles.confirm_delete": "Profil löschen?",
"settings.scan_profiles.ext_default": "Standard", "settings.scan_profiles.ext_default": "Standard",
"settings.scanner_defaults.title": "Globale Scanner-Defaults", "settings.scanner_defaults.title": "Globale Scanner-Defaults",
"settings.library_layout.title": "Library", "settings.library.title": "Library",
"settings.library_layout.preview": "Vorschau", "settings.library.preview": "Vorschau",
"settings.library_layout.roots": "Ordner", "settings.library.roots": "Ordner",
"settings.library_layout.add_root": "Hinzufuegen", "settings.library.add_root": "Hinzufuegen",
"settings.library_layout.th_type": "Typ", "settings.library.th_type": "Typ",
"settings.library_layout.th_path": "Pfad", "settings.library.th_path": "Pfad",
"settings.library_layout.th_status": "Status", "settings.library.th_status": "Status",
"settings.library_layout.modal_title": "Ordner", "settings.library.modal_title": "Ordner",
"settings.library_layout.confirm_delete": "Ordner löschen?", "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.movie": "Filme",
"root.type.series": "Serien", "root.type.series": "Serien",
"root.type.staging": "Staging", "root.type.staging": "Staging",
@ -332,21 +358,21 @@
"settings.scanner_defaults.max_files": "Max. Dateien pro Element", "settings.scanner_defaults.max_files": "Max. Dateien pro Element",
"settings.scanner_defaults.max_items": "Max. Elemente pro Scan (0 = ohne Limit)", "settings.scanner_defaults.max_items": "Max. Elemente pro Scan (0 = ohne Limit)",
"settings.library_layout.movies_root": "Filme-Ordner", "settings.library.movies_root": "Filme-Ordner",
"settings.library_layout.movies_root_ph": "/mnt/media/library/movies", "settings.library.movies_root_ph": "/mnt/media/library/movies",
"settings.library_layout.series_root": "Serien-Ordner", "settings.library.series_root": "Serien-Ordner",
"settings.library_layout.series_root_ph": "/mnt/media/library/series", "settings.library.series_root_ph": "/mnt/media/library/series",
"settings.library_layout.staging_root": "Staging (optional)", "settings.library.staging_root": "Staging (optional)",
"settings.library_layout.staging_root_ph": "/mnt/media/.staging", "settings.library.staging_root_ph": "/mnt/media/.staging",
"settings.library_layout.movies_strategy_title": "Filme-Strategie", "settings.library.movies_strategy_title": "Filme-Strategie",
"settings.library_layout.series_strategy_title": "Serien-Strategie", "settings.library.series_strategy_title": "Serien-Strategie",
"settings.library_layout.strategy": "Strategie", "settings.library.strategy": "Strategie",
"settings.library_layout.season_naming": "Staffelbezeichnung", "settings.library.season_naming": "Staffelbezeichnung",
"settings.library_layout.normalization_title": "Normalisierung", "settings.library.normalization_title": "Normalisierung",
"settings.library_layout.collision_title": "Kollisionsregel", "settings.library.collision_title": "Kollisionsregel",
"settings.library_layout.preview_title": "Vorschau", "settings.library.preview_title": "Vorschau",
"settings.library_layout.preview_hint": "Klicke auf „Vorschau erzeugen“. ", "settings.library.preview_hint": "Klicke auf „Vorschau erzeugen“. ",
"settings.strategy.flat": "Flach", "settings.strategy.flat": "Flach",
"settings.strategy.first_letter": "Nach erstem Buchstaben", "settings.strategy.first_letter": "Nach erstem Buchstaben",
@ -511,7 +537,8 @@
"settings.tasks.add": "Aufgabe hinzufugen", "settings.tasks.add": "Aufgabe hinzufugen",
"settings.tasks.th_name": "Aufgabe", "settings.tasks.th_name": "Aufgabe",
"settings.tasks.th_sources": "Quellen", "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", "settings.tasks.th_status": "Status",
"auth.page_title": "scMedia / Login", "auth.page_title": "scMedia / Login",

View File

@ -78,11 +78,16 @@
"settings.tasks.modal_title": "Task", "settings.tasks.modal_title": "Task",
"settings.tasks.field_name": "Name", "settings.tasks.field_name": "Name",
"settings.tasks.field_sources": "Sources", "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.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.transmission": "Transmission",
"settings.tasks.source.staging": "Staging", "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.analyze": "Analyze",
"settings.tasks.action.identify": "Identify", "settings.tasks.action.identify": "Identify",
"settings.tasks.action.normalize": "Normalize", "settings.tasks.action.normalize": "Normalize",
@ -146,8 +151,10 @@
"settings.title": "Settings", "settings.title": "Settings",
"settings.back": "Back", "settings.back": "Back",
"settings.tabs.scan_profiles": "Scan Profiles", "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.plugins": "Plugins",
"settings.tabs.templates": "Templates",
"settings.tabs.tasks": "Tasks", "settings.tabs.tasks": "Tasks",
"settings.tabs.rules": "Rules", "settings.tabs.rules": "Rules",
"settings.tabs.tools": "Programs", "settings.tabs.tools": "Programs",
@ -162,15 +169,34 @@
"settings.scan_profiles.confirm_delete": "Delete profile?", "settings.scan_profiles.confirm_delete": "Delete profile?",
"settings.scan_profiles.ext_default": "default", "settings.scan_profiles.ext_default": "default",
"settings.scanner_defaults.title": "Global scanner defaults", "settings.scanner_defaults.title": "Global scanner defaults",
"settings.library_layout.title": "Library", "settings.library.title": "Library",
"settings.library_layout.preview": "Preview", "settings.library.preview": "Preview",
"settings.library_layout.roots": "Folders", "settings.library.roots": "Folders",
"settings.library_layout.add_root": "Add", "settings.library.add_root": "Add",
"settings.library_layout.th_type": "Type", "settings.library.th_type": "Type",
"settings.library_layout.th_path": "Path", "settings.library.th_path": "Path",
"settings.library_layout.th_status": "Status", "settings.library.th_status": "Status",
"settings.library_layout.modal_title": "Root", "settings.library.modal_title": "Root",
"settings.library_layout.confirm_delete": "Delete 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.movie": "Movie",
"root.type.series": "Series", "root.type.series": "Series",
"root.type.staging": "Staging", "root.type.staging": "Staging",
@ -332,21 +358,21 @@
"settings.scanner_defaults.max_files": "Max files per item", "settings.scanner_defaults.max_files": "Max files per item",
"settings.scanner_defaults.max_items": "Max items per scan (0 = no limit)", "settings.scanner_defaults.max_items": "Max items per scan (0 = no limit)",
"settings.library_layout.movies_root": "Movies root", "settings.library.movies_root": "Movies root",
"settings.library_layout.movies_root_ph": "/mnt/media/library/movies", "settings.library.movies_root_ph": "/mnt/media/library/movies",
"settings.library_layout.series_root": "Series root", "settings.library.series_root": "Series root",
"settings.library_layout.series_root_ph": "/mnt/media/library/series", "settings.library.series_root_ph": "/mnt/media/library/series",
"settings.library_layout.staging_root": "Staging root (optional)", "settings.library.staging_root": "Staging root (optional)",
"settings.library_layout.staging_root_ph": "/mnt/media/.staging", "settings.library.staging_root_ph": "/mnt/media/.staging",
"settings.library_layout.movies_strategy_title": "Movies strategy", "settings.library.movies_strategy_title": "Movies strategy",
"settings.library_layout.series_strategy_title": "Series strategy", "settings.library.series_strategy_title": "Series strategy",
"settings.library_layout.strategy": "Strategy", "settings.library.strategy": "Strategy",
"settings.library_layout.season_naming": "Season naming", "settings.library.season_naming": "Season naming",
"settings.library_layout.normalization_title": "Normalization", "settings.library.normalization_title": "Normalization",
"settings.library_layout.collision_title": "Collision policy", "settings.library.collision_title": "Collision policy",
"settings.library_layout.preview_title": "Preview", "settings.library.preview_title": "Preview",
"settings.library_layout.preview_hint": "Click \"Generate preview\".", "settings.library.preview_hint": "Click \"Generate preview\".",
"settings.strategy.flat": "Flat", "settings.strategy.flat": "Flat",
"settings.strategy.first_letter": "By first letter", "settings.strategy.first_letter": "By first letter",
@ -511,7 +537,8 @@
"settings.tasks.add": "Add task", "settings.tasks.add": "Add task",
"settings.tasks.th_name": "Task", "settings.tasks.th_name": "Task",
"settings.tasks.th_sources": "Sources", "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", "settings.tasks.th_status": "Status",
"auth.page_title": "scMedia / Login", "auth.page_title": "scMedia / Login",

View File

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

View File

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

View File

@ -1369,7 +1369,7 @@ function initEventsPipe() {
initSourcesPolling(); initSourcesPolling();
return; 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)); await new Promise((r) => setTimeout(r, 50));
const es = new EventSource('/api/events'); const es = new EventSource('/api/events');
state.eventsSource = es; state.eventsSource = es;
@ -1643,9 +1643,6 @@ function init() {
if (prefs?.theme && window.UI?.setTheme) { if (prefs?.theme && window.UI?.setTheme) {
window.UI.setTheme(prefs.theme); window.UI.setTheme(prefs.theme);
} }
if (prefs?.language && prefs.language !== state.lang) {
localStorage.setItem('scmedia_lang', prefs.language);
}
}).catch(() => {}); }).catch(() => {});
} }
UI.initThemeToggle(); UI.initThemeToggle();

View File

@ -2,14 +2,24 @@
/* English comments: auth token helpers and login UI */ /* English comments: auth token helpers and login UI */
(function () { (function () {
const LS_ACCESS = 'scmedia_access_token';
const LS_REFRESH = 'scmedia_refresh_token'; const LS_REFRESH = 'scmedia_refresh_token';
const LS_CLIENT = 'scmedia_client_type'; const LS_CLIENT = 'scmedia_client_type';
const ACCESS_COOKIE = 'scmedia_access_token';
let refreshPromise = null; let refreshPromise = null;
let refreshTimer = null; let refreshTimer = null;
function getAccessToken() { 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() { function getRefreshToken() {
@ -22,22 +32,22 @@
function setTokens(tokens, clientType = 'web', remember = true) { function setTokens(tokens, clientType = 'web', remember = true) {
const storage = remember ? localStorage : sessionStorage; 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); if (tokens?.refresh_token) storage.setItem(LS_REFRESH, tokens.refresh_token);
storage.setItem(LS_CLIENT, clientType); storage.setItem(LS_CLIENT, clientType);
const other = remember ? sessionStorage : localStorage; const other = remember ? sessionStorage : localStorage;
other.removeItem(LS_ACCESS);
other.removeItem(LS_REFRESH); other.removeItem(LS_REFRESH);
other.removeItem(LS_CLIENT); other.removeItem(LS_CLIENT);
localStorage.removeItem('scmedia_access_token');
sessionStorage.removeItem('scmedia_access_token');
} }
function clearTokens() { function clearTokens() {
localStorage.removeItem(LS_ACCESS);
localStorage.removeItem(LS_REFRESH); localStorage.removeItem(LS_REFRESH);
localStorage.removeItem(LS_CLIENT); localStorage.removeItem(LS_CLIENT);
sessionStorage.removeItem(LS_ACCESS);
sessionStorage.removeItem(LS_REFRESH); sessionStorage.removeItem(LS_REFRESH);
sessionStorage.removeItem(LS_CLIENT); sessionStorage.removeItem(LS_CLIENT);
localStorage.removeItem('scmedia_access_token');
sessionStorage.removeItem('scmedia_access_token');
} }
function parseJwt(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_LAST_TYPE);
localStorage.removeItem(SSE_LEASE_KEY); localStorage.removeItem(SSE_LEASE_KEY);
sessionStorage.removeItem(SSE_KEY_EXP); 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 }; window.Sse = { start, stop, on };

View File

@ -2,7 +2,7 @@
/* English comments: shared DOM + UI helpers */ /* English comments: shared DOM + UI helpers */
(function () { (function () {
const LS_THEME = 'scmedia_theme'; const THEME_COOKIE = 'scmedia_theme';
let themeToggleReady = false; let themeToggleReady = false;
let queueState = { active: [], recent: [] }; let queueState = { active: [], recent: [] };
let sseOpening = false; let sseOpening = false;
@ -21,16 +21,33 @@
return Array.from(document.querySelectorAll(selector)); 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) { function setTheme(theme) {
const mode = theme === 'light' ? 'light' : 'dark'; const mode = theme === 'light' ? 'light' : 'dark';
document.documentElement.setAttribute('data-theme', mode); document.documentElement.setAttribute('data-theme', mode);
localStorage.setItem(LS_THEME, mode); setCookie(THEME_COOKIE, mode);
} }
function initThemeToggle() { function initThemeToggle() {
const btn = qs('themeToggle'); const btn = qs('themeToggle');
const saved = localStorage.getItem(LS_THEME) || 'dark'; const saved = getCookie(THEME_COOKIE) || 'dark';
setTheme(saved); setTheme(saved);
if (!btn || themeToggleReady) return; if (!btn || themeToggleReady) return;
btn.addEventListener('click', () => { btn.addEventListener('click', () => {