first commit
Some checks failed
Build / run (push) Has been cancelled

This commit is contained in:
maher
2025-10-29 11:42:25 +01:00
commit 703f50a09d
4595 changed files with 385164 additions and 0 deletions

View File

@@ -0,0 +1,97 @@
<?php namespace Common\Admin\Appearance;
use Common\Admin\Appearance\Themes\CssTheme;
use Common\Settings\DotEnvEditor;
use Common\Settings\Settings;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\File;
class AppearanceSaver
{
const CUSTOM_CSS_PATH = 'custom-code/custom-styles.css';
const CUSTOM_HTML_PATH = 'custom-code/custom-html.html';
public function __construct(
protected Settings $settings,
protected DotEnvEditor $envEditor,
) {
}
public function save(array $values): void
{
foreach ($values['appearance'] as $groupName => $groupValues) {
if ($groupName === 'env') {
$this->saveEnvSettings($groupValues);
} elseif ($groupName === 'custom_code') {
$this->saveCustomCode($groupValues);
} elseif ($groupName === 'themes') {
$this->syncThemes($groupValues['all']);
}
}
if (!empty($values['settings'])) {
//generate and store favicon
if (isset($values['settings']['branding']['favicon'])) {
$path = $values['settings']['branding']['favicon'];
unset($values['settings']['branding']['favicon']);
app(GenerateFavicon::class)->execute($path);
}
$this->settings->save($values['settings']);
}
}
private function saveEnvSettings(array $settings): void
{
foreach ($settings as $key => $value) {
$this->envEditor->write([
$key => $value,
]);
}
}
private function saveCustomCode(array $settings): void
{
foreach ($settings as $key => $value) {
$path =
$key === 'css' ? self::CUSTOM_CSS_PATH : self::CUSTOM_HTML_PATH;
if (!File::exists(public_path('storage/custom-code'))) {
File::makeDirectory(public_path('storage/custom-code'));
}
File::put(public_path("storage/$path"), trim($value));
}
}
private function syncThemes(array $themes): void
{
$dbThemes = CssTheme::get();
// delete themes that were removed in appearance editor
$dbThemes->each(function (CssTheme $theme) use ($themes) {
if (
!Arr::first(
$themes,
fn($current) => $current['id'] === $theme['id'],
)
) {
$theme->delete();
}
});
// update changed themes and create new ones
foreach ($themes as $theme) {
$existing = $dbThemes->find($theme['id']);
$newValue = Arr::except($theme, ['id', 'updated_at']);
if (!$existing) {
CssTheme::create(
array_merge($newValue, ['user_id' => Auth::id()]),
);
} else {
$existing->fill($newValue)->save();
}
}
}
}

View File

