WebOrbiton
v3.0.0.0

FlatlyPage

454 lines · 21.3 KB
  1. <?php
  2. require_once __DIR__ . '/../config.php';
  3. require_once __DIR__ . '/sidebar.php';
  4. ​
  5. require_login();
  6. ​
  7. $message = '';
  8. $message_type = '';
  9. ​
  10. if ($_SERVER['REQUEST_METHOD'] === 'POST') {
  11. $token = $_POST['csrf_token'] ?? '';
  12. if (!verify_csrf_token($token)) {
  13. $message = 'Invalid request. Please try again.';
  14. $message_type = 'error';
  15. } else {
  16. $action = $_POST['action'] ?? '';
  17. switch ($action) {
  18. case 'activate':
  19. $theme_file = $_POST['theme_file'] ?? '';
  20. if ($theme_file && file_exists(__DIR__ . '/../themes/' . $theme_file)) {
  21. $source = __DIR__ . '/../themes/' . $theme_file;
  22. $target = __DIR__ . '/../css/theme.css';
  23. if (copy($source, $target)) {
  24. $message = 'Theme activated successfully!';
  25. $message_type = 'success';
  26. } else {
  27. $message = 'Failed to activate theme.';
  28. $message_type = 'error';
  29. }
  30. } else {
  31. $message = 'Theme file not found.';
  32. $message_type = 'error';
  33. }
  34. break;
  35. case 'deactivate':
  36. $target = __DIR__ . '/../css/theme.css';
  37. if (!file_exists($target) || unlink($target)) {
  38. $message = 'Theme deactivated successfully!';
  39. $message_type = 'success';
  40. } else {
  41. $message = 'Failed to deactivate theme.';
  42. $message_type = 'error';
  43. }
  44. break;
  45. case 'delete':
  46. $theme_file = $_POST['theme_file'] ?? '';
  47. if (!$theme_file || !file_exists(__DIR__ . '/../themes/' . $theme_file)) {
  48. $message = 'Theme file not found.';
  49. $message_type = 'error';
  50. } else {
  51. $current_theme = getCurrentThemeFile();
  52. if ($theme_file === $current_theme) {
  53. $target = __DIR__ . '/../css/theme.css';
  54. @unlink($target);
  55. }
  56. if (unlink(__DIR__ . '/../themes/' . $theme_file)) {
  57. $message = 'Theme deleted successfully!';
  58. $message_type = 'success';
  59. } else {
  60. $message = 'Failed to delete theme.';
  61. $message_type = 'error';
  62. }
  63. }
  64. break;
  65. case 'upload':
  66. if (isset($_FILES['theme_file']) && $_FILES['theme_file']['error'] === 0) {
  67. $file = $_FILES['theme_file'];
  68. $file_ext = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
  69. if ($file_ext !== 'css') {
  70. $message = 'Please upload a CSS file.';
  71. $message_type = 'error';
  72. } else {
  73. $safe_filename = preg_replace('/[^a-z0-9-_]/i', '', pathinfo($file['name'], PATHINFO_FILENAME));
  74. $target_file = __DIR__ . '/../themes/' . $safe_filename . '.css';
  75. if (file_exists($target_file)) {
  76. $message = 'Theme with this name already exists.';
  77. $message_type = 'error';
  78. } elseif (move_uploaded_file($file['tmp_name'], $target_file)) {
  79. $message = 'Theme uploaded successfully!';
  80. $message_type = 'success';
  81. } else {
  82. $message = 'Failed to upload theme.';
  83. $message_type = 'error';
  84. }
  85. }
  86. } else {
  87. $message = 'Please select a valid CSS file.';
  88. $message_type = 'error';
  89. }
  90. break;
  91. }
  92. }
  93. }
  94. ​
  95. function getCurrentThemeFile() {
  96. $current_theme_path = __DIR__ . '/../css/theme.css';
  97. if (!file_exists($current_theme_path)) {
  98. return null;
  99. }
  100. $current_content = file_get_contents($current_theme_path);
  101. if (trim($current_content) === '/* No theme active */' || empty(trim($current_content))) {
  102. return null;
  103. }
  104. $themes_dir = __DIR__ . '/../themes/';
  105. if (!is_dir($themes_dir)) {
  106. return null;
  107. }
  108. $theme_files = glob($themes_dir . '*.css');
  109. foreach ($theme_files as $theme_file) {
  110. $theme_content = file_get_contents($theme_file);
  111. if ($theme_content === $current_content) {
  112. return basename($theme_file);
  113. }
  114. }
  115. return null;
  116. }
  117. ​
  118. function parseThemeMetadata($file_path) {
  119. $content = file_get_contents($file_path);
  120. $filename = basename($file_path, '.css');
  121. $metadata = [
  122. 'name' => $filename,
  123. 'author' => 'Unknown',
  124. 'file' => basename($file_path)
  125. ];
  126. if (preg_match('/Theme:\s*([^\r\n]+)/i', $content, $m)) $metadata['name'] = trim($m[1]);
  127. if (preg_match('/Author:\s*([^\r\n]+)/i', $content, $m)) $metadata['author'] = trim($m[1]);
  128. $metadata['colors'] = extractThemeColors($content);
  129. return $metadata;
  130. }
  131. ​
  132. function extractThemeColors($css_content) {
  133. $colors = [];
  134. ​
  135. if (preg_match('/Colors:\s*([^\r\n]*)/i', $css_content, $matches)) {
  136. $color_line = $matches[1];
  137. if (preg_match_all('/#(?:[0-9a-fA-F]{3}){1,2}/', $color_line, $color_matches)) {
  138. $colors = array_values(array_map('strtolower', $color_matches[0]));
  139. }
  140. }
  141. if (empty($colors)) {
  142. $color_patterns = ['/--primary:\s*([^;]+);/i', '/--accent:\s*([^;]+);/i', '/--secondary:\s*([^;]+);/i'];
  143. foreach ($color_patterns as $pattern) {
  144. if (preg_match($pattern, $css_content, $match)) {
  145. $normalized = normalizeColor($match[1]);
  146. if ($normalized) $colors[] = $normalized;
  147. }
  148. }
  149. }
  150. ​
  151. $colors = array_values(array_filter($colors, fn($c) => $c !== null));
  152. ​
  153. while (count($colors) < 3) {
  154. $colors[] = 'No data';
  155. }
  156. return array_slice($colors, 0, 3);
  157. }
  158. ​
  159. function normalizeColor($color) {
  160. $color = trim($color);
  161. ​
  162. if (preg_match('/^#([0-9a-f]{3}|[0-9a-f]{6})$/i', $color)) {
  163. return strtolower($color);
  164. }
  165. ​
  166. if (preg_match('/rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*[\d.]+)?\)/i', $color, $matches)) {
  167. $r = min(255, max(0, intval($matches[1])));
  168. $g = min(255, max(0, intval($matches[2])));
  169. $b = min(255, max(0, intval($matches[3])));
  170. return sprintf('#%02x%02x%02x', $r, $g, $b);
  171. }
  172. return null;
  173. }
  174. ​
  175. function isGrayscaleColor($hex) {
  176. $hex = ltrim($hex, '#');
  177. if (strlen($hex) === 3) {
  178. $hex = $hex[0] . $hex[0] . $hex[1] . $hex[1] . $hex[2] . $hex[2];
  179. }
  180. $r = hexdec(substr($hex, 0, 2));
  181. $g = hexdec(substr($hex, 2, 2));
  182. $b = hexdec(substr($hex, 4, 2));
  183. $max_diff = max(abs($r - $g), abs($g - $b), abs($r - $b));
  184. return $max_diff < 15;
  185. }
  186. ​
  187. function isSimilarToExisting($new_color, $existing_colors) {
  188. $new_hex = ltrim($new_color, '#');
  189. if (strlen($new_hex) === 3) {
  190. $new_hex = $new_hex[0] . $new_hex[0] . $new_hex[1] . $new_hex[1] . $new_hex[2] . $new_hex[2];
  191. }
  192. $new_r = hexdec(substr($new_hex, 0, 2));
  193. $new_g = hexdec(substr($new_hex, 2, 2));
  194. $new_b = hexdec(substr($new_hex, 4, 2));
  195. foreach ($existing_colors as $existing) {
  196. $existing_hex = ltrim($existing, '#');
  197. if (strlen($existing_hex) === 3) {
  198. $existing_hex = $existing_hex[0] . $existing_hex[0] . $existing_hex[1] . $existing_hex[1] . $existing_hex[2] . $existing_hex[2];
  199. }
  200. $exist_r = hexdec(substr($existing_hex, 0, 2));
  201. $exist_g = hexdec(substr($existing_hex, 2, 2));
  202. $exist_b = hexdec(substr($existing_hex, 4, 2));
  203. $distance = sqrt(
  204. pow($new_r - $exist_r, 2) +
  205. pow($new_g - $exist_g, 2) +
  206. pow($new_b - $exist_b, 2)
  207. );
  208. if ($distance < 80) {
  209. return true;
  210. }
  211. }
  212. return false;
  213. }
  214. ​
  215. function getAllThemes() {
  216. $themes_dir = __DIR__ . '/../themes/';
  217. $themes = [];
  218. if (!is_dir($themes_dir)) {
  219. mkdir($themes_dir, 0755, true);
  220. return $themes;
  221. }
  222. $theme_files = glob($themes_dir . '*.css');
  223. foreach ($theme_files as $theme_file) {
  224. $themes[] = parseThemeMetadata($theme_file);
  225. }
  226. return $themes;
  227. }
  228. ​
  229. $csrf_token = generate_csrf_token();
  230. $all_themes = getAllThemes();
  231. $current_theme_file = getCurrentThemeFile();
  232. ​
  233. ?>
  234. <!DOCTYPE html>
  235. <html lang="en">
  236. <head>
  237. <meta charset="UTF-8">
  238. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  239. <title>Themes - FlatlyPage CMS</title>
  240. <meta name="generator" content="FlatlyPage CMS">
  241. <link rel="icon" href="admin.ico?v=<?= filemtime(__DIR__ . '/../css/admin.css') ?>" type="image/x-icon">
  242. <?= admin_font_head() ?>
  243. <link rel="stylesheet" href="/css/admin.css?v=<?= filemtime(__DIR__ . '/../css/admin.css') ?>">
  244. <script src="/assets/js/admin-theme.js?v=5"></script>
  245. </head>
  246. <body>
  247. <div class="app">
  248. <?php admin_sidebar('themes'); ?>
  249. <main class="main">
  250. <header class="main-header">
  251. <button class="mobile-nav-toggle" onclick="document.querySelector('.sidebar').classList.add('open')">
  252. <svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
  253. <line x1="3" y1="6" x2="21" y2="6"/>
  254. <line x1="3" y1="12" x2="21" y2="12"/>
  255. <line x1="3" y1="18" x2="21" y2="18"/>
  256. </svg>
  257. </button>
  258. ​
  259. <div class="main-header-inner">
  260. <h1>Themes</h1>
  261. <div class="header-actions">
  262. <?php if ($current_theme_file): ?>
  263. <form method="POST" action="" style="display: inline-block; margin-right: 8px;">
  264. <input type="hidden" name="csrf_token" value="<?= e($csrf_token) ?>">
  265. <input type="hidden" name="action" value="deactivate">
  266. <button type="submit" class="btn btn-secondary btn-sm">
  267. <svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24" width="16" height="16"><circle cx="12" cy="12" r="10"/><line x1="15" y1="9" x2="9" y2="15"/><line x1="9" y1="9" x2="15" y2="15"/></svg>
  268. Deactivate Theme
  269. </button>
  270. </form>
  271. <?php endif; ?>
  272. <button type="button" class="btn btn-primary btn-sm" onclick="window.location.href='theme-edit/'">
  273. <svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M20 7h-3a2 2 0 0 1-2-2V2"/><path d="M9 18a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h7l4 4v10a2 2 0 0 1-2 2Z"/><path d="M3 7.6v12.8A1.6 1.6 0 0 0 4.6 22h9.8"/></svg>
  274. Create your own
  275. </button>
  276. <button type="button" class="btn btn-primary btn-sm" onclick="document.getElementById('uploadModal').classList.add('active')">
  277. <svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24" width="16" height="16"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="17 8 12 3 7 8"/><line x1="12" y1="3" x2="12" y2="15"/></svg>
  278. Upload Theme
  279. </button>
  280. </div>
  281. </div>
  282. </header>
  283. <div class="main-content">
  284. <?php if ($message): ?>
  285. <div class="message <?= $message_type ?>">
  286. <svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24" width="20" height="20">
  287. <?php if ($message_type === 'success'): ?>
  288. <path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/><polyline points="22 4 12 14.01 9 11.01"/>
  289. <?php else: ?>
  290. <circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/>
  291. <?php endif; ?>
  292. </svg>
  293. <?= e($message) ?>
  294. </div>
  295. <?php endif; ?>
  296. ​
  297. <?php if (empty($all_themes)): ?>
  298. <div class="card">
  299. <div class="empty-state">
  300. <svg class="empty-state-icon" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
  301. <path d="M20 7h-3a2 2 0 0 1-2-2V2"/><path d="M9 18a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h7l4 4v10a2 2 0 0 1-2 2Z"/><path d="M3 7.6v12.8A1.6 1.6 0 0 0 4.6 22h9.8"/>
  302. </svg>
  303. <h3>No themes installed</h3>
  304. <p>Upload a theme to get started.</p>
  305. <button type="button" class="btn btn-primary" onclick="document.getElementById('uploadModal').classList.add('active')">Upload Theme</button>
  306. </div>
  307. </div>
  308. <?php else: ?>
  309. <div class="extensions-grid" style="display: grid; grid-template-columns: repeat(auto-fill, minmax(340px, 1fr)); gap: 24px;">
  310. <?php foreach ($all_themes as $theme):
  311. $is_active = $theme['file'] === $current_theme_file;
  312. ?>
  313. <div class="card extension-card" style="position: relative;">
  314. <?php if ($is_active): ?>
  315. <div style="position: absolute; top: 16px; right: 16px;">
  316. <span class="badge" style="background: var(--success); color: white; font-size: 0.75rem; padding: 4px 12px; border-radius: 12px;">Active</span>
  317. </div>
  318. <?php endif; ?>
  319. <div class="card-body">
  320. <h3 style="margin: 0 0 8px 0; font-size: 1.25rem;"><?= e($theme['name']) ?></h3>
  321. <p style="color: var(--text-muted); font-size: 0.875rem; margin: 0 0 12px 0;">by <?= e($theme['author']) ?></p>
  322. <?php if (!empty($theme['colors'])): ?>
  323. <div style="display: flex; gap: 6px; margin-bottom: 16px; flex-wrap: wrap; min-height: 24px;">
  324. <?php foreach ($theme['colors'] as $color): ?>
  325. <div title="<?= e($color) ?>"
  326. style="width: 24px; height: 24px; border-radius: 4px; background: <?= e($color) ?>; border: 1px solid var(--border); flex-shrink: 0;">
  327. </div>
  328. <?php endforeach; ?>
  329. </div>
  330. <?php else: ?>
  331. <div style="display: flex; gap: 6px; margin-bottom: 16px;">
  332. <div style="width: 24px; height: 24px; border-radius: 4px; background: #333333; border: 1px solid var(--border); opacity: 0.5;"></div>
  333. </div>
  334. <?php endif; ?>
  335. <div style="display: flex; gap: 8px;">
  336. <?php if ($is_active): ?>
  337. <form method="POST" action="" style="flex: 1;">
  338. <input type="hidden" name="csrf_token" value="<?= e($csrf_token) ?>">
  339. <input type="hidden" name="action" value="deactivate">
  340. <button type="submit" class="btn btn-secondary btn-sm" style="width: 100%;">Deactivate</button>
  341. </form>
  342. <form method="POST" action="" onsubmit="return confirm('Are you sure you want to delete this theme? It will be deactivated first.');">
  343. <input type="hidden" name="csrf_token" value="<?= e($csrf_token) ?>">
  344. <input type="hidden" name="action" value="delete">
  345. <input type="hidden" name="theme_file" value="<?= e($theme['file']) ?>">
  346. <button type="submit" class="btn btn-ghost btn-sm" style="color: var(--error);">
  347. <svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24" width="16" height="16"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg>
  348. </button>
  349. </form>
  350. <?php else: ?>
  351. <form method="POST" action="" style="flex: 1;">
  352. <input type="hidden" name="csrf_token" value="<?= e($csrf_token) ?>">
  353. <input type="hidden" name="action" value="activate">
  354. <input type="hidden" name="theme_file" value="<?= e($theme['file']) ?>">
  355. <button type="submit" class="btn btn-primary btn-sm" style="width: 100%;">Activate</button>
  356. </form>
  357. <form method="POST" action="" onsubmit="return confirm('Are you sure you want to delete this theme?');">
  358. <input type="hidden" name="csrf_token" value="<?= e($csrf_token) ?>">
  359. <input type="hidden" name="action" value="delete">
  360. <input type="hidden" name="theme_file" value="<?= e($theme['file']) ?>">
  361. <button type="submit" class="btn btn-ghost btn-sm" style="color: var(--error);">
  362. <svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24" width="16" height="16"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg>
  363. </button>
  364. </form>
  365. <?php endif; ?>
  366. </div>
  367. </div>
  368. </div>
  369. <?php endforeach; ?>
  370. </div>
  371. <?php endif; ?>
  372. </div>
  373. </main>
  374. </div>
  375. ​
  376. <div class="modal-overlay" id="uploadModal">
  377. <div class="modal" style="max-width: 500px;">
  378. <div class="modal-header">
  379. <h3 class="modal-title">Upload Theme</h3>
  380. <button type="button" class="modal-close" onclick="document.getElementById('uploadModal').classList.remove('active')">
  381. <svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24" width="20" height="20">
  382. <line x1="18" y1="6" x2="6" y2="18"/>
  383. <line x1="6" y1="6" x2="18" y2="18"/>
  384. </svg>
  385. </button>
  386. </div>
  387. <div class="modal-body">
  388. <form method="POST" action="" enctype="multipart/form-data">
  389. <input type="hidden" name="csrf_token" value="<?= e($csrf_token) ?>">
  390. <input type="hidden" name="action" value="upload">
  391. <div class="form-group">
  392. <label class="form-label">Theme CSS File</label>
  393. <input type="file" name="theme_file" class="form-input" accept=".css" required>
  394. <p class="form-hint">Upload a CSS file with theme styles</p>
  395. </div>
  396. <div class="modal-actions">
  397. <button type="button" class="btn btn-secondary" onclick="document.getElementById('uploadModal').classList.remove('active')">Cancel</button>
  398. <button type="submit" class="btn btn-primary">Upload Theme</button>
  399. </div>
  400. </form>
  401. </div>
  402. </div>
  403. </div>
  404. ​
  405. </body>
  406. </html>