Files
Xboard/app/Services/ThemeService.php

425 lines
13 KiB
PHP
Raw Normal View History

2023-11-17 14:44:01 +08:00
<?php
namespace App\Services;
use Illuminate\Support\Facades\File;
2025-01-21 14:57:54 +08:00
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\View;
use Illuminate\Http\UploadedFile;
use Exception;
use ZipArchive;
2023-11-17 14:44:01 +08:00
class ThemeService
{
2025-01-21 14:57:54 +08:00
private const SYSTEM_THEME_DIR = 'theme/';
private const USER_THEME_DIR = '/storage/theme/';
private const CONFIG_FILE = 'config.json';
private const SETTING_PREFIX = 'theme_';
private const SYSTEM_THEMES = ['Xboard', 'v2board'];
2023-11-17 14:44:01 +08:00
2025-01-21 14:57:54 +08:00
public function __construct()
2023-11-17 14:44:01 +08:00
{
2025-01-21 14:57:54 +08:00
$this->registerThemeViewPaths();
2023-11-17 14:44:01 +08:00
}
2025-01-21 14:57:54 +08:00
/**
* Register theme view paths
2025-01-21 14:57:54 +08:00
*/
private function registerThemeViewPaths(): void
2023-11-17 14:44:01 +08:00
{
2025-01-21 14:57:54 +08:00
$systemPath = base_path(self::SYSTEM_THEME_DIR);
if (File::exists($systemPath)) {
View::addNamespace('theme', $systemPath);
}
$userPath = base_path(self::USER_THEME_DIR);
if (File::exists($userPath)) {
View::prependNamespace('theme', $userPath);
}
}
/**
* Get theme view path
2025-01-21 14:57:54 +08:00
*/
public function getThemeViewPath(string $theme): ?string
{
$themePath = $this->getThemePath($theme);
if (!$themePath) {
return null;
}
return $themePath . '/dashboard.blade.php';
}
/**
* Get all available themes
2025-01-21 14:57:54 +08:00
*/
public function getList(): array
{
$themes = [];
// 获取系统主题
$systemPath = base_path(self::SYSTEM_THEME_DIR);
if (File::exists($systemPath)) {
$themes = $this->getThemesFromPath($systemPath, false);
}
// 获取用户主题
$userPath = base_path(self::USER_THEME_DIR);
if (File::exists($userPath)) {
$themes = array_merge($themes, $this->getThemesFromPath($userPath, true));
}
return $themes;
}
/**
* Get themes from specified path
2025-01-21 14:57:54 +08:00
*/
private function getThemesFromPath(string $path, bool $canDelete): array
{
return collect(File::directories($path))
->mapWithKeys(function ($dir) use ($canDelete) {
$name = basename($dir);
if (
!File::exists($dir . '/' . self::CONFIG_FILE) ||
!File::exists($dir . '/dashboard.blade.php')
) {
return [];
}
$config = $this->readConfigFile($name);
if (!$config) {
return [];
}
$config['can_delete'] = $canDelete && $name !== admin_setting('current_theme');
$config['is_system'] = !$canDelete;
return [$name => $config];
})->toArray();
}
/**
* Upload new theme
2025-01-21 14:57:54 +08:00
*/
public function upload(UploadedFile $file): bool
{
$zip = new ZipArchive;
$tmpPath = storage_path('tmp/' . uniqid());
try {
if ($zip->open($file->path()) !== true) {
throw new Exception('Invalid theme package');
2025-01-21 14:57:54 +08:00
}
$configEntry = collect(range(0, $zip->numFiles - 1))
->map(fn($i) => $zip->getNameIndex($i))
->first(fn($name) => basename($name) === self::CONFIG_FILE);
if (!$configEntry) {
throw new Exception('Theme config file not found');
2025-01-21 14:57:54 +08:00
}
$zip->extractTo($tmpPath);
$zip->close();
$sourcePath = $tmpPath . '/' . rtrim(dirname($configEntry), '.');
$configFile = $sourcePath . '/' . self::CONFIG_FILE;
if (!File::exists($configFile)) {
throw new Exception('Theme config file not found');
2025-01-21 14:57:54 +08:00
}
$config = json_decode(File::get($configFile), true);
if (empty($config['name'])) {
throw new Exception('Theme name not configured');
2025-01-21 14:57:54 +08:00
}
if (in_array($config['name'], self::SYSTEM_THEMES)) {
throw new Exception('Cannot upload theme with same name as system theme');
2025-01-21 14:57:54 +08:00
}
if (!File::exists($sourcePath . '/dashboard.blade.php')) {
throw new Exception('Missing required theme file: dashboard.blade.php');
2025-01-21 14:57:54 +08:00
}
$userThemePath = base_path(self::USER_THEME_DIR);
if (!File::exists($userThemePath)) {
File::makeDirectory($userThemePath, 0755, true);
}
$targetPath = $userThemePath . $config['name'];
if (File::exists($targetPath)) {
$oldConfigFile = $targetPath . '/config.json';
if (!File::exists($oldConfigFile)) {
throw new Exception('Existing theme missing config file');
}
$oldConfig = json_decode(File::get($oldConfigFile), true);
$oldVersion = $oldConfig['version'] ?? '0.0.0';
$newVersion = $config['version'] ?? '0.0.0';
if (version_compare($newVersion, $oldVersion, '>')) {
$this->cleanupThemeFiles($config['name']);
File::deleteDirectory($targetPath);
File::copyDirectory($sourcePath, $targetPath);
// 更新主题时保留用户配置
$this->initConfig($config['name'], true);
return true;
} else {
throw new Exception('Theme exists and not a newer version');
}
2025-01-21 14:57:54 +08:00
}
File::copyDirectory($sourcePath, $targetPath);
$this->initConfig($config['name']);
return true;
} catch (Exception $e) {
throw $e;
} finally {
if (File::exists($tmpPath)) {
File::deleteDirectory($tmpPath);
}
}
}
/**
* Switch theme
2025-01-21 14:57:54 +08:00
*/
2025-07-15 20:49:08 +08:00
public function switch(string|null $theme): bool
2025-01-21 14:57:54 +08:00
{
2025-07-15 20:49:08 +08:00
if ($theme === null) {
return true;
}
2025-01-21 14:57:54 +08:00
$currentTheme = admin_setting('current_theme');
2023-11-17 14:44:01 +08:00
try {
2025-01-21 14:57:54 +08:00
$themePath = $this->getThemePath($theme);
if (!$themePath) {
throw new Exception('Theme not found');
2025-01-21 14:57:54 +08:00
}
if (!File::exists($this->getThemeViewPath($theme))) {
throw new Exception('Theme view file not found');
2025-01-21 14:57:54 +08:00
}
if ($currentTheme && $currentTheme !== $theme) {
$this->cleanupThemeFiles($currentTheme);
2025-01-21 14:57:54 +08:00
}
$targetPath = public_path('theme/' . $theme);
if (!File::copyDirectory($themePath, $targetPath)) {
throw new Exception('Failed to copy theme files');
}
2025-01-21 14:57:54 +08:00
admin_setting(['current_theme' => $theme]);
return true;
} catch (Exception $e) {
Log::error('Theme switch failed', ['theme' => $theme, 'error' => $e->getMessage()]);
throw $e;
2023-11-17 14:44:01 +08:00
}
}
2025-01-21 14:57:54 +08:00
/**
* Delete theme
2025-01-21 14:57:54 +08:00
*/
public function delete(string $theme): bool
{
try {
if (in_array($theme, self::SYSTEM_THEMES)) {
throw new Exception('System theme cannot be deleted');
2025-01-21 14:57:54 +08:00
}
if ($theme === admin_setting('current_theme')) {
throw new Exception('Current theme cannot be deleted');
2025-01-21 14:57:54 +08:00
}
$themePath = base_path(self::USER_THEME_DIR . $theme);
if (!File::exists($themePath)) {
throw new Exception('Theme not found');
2025-01-21 14:57:54 +08:00
}
$this->cleanupThemeFiles($theme);
2025-01-21 14:57:54 +08:00
File::deleteDirectory($themePath);
admin_setting([self::SETTING_PREFIX . $theme => null]);
return true;
} catch (Exception $e) {
Log::error('Theme deletion failed', ['theme' => $theme, 'error' => $e->getMessage()]);
throw $e;
}
}
/**
* Check if theme exists
2025-01-21 14:57:54 +08:00
*/
public function exists(string $theme): bool
{
return $this->getThemePath($theme) !== null;
}
/**
* Get theme path
2025-01-21 14:57:54 +08:00
*/
public function getThemePath(string $theme): ?string
{
$systemPath = base_path(self::SYSTEM_THEME_DIR . $theme);
if (File::exists($systemPath)) {
return $systemPath;
}
$userPath = base_path(self::USER_THEME_DIR . $theme);
if (File::exists($userPath)) {
return $userPath;
}
return null;
}
/**
* Get theme config
2025-01-21 14:57:54 +08:00
*/
public function getConfig(string $theme): ?array
{
$config = admin_setting(self::SETTING_PREFIX . $theme);
if ($config === null) {
$this->initConfig($theme);
$config = admin_setting(self::SETTING_PREFIX . $theme);
}
return $config;
}
/**
* Update theme config
2025-01-21 14:57:54 +08:00
*/
public function updateConfig(string $theme, array $config): bool
{
try {
if (!$this->getThemePath($theme)) {
throw new Exception('Theme not found');
2025-01-21 14:57:54 +08:00
}
$schema = $this->readConfigFile($theme);
if (!$schema) {
throw new Exception('Invalid theme config file');
2025-01-21 14:57:54 +08:00
}
$validFields = collect($schema['configs'] ?? [])->pluck('field_name')->toArray();
$validConfig = collect($config)
->only($validFields)
->toArray();
$currentConfig = $this->getConfig($theme) ?? [];
$newConfig = array_merge($currentConfig, $validConfig);
admin_setting([self::SETTING_PREFIX . $theme => $newConfig]);
return true;
} catch (Exception $e) {
Log::error('Config update failed', ['theme' => $theme, 'error' => $e->getMessage()]);
throw $e;
}
}
/**
* Read theme config file
2025-01-21 14:57:54 +08:00
*/
private function readConfigFile(string $theme): ?array
{
$themePath = $this->getThemePath($theme);
if (!$themePath) {
return null;
}
$file = $themePath . '/' . self::CONFIG_FILE;
return File::exists($file) ? json_decode(File::get($file), true) : null;
}
/**
* Clean up theme files including public directory
*/
public function cleanupThemeFiles(string $theme): void
{
try {
$publicThemePath = public_path('theme/' . $theme);
if (File::exists($publicThemePath)) {
File::deleteDirectory($publicThemePath);
Log::info('Cleaned up public theme files', ['theme' => $theme, 'path' => $publicThemePath]);
}
$cacheKey = "theme_{$theme}_assets";
if (cache()->has($cacheKey)) {
cache()->forget($cacheKey);
Log::info('Cleaned up theme cache', ['theme' => $theme, 'cache_key' => $cacheKey]);
}
} catch (Exception $e) {
Log::warning('Failed to cleanup theme files', [
'theme' => $theme,
'error' => $e->getMessage()
]);
}
}
/**
* Force refresh current theme public files
*/
public function refreshCurrentTheme(): bool
{
try {
$currentTheme = admin_setting('current_theme');
if (!$currentTheme) {
return false;
}
$this->cleanupThemeFiles($currentTheme);
$themePath = $this->getThemePath($currentTheme);
if (!$themePath) {
throw new Exception('Current theme path not found');
}
$targetPath = public_path('theme/' . $currentTheme);
if (!File::copyDirectory($themePath, $targetPath)) {
throw new Exception('Failed to copy theme files');
}
Log::info('Refreshed current theme files', ['theme' => $currentTheme]);
return true;
} catch (Exception $e) {
Log::error('Failed to refresh current theme', [
'theme' => $currentTheme,
'error' => $e->getMessage()
]);
return false;
}
}
/**
* Initialize theme config
*
* @param string $theme 主题名称
* @param bool $preserveExisting 是否保留现有配置(更新主题时使用)
2025-01-21 14:57:54 +08:00
*/
private function initConfig(string $theme, bool $preserveExisting = false): void
2025-01-21 14:57:54 +08:00
{
$config = $this->readConfigFile($theme);
if (!$config) {
return;
}
$defaults = collect($config['configs'] ?? [])
->mapWithKeys(fn($col) => [$col['field_name'] => $col['default_value'] ?? ''])
->toArray();
if ($preserveExisting) {
$existingConfig = admin_setting(self::SETTING_PREFIX . $theme) ?? [];
$mergedConfig = array_merge($defaults, $existingConfig);
admin_setting([self::SETTING_PREFIX . $theme => $mergedConfig]);
} else {
admin_setting([self::SETTING_PREFIX . $theme => $defaults]);
}
2025-01-21 14:57:54 +08:00
}
2023-11-17 14:44:01 +08:00
}