@@ -0,0 +1,127 @@
<?php namespace Common\Admin\Appearance;
use Common\Admin\Appearance\Themes\CssTheme;
use Common\Core\Prerender\MetaTags;
use Common\Settings\Settings;
use Exception;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\File;
class AppearanceValues
{
/**
* ENV values to include.
*/
const ENV_KEYS = ['app_name'];
public function get(): array
{
// split values into db settings and appearance specific settings, to avoid naming collisions
$values = [
'settings' => app(Settings::class)->getUnflattened(),
'appearance' => [],
];
// add env settings
$values['appearance']['env'] = [];
foreach (self::ENV_KEYS as $key) {
$values['appearance']['env'][$key] = config(
str_replace('_', '.', $key),
);
}
$values['appearance']['themes'] = [
'all' => CssTheme::get(),
'selectedThemeId' => null,
];
// add custom code
$values['appearance']['custom_code'] = [
'css' => $this->getCustomCodeValue(
AppearanceSaver::CUSTOM_CSS_PATH,
),
'html' => $this->getCustomCodeValue(
AppearanceSaver::CUSTOM_HTML_PATH,
),
];
$values['appearance']['seo'] = $this->prepareSeoValues();
$defaults = [];
$defaultSettings = collect(config('common.default-settings'))
->mapWithKeys(fn($item) => [$item['name'] => $item['value']])
->toArray();
$defaults['settings'] = settings()->getUnflattened(
false,
$defaultSettings,
);
$defaults['appearance']['themes'] = config('common.themes');
return [
'values' => $values,
'defaults' => $defaults,
];
}
private function prepareSeoValues(): array
{
$flat = [];
$seoConfig = config('seo');
if (!$seoConfig) {
return [];
}
$seo = Arr::except($seoConfig, 'common');
$seo = array_filter($seo, function ($config) {
return is_array($config);
});
// resource groups meta tags for artist, movie, track etc.
foreach ($seo as $resourceName => $resource) {
// resource has config for each verb (show, index etc.)
foreach ($resource as $verbName => $verb) {
// verb has a list of meta tags (og:title, description etc.)
if (is_array($verb)) {
foreach ($verb as $metaTag) {
$property = Arr::get($metaTag, 'property');
if (!in_array($property, MetaTags::EDITABLE_TAGS)) {
continue;
}
$name = str_replace(
'og:',
'',
"$resourceName / $verbName / $property",
);
$name = str_replace('-', ' ', $name);
$key = "seo.$resourceName.$verbName.$property";
$defaultValue = $metaTag['content'];
$flat[] = [
'name' => $name,
'key' => $key,
'value' => app(Settings::class)->get(
$key,
$defaultValue,
),
'defaultValue' => $defaultValue,
];
}
}
}
}
return $flat;
}
private function getCustomCodeValue($path): string
{
try {
return File::get(public_path("storage/$path"));
} catch (Exception $e) {
return '';
}
}
}

View File

@@ -0,0 +1,32 @@
<?php namespace Common\Admin\Appearance\Controllers;
use Common\Admin\Appearance\AppearanceSaver;
use Common\Admin\Appearance\AppearanceValues;
use Common\Core\BaseController;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class AppearanceController extends BaseController
{
public function __construct(
protected Request $request,
protected AppearanceValues $values,
protected AppearanceSaver $saver,
) {
}
public function save()
{
$this->authorize('update', 'AppearancePolicy');
$this->saver->save($this->request->get('changes'));
return $this->success();
}
public function getValues(): JsonResponse
{
$this->authorize('update', 'AppearancePolicy');
return $this->success($this->values->get());
}
}

View File

@@ -0,0 +1,55 @@
<?php
namespace Common\Admin\Appearance\Controllers;
use Common\Core\BaseController;
use Exception;
use Illuminate\Support\Facades\File;
class SeoTagsController extends BaseController
{
public function show(string $names)
{
$this->authorize('update', 'AppearancePolicy');
$names = explode(',', $names);
$response = [];
foreach ($names as $name) {
try {
$customView = storage_path(
"app/editable-views/seo-tags/$name.blade.php",
);
$response[$name] = [
'custom' => file_exists($customView)
? file_get_contents($customView)
: null,
'original' => file_get_contents(
resource_path("views/seo/$name/seo-tags.blade.php"),
),
];
} catch (Exception $e) {
//
}
}
return $this->success($response);
}
public function update(string $name)
{
$this->authorize('update', 'AppearancePolicy');
$data = $this->validate(request(), [
'tags' => 'required|string',
]);
$directory = storage_path('app/editable-views/seo-tags');
File::ensureDirectoryExists($directory);
file_put_contents("$directory/$name.blade.php", $data['tags']);
return $this->success();
}
}

View File

@@ -0,0 +1,39 @@
<?php
namespace Common\Admin\Appearance\Events;
class AppearanceSettingSaved
{
/**
* @var string
*/
public $type;
/**
* @var string
*/
public $key;
/**
* @var string
*/
public $value;
/**
* @var string
*/
public $previousValue;
public function __construct(
string $type,
string $key,
string $value,
string $previousValue = null
) {
//
$this->type = $type;
$this->key = $key;
$this->value = $value;
$this->previousValue = $previousValue;
}
}

