Xboard/app/Services/ThemeService.php
xboard 0de998dc25
Some checks failed
Docker Build and Publish / build (push) Has been cancelled
fix: theme update
2025-01-11 07:06:53 +08:00

366 lines
11 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
namespace App\Services;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\View;
use Illuminate\Http\UploadedFile;
use Exception;
use ZipArchive;
class ThemeService
{
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'];
public function __construct()
{
$this->registerThemeViewPaths();
}
/**
* 注册主题视图路径
*/
private function registerThemeViewPaths(): void
{
// 系统主题路径
$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);
}
}
/**
* 获取主题视图路径
*/
public function getThemeViewPath(string $theme): ?string
{
$themePath = $this->getThemePath($theme);
if (!$themePath) {
return null;
}
return $themePath . '/dashboard.blade.php';
}
/**
* 获取所有可用主题列表
*/
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;
}
/**
* 从指定路径获取主题列表
*/
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();
}
/**
* 上传新主题
*/
public function upload(UploadedFile $file): bool
{
$zip = new ZipArchive;
$tmpPath = storage_path('tmp/' . uniqid());
try {
if ($zip->open($file->path()) !== true) {
throw new Exception('无效的主题包');
}
// 查找配置文件
$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('主题配置文件不存在');
}
// 解压并读取配置
$zip->extractTo($tmpPath);
$zip->close();
$sourcePath = $tmpPath . '/' . rtrim(dirname($configEntry), '.');
$configFile = $sourcePath . '/' . self::CONFIG_FILE;
if (!File::exists($configFile)) {
throw new Exception('主题配置文件不存在');
}
$config = json_decode(File::get($configFile), true);
if (empty($config['name'])) {
throw new Exception('主题名称未配置');
}
// 检查是否为系统主题
if (in_array($config['name'], self::SYSTEM_THEMES)) {
throw new Exception('不能上传与系统主题同名的主题');
}
// 检查必要文件
if (!File::exists($sourcePath . '/dashboard.blade.php')) {
throw new Exception('缺少必要的主题文件dashboard.blade.php');
}
// 确保目标目录存在
$userThemePath = base_path(self::USER_THEME_DIR);
if (!File::exists($userThemePath)) {
File::makeDirectory($userThemePath, 0755, true);
}
$targetPath = $userThemePath . $config['name'];
if (File::exists($targetPath)) {
throw new Exception('主题已存在');
}
File::copyDirectory($sourcePath, $targetPath);
$this->initConfig($config['name']);
return true;
} catch (Exception $e) {
throw $e;
} finally {
// 清理临时文件
if (File::exists($tmpPath)) {
File::deleteDirectory($tmpPath);
}
}
}
/**
* 切换主题
*/
public function switch(string $theme): bool
{
$currentTheme = admin_setting('current_theme');
if ($theme === $currentTheme) {
return true;
}
try {
// 验证主题是否存在
$themePath = $this->getThemePath($theme);
if (!$themePath) {
throw new Exception('主题不存在');
}
// 验证视图文件是否存在
if (!File::exists($this->getThemeViewPath($theme))) {
throw new Exception('主题视图文件不存在');
}
// 复制主题文件到public目录
$targetPath = public_path('theme/' . $theme);
if (!File::copyDirectory($themePath, $targetPath)) {
throw new Exception('复制主题文件失败');
}
// 清理旧主题文件
if ($currentTheme) {
$oldPath = public_path('theme/' . $currentTheme);
if (File::exists($oldPath)) {
File::deleteDirectory($oldPath);
}
}
admin_setting(['current_theme' => $theme]);
return true;
} catch (Exception $e) {
Log::error('Theme switch failed', ['theme' => $theme, 'error' => $e->getMessage()]);
throw $e;
}
}
/**
* 删除主题
*/
public function delete(string $theme): bool
{
try {
// 检查是否为系统主题
if (in_array($theme, self::SYSTEM_THEMES)) {
throw new Exception('系统主题不能删除');
}
// 检查是否为当前使用的主题
if ($theme === admin_setting('current_theme')) {
throw new Exception('当前使用的主题不能删除');
}
// 获取主题路径
$themePath = base_path(self::USER_THEME_DIR . $theme);
if (!File::exists($themePath)) {
throw new Exception('主题不存在');
}
// 删除主题文件
File::deleteDirectory($themePath);
// 删除public目录下的主题文件
$publicPath = public_path('theme/' . $theme);
if (File::exists($publicPath)) {
File::deleteDirectory($publicPath);
}
// 清理主题配置
admin_setting([self::SETTING_PREFIX . $theme => null]);
return true;
} catch (Exception $e) {
Log::error('Theme deletion failed', ['theme' => $theme, 'error' => $e->getMessage()]);
throw $e;
}
}
/**
* 检查主题是否存在
*/
public function exists(string $theme): bool
{
return $this->getThemePath($theme) !== null;
}
/**
* 获取主题路径
*/
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;
}
/**
* 获取主题配置
*/
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;
}
/**
* 更新主题配置
*/
public function updateConfig(string $theme, array $config): bool
{
try {
// 验证主题是否存在
if (!$this->getThemePath($theme)) {
throw new Exception('主题不存在');
}
$schema = $this->readConfigFile($theme);
if (!$schema) {
throw new Exception('主题配置文件无效');
}
// 只保留有效的配置字段
$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;
}
}
/**
* 读取主题配置文件
*/
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;
}
/**
* 初始化主题配置
*/
private function initConfig(string $theme): void
{
$config = $this->readConfigFile($theme);
if (!$config) {
return;
}
$defaults = collect($config['configs'] ?? [])
->mapWithKeys(fn($col) => [$col['field_name'] => $col['default_value'] ?? ''])
->toArray();
admin_setting([self::SETTING_PREFIX . $theme => $defaults]);
}
}