style updates
This commit is contained in:
@@ -26,6 +26,7 @@ final class ThemeAccessibilityValidator
|
||||
$issues[] = ['severity' => 'warning', 'message' => count($matches[0]).' button(s) may lack an accessible name.', 'file' => $relative];
|
||||
}
|
||||
}
|
||||
|
||||
return $issues;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,9 +14,13 @@ final class ThemeOptimizer
|
||||
$result = ['css' => 0, 'js' => 0, 'images' => 0];
|
||||
foreach (['css' => 'css', 'js' => 'js'] as $type => $folder) {
|
||||
$directory = $theme->path().'/'.$folder;
|
||||
if (! $this->files->isDirectory($directory)) continue;
|
||||
if (! $this->files->isDirectory($directory)) {
|
||||
continue;
|
||||
}
|
||||
foreach ($this->files->files($directory) as $file) {
|
||||
if ($file->getExtension() !== $type || str_ends_with($file->getFilename(), '.min.'.$type)) continue;
|
||||
if ($file->getExtension() !== $type || str_ends_with($file->getFilename(), '.min.'.$type)) {
|
||||
continue;
|
||||
}
|
||||
$content = $file->getContents();
|
||||
$content = preg_replace('!/\*.*?\*/!s', '', $content) ?? $content;
|
||||
$content = preg_replace('/\s+/', ' ', $content) ?? $content;
|
||||
@@ -29,15 +33,25 @@ final class ThemeOptimizer
|
||||
if (function_exists('imagewebp')) {
|
||||
foreach (['images', 'icons'] as $folder) {
|
||||
$directory = $theme->path().'/'.$folder;
|
||||
if (! $this->files->isDirectory($directory)) continue;
|
||||
if (! $this->files->isDirectory($directory)) {
|
||||
continue;
|
||||
}
|
||||
foreach ($this->files->files($directory) as $file) {
|
||||
$ext = strtolower($file->getExtension());
|
||||
if (! in_array($ext, ['jpg', 'jpeg', 'png'], true)) continue;
|
||||
if (! in_array($ext, ['jpg', 'jpeg', 'png'], true)) {
|
||||
continue;
|
||||
}
|
||||
$image = $ext === 'png' ? @imagecreatefrompng($file->getPathname()) : @imagecreatefromjpeg($file->getPathname());
|
||||
if ($image) { imagepalettetotruecolor($image); imagewebp($image, $file->getPath().'/'.$file->getBasename('.'.$ext).'.webp', 82); imagedestroy($image); $result['images']++; }
|
||||
if ($image) {
|
||||
imagepalettetotruecolor($image);
|
||||
imagewebp($image, $file->getPath().'/'.$file->getBasename('.'.$ext).'.webp', 82);
|
||||
imagedestroy($image);
|
||||
$result['images']++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,28 +15,46 @@ final class ThemePackageManager
|
||||
|
||||
public function install(UploadedFile $upload): Theme
|
||||
{
|
||||
if (strtolower($upload->getClientOriginalExtension()) !== 'zip') throw new RuntimeException('Only ZIP theme packages may be installed.');
|
||||
if (strtolower($upload->getClientOriginalExtension()) !== 'zip') {
|
||||
throw new RuntimeException('Only ZIP theme packages may be installed.');
|
||||
}
|
||||
$temporary = storage_path('app/stylist/install-'.bin2hex(random_bytes(8)));
|
||||
$this->files->ensureDirectoryExists($temporary);
|
||||
$zip = new ZipArchive();
|
||||
if ($zip->open($upload->getRealPath()) !== true) throw new RuntimeException('The uploaded ZIP could not be opened.');
|
||||
for ($i=0; $i<$zip->numFiles; $i++) {
|
||||
$name = $zip->getNameIndex($i);
|
||||
if (str_contains($name, '..') || str_starts_with($name, '/') || str_contains($name, "\0")) { $zip->close(); throw new RuntimeException('The ZIP contains an unsafe path.'); }
|
||||
$zip = new ZipArchive;
|
||||
if ($zip->open($upload->getRealPath()) !== true) {
|
||||
throw new RuntimeException('The uploaded ZIP could not be opened.');
|
||||
}
|
||||
$zip->extractTo($temporary); $zip->close();
|
||||
for ($i = 0; $i < $zip->numFiles; $i++) {
|
||||
$name = $zip->getNameIndex($i);
|
||||
if (str_contains($name, '..') || str_starts_with($name, '/') || str_contains($name, "\0")) {
|
||||
$zip->close();
|
||||
throw new RuntimeException('The ZIP contains an unsafe path.');
|
||||
}
|
||||
}
|
||||
$zip->extractTo($temporary);
|
||||
$zip->close();
|
||||
$manifest = collect($this->files->allFiles($temporary))->first(fn ($file) => $file->getFilename() === 'theme.json');
|
||||
if (! $manifest) { $this->files->deleteDirectory($temporary); throw new RuntimeException('The package does not contain theme.json.'); }
|
||||
if (! $manifest) {
|
||||
$this->files->deleteDirectory($temporary);
|
||||
throw new RuntimeException('The package does not contain theme.json.');
|
||||
}
|
||||
$source = $manifest->getPath();
|
||||
$theme = $this->loader->fromPath($source);
|
||||
if ($theme->parent() && ! $this->loader->all()->has($theme->parent())) throw new RuntimeException('Parent theme '.$theme->parent().' is not installed.');
|
||||
if ($theme->parent() && ! $this->loader->all()->has($theme->parent())) {
|
||||
throw new RuntimeException('Parent theme '.$theme->parent().' is not installed.');
|
||||
}
|
||||
$issues = $this->validator->validate($theme);
|
||||
if (collect($issues)->contains('severity', 'error')) throw new RuntimeException('Accessibility validation failed: '.collect($issues)->where('severity', 'error')->pluck('message')->implode(' '));
|
||||
if (collect($issues)->contains('severity', 'error')) {
|
||||
throw new RuntimeException('Accessibility validation failed: '.collect($issues)->where('severity', 'error')->pluck('message')->implode(' '));
|
||||
}
|
||||
$target = rtrim((string) config('stylist.themes_path'), '/').'/'.$theme->key();
|
||||
if ($this->files->isDirectory($target)) $this->files->deleteDirectory($target);
|
||||
if ($this->files->isDirectory($target)) {
|
||||
$this->files->deleteDirectory($target);
|
||||
}
|
||||
$this->files->copyDirectory($source, $target);
|
||||
$this->files->deleteDirectory($temporary);
|
||||
$this->loader->flush();
|
||||
|
||||
return $this->loader->find($theme->key());
|
||||
}
|
||||
|
||||
@@ -44,17 +62,30 @@ final class ThemePackageManager
|
||||
{
|
||||
$projectRoot = realpath((string) config('stylist.themes_path'));
|
||||
$themePath = realpath($theme->path());
|
||||
if (! $projectRoot || ! $themePath || ! str_starts_with($themePath, $projectRoot.DIRECTORY_SEPARATOR)) throw new RuntimeException('Bundled themes cannot be uninstalled.');
|
||||
if (collect($this->loader->all())->contains(fn (Theme $candidate) => $candidate->parent() === $theme->key())) throw new RuntimeException('This theme has installed child themes.');
|
||||
$this->files->deleteDirectory($themePath); $this->loader->flush();
|
||||
if (! $projectRoot || ! $themePath || ! str_starts_with($themePath, $projectRoot.DIRECTORY_SEPARATOR)) {
|
||||
throw new RuntimeException('Bundled themes cannot be uninstalled.');
|
||||
}
|
||||
if (collect($this->loader->all())->contains(fn (Theme $candidate) => $candidate->parent() === $theme->key())) {
|
||||
throw new RuntimeException('This theme has installed child themes.');
|
||||
}
|
||||
$this->files->deleteDirectory($themePath);
|
||||
$this->loader->flush();
|
||||
}
|
||||
|
||||
public function export(Theme $theme): string
|
||||
{
|
||||
$directory = storage_path('app/stylist/exports'); $this->files->ensureDirectoryExists($directory);
|
||||
$directory = storage_path('app/stylist/exports');
|
||||
$this->files->ensureDirectoryExists($directory);
|
||||
$path = $directory.'/'.$theme->key().'-'.$theme->version().'.zip';
|
||||
$zip = new ZipArchive(); if ($zip->open($path, ZipArchive::CREATE|ZipArchive::OVERWRITE) !== true) throw new RuntimeException('Unable to create theme archive.');
|
||||
foreach ($this->files->allFiles($theme->path()) as $file) $zip->addFile($file->getPathname(), $theme->key().'/'.$file->getRelativePathname());
|
||||
$zip->close(); return $path;
|
||||
$zip = new ZipArchive;
|
||||
if ($zip->open($path, ZipArchive::CREATE | ZipArchive::OVERWRITE) !== true) {
|
||||
throw new RuntimeException('Unable to create theme archive.');
|
||||
}
|
||||
foreach ($this->files->allFiles($theme->path()) as $file) {
|
||||
$zip->addFile($file->getPathname(), $theme->key().'/'.$file->getRelativePathname());
|
||||
}
|
||||
$zip->close();
|
||||
|
||||
return $path;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ use Modules\Stylist\Theme\Theme;
|
||||
final class ThemeSettings
|
||||
{
|
||||
private const APPEARANCE_KEY = '__stylist_appearance';
|
||||
|
||||
private const ACCENT_KEY = '__stylist_accent';
|
||||
|
||||
public function get(Theme $theme): array
|
||||
@@ -24,6 +25,7 @@ final class ThemeSettings
|
||||
public function variant(Theme $theme): string
|
||||
{
|
||||
$variant = (string) ($this->stored($theme)?->variant ?? array_key_first($theme->variants()) ?? 'light');
|
||||
|
||||
return array_key_exists($variant, $theme->variants()) ? $variant : (array_key_first($theme->variants()) ?? 'light');
|
||||
}
|
||||
|
||||
@@ -31,8 +33,11 @@ final class ThemeSettings
|
||||
{
|
||||
$stored = (array) ($this->stored($theme)?->settings ?? []);
|
||||
$allowed = array_keys($theme->appearances());
|
||||
if ($allowed === []) return '';
|
||||
if ($allowed === []) {
|
||||
return '';
|
||||
}
|
||||
$appearance = (string) ($stored[self::APPEARANCE_KEY] ?? $theme->defaultAppearance());
|
||||
|
||||
return in_array($appearance, $allowed, true) ? $appearance : (string) array_key_first($theme->appearances());
|
||||
}
|
||||
|
||||
@@ -40,8 +45,11 @@ final class ThemeSettings
|
||||
{
|
||||
$stored = (array) ($this->stored($theme)?->settings ?? []);
|
||||
$allowed = array_keys($theme->accents());
|
||||
if ($allowed === []) return '';
|
||||
if ($allowed === []) {
|
||||
return '';
|
||||
}
|
||||
$accent = (string) ($stored[self::ACCENT_KEY] ?? $theme->defaultAccent());
|
||||
|
||||
return in_array($accent, $allowed, true) ? $accent : (string) array_key_first($theme->accents());
|
||||
}
|
||||
|
||||
@@ -53,18 +61,24 @@ final class ThemeSettings
|
||||
$allowedAppearances = array_keys($theme->appearances());
|
||||
if ($allowedAppearances !== []) {
|
||||
$appearance ??= (string) ($existing[self::APPEARANCE_KEY] ?? $theme->defaultAppearance());
|
||||
if (! in_array($appearance, $allowedAppearances, true)) $appearance = (string) array_key_first($theme->appearances());
|
||||
if (! in_array($appearance, $allowedAppearances, true)) {
|
||||
$appearance = (string) array_key_first($theme->appearances());
|
||||
}
|
||||
$settings[self::APPEARANCE_KEY] = $appearance;
|
||||
}
|
||||
|
||||
$allowedAccents = array_keys($theme->accents());
|
||||
if ($allowedAccents !== []) {
|
||||
$accent ??= (string) ($existing[self::ACCENT_KEY] ?? $theme->defaultAccent());
|
||||
if (! in_array($accent, $allowedAccents, true)) $accent = (string) array_key_first($theme->accents());
|
||||
if (! in_array($accent, $allowedAccents, true)) {
|
||||
$accent = (string) array_key_first($theme->accents());
|
||||
}
|
||||
$settings[self::ACCENT_KEY] = $accent;
|
||||
}
|
||||
|
||||
if ($theme->variants() && ! array_key_exists($variant, $theme->variants())) $variant = array_key_first($theme->variants());
|
||||
if ($theme->variants() && ! array_key_exists($variant, $theme->variants())) {
|
||||
$variant = array_key_first($theme->variants());
|
||||
}
|
||||
|
||||
return ThemeSetting::query()->updateOrCreate(
|
||||
['theme_key' => $theme->key()],
|
||||
@@ -74,7 +88,10 @@ final class ThemeSettings
|
||||
|
||||
private function stored(Theme $theme): ?ThemeSetting
|
||||
{
|
||||
try { return ThemeSetting::query()->where('theme_key', $theme->key())->first(); }
|
||||
catch (QueryException) { return null; }
|
||||
try {
|
||||
return ThemeSetting::query()->where('theme_key', $theme->key())->first();
|
||||
} catch (QueryException) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user