View File

@@ -0,0 +1,65 @@
<?php
namespace Common\Admin\Appearance;
use Common\Settings\Settings;
use Illuminate\Support\Facades\File;
use Intervention\Image\Drivers\Gd\Driver;
use Intervention\Image\ImageManager;
class GenerateFavicon
{
const FAVICON_DIR = 'favicon';
protected string $absoluteFaviconDir;
protected array $sizes = [
[72, 72],
[96, 96],
[128, 128],
[144, 144],
[152, 152],
[192, 192],
[384, 384],
[512, 512],
];
protected string $initialFilePath;
public function __construct()
{
$this->absoluteFaviconDir = public_path(self::FAVICON_DIR);
}
public function execute(string $filePath): void
{
if (str_starts_with($filePath, 'http') || !file_exists($filePath)) {
return;
}
File::ensureDirectoryExists($this->absoluteFaviconDir);
$this->initialFilePath = $filePath;
foreach ($this->sizes as $size) {
$this->generateFaviconForSize($size);
}
$this->generateFaviconForSize([16, 16], public_path(), 'favicon.ico');
$uri = self::FAVICON_DIR . '/icon-144x144.png?v=' . time();
app(Settings::class)->save(['branding.favicon' => $uri]);
}
private function generateFaviconForSize(
array $size,
string $dir = null,
string $name = null,
): void {
$manager = new ImageManager(new Driver());
$img = $manager->read($this->initialFilePath);
$img->coverDown($size[0], $size[1]);
$dir = $dir ?? $this->absoluteFaviconDir;
$name = $name ?? "icon-$size[0]x$size[1].png";
$img->toPng()->save("$dir/$name");
}
}

View File

@@ -0,0 +1,33 @@
<?php
namespace Common\Admin\Appearance\Themes;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Auth;
class CrupdateCssTheme
{
public function execute(array $data, CssTheme $cssTheme = null): ?CssTheme
{
if (!$cssTheme) {
$cssTheme = CssTheme::newInstance([
'user_id' => Auth::id(),
'values' => $data['is_dark']
? config('common.themes.dark')
: config('common.themes.light'),
]);
}
$attributes = Arr::only($data, [
'name',
'is_dark',
'default_dark',
'default_light',
'values',
]);
$cssTheme->fill($attributes)->save();
return $cssTheme;
}
}

View File

@@ -0,0 +1,28 @@
<?php
namespace Common\Admin\Appearance\Themes;
use Auth;
use Common\Core\BaseFormRequest;
use Illuminate\Validation\Rule;
class CrupdateCssThemeRequest extends BaseFormRequest
{
public function rules(): array
{
$required = $this->getMethod() === 'POST' ? 'required' : '';
$ignore = $this->getMethod() === 'PUT' ? $this->route('css_theme')->id : '';
$userId = $this->route('css_theme') ? $this->route('css_theme')->user_id : Auth::id();
return [
'name' => [
$required, 'string', 'min:3',
Rule::unique('css_themes')->where('user_id', $userId)->ignore($ignore)
],
'is_dark' => 'boolean',
'default_dark' => 'boolean',
'default_light' => 'boolean',
'colors' => 'array',
];
}
}

View File

@@ -0,0 +1,75 @@
<?php
namespace Common\Admin\Appearance\Themes;
use Illuminate\Database\Eloquent\Model;
class CssTheme extends Model
{
protected $guarded = ['id'];
protected $casts = [
'id' => 'integer',
'user_id' => 'integer',
'is_dark' => 'boolean',
'default_dark' => 'boolean',
'default_light' => 'boolean',
'font' => 'json',
];
const MODEL_TYPE = 'css_theme';
public static function getModelTypeAttribute(): string
{
return self::MODEL_TYPE;
}
public function setValuesAttribute($value)
{
if ($value && is_array($value)) {
$this->attributes['values'] = json_encode($value);
}
}
public function getValuesAttribute($value): array
{
if ($value && is_string($value)) {
return json_decode($value, true);
} else {
return [];
}
}
public function getCssVariables(): string
{
// don't decode from json
$values = $this->attributes['values'] ?? '';
$values = preg_replace('/"/', '', $values);
$values = preg_replace('/\\\/', '', $values);
$values = preg_replace('/[{}]/', '', $values);
$values = preg_replace('/, ?--/', ';--', $values);
if ($family = $this->getFontFamily()) {
$values .= ";--be-font-family: $family";
}
return $values;
}
public function getFontFamily(): string|null
{
return $this->font['family'] ?? null;
}
public function isGoogleFont(): bool
{
return $this->font['google'] ?? false;
}
public function getHtmlThemeColor()
{
if ($this->is_dark) {
return $this->values['--be-background-alt'];
} else {
return $this->values['--be-primary'];
}
}
}

View File

@@ -0,0 +1,76 @@
<?php
namespace Common\Admin\Appearance\Themes;
use Common\Core\BaseController;
use Common\Database\Datasource\Datasource;
use Illuminate\Http\Request;
class CssThemeController extends BaseController
{
/**
* @var CssTheme
*/
private $cssTheme;
/**
* @var Request
*/
private $request;
public function __construct(CssTheme $cssTheme, Request $request)
{
$this->cssTheme = $cssTheme;
$this->request = $request;
}
public function index()
{
$userId = $this->request->get('userId');
$this->authorize('index', [CssTheme::class, $userId]);
$builder = $this->cssTheme->newQuery();
if ($userId) {
$builder->where('user_id', $userId);
}
$dataSource = new Datasource($this->cssTheme, $this->request->all());
$pagination = $dataSource->paginate();
return $this->success(['pagination' => $pagination]);
}
public function show(CssTheme $cssTheme)
{
$this->authorize('show', $cssTheme);
return $this->success(['theme' => $cssTheme]);
}
public function store(CrupdateCssThemeRequest $request)
{
$this->authorize('store', CssTheme::class);
$cssTheme = app(CrupdateCssTheme::class)->execute($request->all());
return $this->success(['theme' => $cssTheme]);
}
public function update(CssTheme $cssTheme, CrupdateCssThemeRequest $request)
{
$this->authorize('store', $cssTheme);
$cssTheme = app(CrupdateCssTheme::class)->execute($request->all(), $cssTheme);
return $this->success(['theme' => $cssTheme]);
}
public function destroy(CssTheme $cssTheme)
{
$this->authorize('destroy', $cssTheme);
$cssTheme->delete();
return $this->success();
}
}

View File

@@ -0,0 +1,46 @@
<?php
namespace Common\Admin\Appearance\Themes;
use Common\Auth\BaseUser;
use Common\Core\Policies\BasePolicy;
class CssThemePolicy extends BasePolicy
{
public function index(BaseUser $user, $userId = null)
{
return $user->hasPermission('cssTheme.view') || $user->id === (int) $userId;
}
public function show(BaseUser $user, CssTheme $cssTheme)
{
return $user->hasPermission('cssTheme.view') || $cssTheme->user_id === $user->id;
}
public function store(BaseUser $user)
{
return $user->hasPermission('cssTheme.create');
}
public function update(BaseUser $user, CssTheme $cssTheme)
{
return $user->hasPermission('cssTheme.update') || $cssTheme->user_id === $user->id;
}
public function destroy(BaseUser $user, CssTheme $theme)
{
if ($theme->default_dark && app(CssTheme::class)->where('default_dark', true)->count() < 2) {
return $this->deny("Default dark theme can't be deleted");
}
if ($theme->default_light && app(CssTheme::class)->where('default_light', true)->count() < 2) {
return $this->deny("Default light theme can't be deleted");
}
if (app(CssTheme::class)->count() <= 1) {
return $this->deny("All themes can't be deleted");
}
return $user->hasPermission('cssTheme.delete') || $theme->user_id === $user->id;
}
}