mirror of
https://github.com/pelican-dev/panel.git
synced 2026-05-04 18:00:48 +03:00
Refactor egg & server icon uploading (#2281)
This commit is contained in:
@@ -5,8 +5,10 @@ namespace App\Filament\Admin\Resources\Eggs\Pages;
|
||||
use App\Enums\EditorLanguages;
|
||||
use App\Enums\TablerIcon;
|
||||
use App\Filament\Admin\Resources\Eggs\EggResource;
|
||||
use App\Filament\Components\Actions\DeleteIcon;
|
||||
use App\Filament\Components\Actions\ExportEggAction;
|
||||
use App\Filament\Components\Actions\ImportEggAction;
|
||||
use App\Filament\Components\Actions\UploadIcon;
|
||||
use App\Filament\Components\Forms\Fields\CopyFrom;
|
||||
use App\Filament\Components\Forms\Fields\MonacoEditor;
|
||||
use App\Models\Egg;
|
||||
@@ -14,12 +16,10 @@ use App\Models\EggVariable;
|
||||
use App\Traits\Filament\CanCustomizeHeaderActions;
|
||||
use App\Traits\Filament\CanCustomizeHeaderWidgets;
|
||||
use App\Traits\Filament\CanCustomizeTabs;
|
||||
use Exception;
|
||||
use Filament\Actions\Action;
|
||||
use Filament\Actions\ActionGroup;
|
||||
use Filament\Actions\DeleteAction;
|
||||
use Filament\Forms\Components\Checkbox;
|
||||
use Filament\Forms\Components\FileUpload;
|
||||
use Filament\Forms\Components\Hidden;
|
||||
use Filament\Forms\Components\KeyValue;
|
||||
use Filament\Forms\Components\Repeater;
|
||||
@@ -28,11 +28,8 @@ use Filament\Forms\Components\TagsInput;
|
||||
use Filament\Forms\Components\Textarea;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Components\Toggle;
|
||||
use Filament\Infolists\Components\TextEntry;
|
||||
use Filament\Notifications\Notification;
|
||||
use Filament\Resources\Pages\EditRecord;
|
||||
use Filament\Schemas\Components\Fieldset;
|
||||
use Filament\Schemas\Components\Flex;
|
||||
use Filament\Schemas\Components\Grid;
|
||||
use Filament\Schemas\Components\Image;
|
||||
use Filament\Schemas\Components\Tabs;
|
||||
@@ -40,10 +37,7 @@ use Filament\Schemas\Components\Tabs\Tab;
|
||||
use Filament\Schemas\Components\Utilities\Get;
|
||||
use Filament\Schemas\Components\Utilities\Set;
|
||||
use Filament\Schemas\Schema;
|
||||
use Filament\Support\Enums\IconSize;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Validation\Rules\Unique;
|
||||
use Livewire\Features\SupportFileUploads\TemporaryUploadedFile;
|
||||
|
||||
class EditEgg extends EditRecord
|
||||
{
|
||||
@@ -74,163 +68,17 @@ class EditEgg extends EditRecord
|
||||
->icon(TablerIcon::Egg)
|
||||
->schema([
|
||||
Grid::make(2)
|
||||
->columnSpan(1)
|
||||
->columnStart(1)
|
||||
->schema([
|
||||
Image::make('', '')
|
||||
->hidden(fn ($record) => !$record->image)
|
||||
->url(fn ($record) => $record->image)
|
||||
->alt('')
|
||||
->alignJustify()
|
||||
Image::make('', 'icon')
|
||||
->hidden(fn ($record) => !$record->icon)
|
||||
->url(fn ($record) => $record->icon)
|
||||
->imageSize(150)
|
||||
->columnSpanFull(),
|
||||
Flex::make([
|
||||
Action::make('uploadImage')
|
||||
->hiddenLabel()
|
||||
->tooltip(trans('admin/egg.import.import_image'))
|
||||
->iconSize(IconSize::Large)
|
||||
->icon(TablerIcon::PhotoUp)
|
||||
->modal()
|
||||
->modalHeading('')
|
||||
->modalSubmitActionLabel(trans('admin/egg.import.import_image'))
|
||||
->schema([
|
||||
Tabs::make()
|
||||
->contained(false)
|
||||
->tabs([
|
||||
Tab::make(trans('admin/egg.import.url'))
|
||||
->schema([
|
||||
Hidden::make('imageUrl'),
|
||||
Hidden::make('imageExtension'),
|
||||
TextInput::make('image_url')
|
||||
->label(trans('admin/egg.import.image_url'))
|
||||
->reactive()
|
||||
->autocomplete(false)
|
||||
->debounce(500)
|
||||
->afterStateUpdated(function ($state, Set $set) {
|
||||
if (!$state) {
|
||||
$set('image_url_error', null);
|
||||
$set('imageUrl', null);
|
||||
$set('imageExtension', null);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (!filter_var($state, FILTER_VALIDATE_URL)) {
|
||||
throw new Exception(trans('admin/egg.import.invalid_url'));
|
||||
}
|
||||
|
||||
$extension = strtolower(pathinfo(parse_url($state, PHP_URL_PATH), PATHINFO_EXTENSION));
|
||||
|
||||
if (!array_key_exists($extension, Egg::IMAGE_FORMATS)) {
|
||||
throw new Exception(trans('admin/egg.import.unsupported_format', ['format' => implode(', ', array_keys(Egg::IMAGE_FORMATS))]));
|
||||
}
|
||||
|
||||
$host = parse_url($state, PHP_URL_HOST);
|
||||
$ip = gethostbyname($host);
|
||||
|
||||
if (
|
||||
filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) === false
|
||||
) {
|
||||
throw new Exception(trans('admin/egg.import.no_local_ip'));
|
||||
}
|
||||
|
||||
$set('imageUrl', $state);
|
||||
$set('imageExtension', $extension);
|
||||
$set('image_url_error', null);
|
||||
|
||||
} catch (Exception $e) {
|
||||
$set('image_url_error', $e->getMessage());
|
||||
$set('imageUrl', null);
|
||||
$set('imageExtension', null);
|
||||
}
|
||||
}),
|
||||
TextEntry::make('image_url_error')
|
||||
->hiddenLabel()
|
||||
->visible(fn ($get) => $get('image_url_error') !== null)
|
||||
->afterStateHydrated(fn ($set, $get) => $get('image_url_error')),
|
||||
Image::make(fn (Get $get) => $get('image_url'), '')
|
||||
->imageSize(150)
|
||||
->visible(fn ($get) => $get('image_url') && !$get('image_url_error'))
|
||||
->alignCenter(),
|
||||
]),
|
||||
Tab::make(trans('admin/egg.import.file'))
|
||||
->schema([
|
||||
FileUpload::make('image')
|
||||
->hiddenLabel()
|
||||
->previewable()
|
||||
->openable(false)
|
||||
->downloadable(false)
|
||||
->maxSize(256)
|
||||
->maxFiles(1)
|
||||
->columnSpanFull()
|
||||
->alignCenter()
|
||||
->imageEditor()
|
||||
->image()
|
||||
->disk('public')
|
||||
->directory(Egg::ICON_STORAGE_PATH)
|
||||
->acceptedFileTypes([
|
||||
'image/png',
|
||||
'image/jpeg',
|
||||
'image/webp',
|
||||
'image/svg+xml',
|
||||
])
|
||||
->getUploadedFileNameForStorageUsing(function (TemporaryUploadedFile $file, $record) {
|
||||
return $record->uuid . '.' . $file->getClientOriginalExtension();
|
||||
}),
|
||||
]),
|
||||
]),
|
||||
])
|
||||
->action(function (array $data, $record): void {
|
||||
if (!empty($data['imageUrl']) && !empty($data['imageExtension'])) {
|
||||
$this->saveImageFromUrl($data['imageUrl'], $data['imageExtension'], $record);
|
||||
|
||||
Notification::make()
|
||||
->title(trans('admin/egg.import.image_updated'))
|
||||
->success()
|
||||
->send();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!empty($data['image'])) {
|
||||
Notification::make()
|
||||
->title(trans('admin/egg.import.image_updated'))
|
||||
->success()
|
||||
->send();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (empty($data['imageUrl']) && empty($data['image'])) {
|
||||
Notification::make()
|
||||
->title(trans('admin/egg.import.no_image'))
|
||||
->warning()
|
||||
->send();
|
||||
}
|
||||
}),
|
||||
Action::make('delete_image')
|
||||
->visible(fn ($record) => $record->image)
|
||||
->hiddenLabel()
|
||||
->tooltip(trans('admin/egg.import.delete_image'))
|
||||
->icon(TablerIcon::Trash)
|
||||
->iconSize(IconSize::Large)
|
||||
->color('danger')
|
||||
->action(function ($record) {
|
||||
foreach (array_keys(Egg::IMAGE_FORMATS) as $ext) {
|
||||
$path = Egg::ICON_STORAGE_PATH . "/$record->uuid.$ext";
|
||||
if (Storage::disk('public')->exists($path)) {
|
||||
Storage::disk('public')->delete($path);
|
||||
}
|
||||
}
|
||||
|
||||
Notification::make()
|
||||
->title(trans('admin/egg.import.image_deleted'))
|
||||
->success()
|
||||
->send();
|
||||
|
||||
$record->refresh();
|
||||
}),
|
||||
]),
|
||||
->columnSpanFull()
|
||||
->alignJustify(),
|
||||
UploadIcon::make(),
|
||||
DeleteIcon::make()
|
||||
->iconStoragePath(Egg::getIconStoragePath()),
|
||||
]),
|
||||
TextInput::make('name')
|
||||
->label(trans('admin/egg.name'))
|
||||
@@ -469,39 +317,6 @@ class EditEgg extends EditRecord
|
||||
$this->fillForm();
|
||||
}
|
||||
|
||||
/**
|
||||
* Save an image from URL download to a file.
|
||||
*
|
||||
* @throws Exception
|
||||
*/
|
||||
private function saveImageFromUrl(string $imageUrl, string $extension, Egg $egg): void
|
||||
{
|
||||
$context = stream_context_create([
|
||||
'http' => ['timeout' => 3],
|
||||
'https' => [
|
||||
'timeout' => 3,
|
||||
'verify_peer' => true,
|
||||
'verify_peer_name' => true,
|
||||
],
|
||||
]);
|
||||
|
||||
$normalizedExtension = match ($extension) {
|
||||
'svg+xml', 'svg' => 'svg',
|
||||
'jpeg', 'jpg' => 'jpg',
|
||||
'png' => 'png',
|
||||
'webp' => 'webp',
|
||||
default => throw new Exception(trans('admin/egg.import.unknown_extension')),
|
||||
};
|
||||
|
||||
$data = @file_get_contents($imageUrl, false, $context, 0, 1048576); // 1024KB
|
||||
|
||||
if (empty($data)) {
|
||||
throw new Exception(trans('admin/egg.import.invalid_url'));
|
||||
}
|
||||
|
||||
Storage::disk('public')->put(Egg::ICON_STORAGE_PATH . "/$egg->uuid.$normalizedExtension", $data);
|
||||
}
|
||||
|
||||
protected function getFormActions(): array
|
||||
{
|
||||
return [];
|
||||
|
||||
@@ -38,6 +38,8 @@ class ListEggs extends ListRecords
|
||||
*/
|
||||
public function table(Table $table): Table
|
||||
{
|
||||
$defaultEggIcon = 'data:image/svg+xml;base64,' . base64_encode(file_get_contents(public_path('pelican.svg')));
|
||||
|
||||
return $table
|
||||
->searchable(true)
|
||||
->defaultPaginationPageOption(25)
|
||||
@@ -45,13 +47,11 @@ class ListEggs extends ListRecords
|
||||
TextColumn::make('id')
|
||||
->label('Id')
|
||||
->hidden(),
|
||||
ImageColumn::make('image')
|
||||
ImageColumn::make('icon')
|
||||
->label('')
|
||||
->alignCenter()
|
||||
->circular()
|
||||
->getStateUsing(fn ($record) => $record->image
|
||||
? $record->image
|
||||
: 'data:image/svg+xml;base64,' . base64_encode(file_get_contents(public_path('pelican.svg')))),
|
||||
->getStateUsing(fn (Egg $record) => $record->icon ?: $defaultEggIcon),
|
||||
TextColumn::make('name')
|
||||
->label(trans('admin/egg.name'))
|
||||
->description(fn ($record): ?string => (strlen($record->description) > 120) ? substr($record->description, 0, 120).'...' : $record->description)
|
||||
|
||||
@@ -5,8 +5,9 @@ namespace App\Filament\Admin\Resources\Servers\Pages;
|
||||
use App\Enums\SuspendAction;
|
||||
use App\Enums\TablerIcon;
|
||||
use App\Filament\Admin\Resources\Servers\ServerResource;
|
||||
use App\Filament\Components\Actions\DeleteServerIcon;
|
||||
use App\Filament\Components\Actions\DeleteIcon;
|
||||
use App\Filament\Components\Actions\PreviewStartupAction;
|
||||
use App\Filament\Components\Actions\UploadIcon;
|
||||
use App\Filament\Components\Forms\Fields\MonacoEditor;
|
||||
use App\Filament\Components\Forms\Fields\StartupVariable;
|
||||
use App\Filament\Components\StateCasts\ServerConditionStateCast;
|
||||
@@ -31,7 +32,6 @@ use Exception;
|
||||
use Filament\Actions\Action;
|
||||
use Filament\Actions\ActionGroup;
|
||||
use Filament\Forms\Components\CheckboxList;
|
||||
use Filament\Forms\Components\FileUpload;
|
||||
use Filament\Forms\Components\Hidden;
|
||||
use Filament\Forms\Components\KeyValue;
|
||||
use Filament\Forms\Components\Repeater;
|
||||
@@ -41,7 +41,6 @@ use Filament\Forms\Components\Textarea;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Components\Toggle;
|
||||
use Filament\Forms\Components\ToggleButtons;
|
||||
use Filament\Infolists\Components\TextEntry;
|
||||
use Filament\Notifications\Notification;
|
||||
use Filament\Resources\Pages\EditRecord;
|
||||
use Filament\Schemas\Components\Actions;
|
||||
@@ -60,9 +59,7 @@ use Filament\Support\Enums\Alignment;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Http\Client\ConnectionException;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\HtmlString;
|
||||
use Livewire\Features\SupportFileUploads\TemporaryUploadedFile;
|
||||
use LogicException;
|
||||
use Random\RandomException;
|
||||
|
||||
@@ -111,141 +108,18 @@ class EditServer extends EditRecord
|
||||
->icon(TablerIcon::InfoCircle)
|
||||
->schema([
|
||||
Grid::make()
|
||||
->columns(2)
|
||||
->columnStart(1)
|
||||
->schema([
|
||||
Image::make('', 'icon')
|
||||
->hidden(fn ($record) => !$record->icon && !$record->egg->image)
|
||||
->url(fn ($record) => $record->icon ?: $record->egg->image)
|
||||
->hidden(fn ($record) => !$record->icon && !$record->egg->icon)
|
||||
->url(fn ($record) => $record->icon ?: $record->egg->icon)
|
||||
->tooltip(fn ($record) => $record->icon ? '' : trans('server/setting.server_info.icon.tooltip'))
|
||||
->columnSpan(2)
|
||||
->imageSize(150)
|
||||
->columnSpanFull()
|
||||
->alignJustify(),
|
||||
Action::make('uploadIcon')
|
||||
->hiddenLabel()
|
||||
->icon(TablerIcon::PhotoUp)
|
||||
->tooltip(trans('admin/server.import_image'))
|
||||
->modal()
|
||||
->modalSubmitActionLabel(trans('server/setting.server_info.icon.upload'))
|
||||
->schema([
|
||||
Tabs::make()
|
||||
->contained(false)
|
||||
->tabs([
|
||||
Tab::make(trans('admin/egg.import.url'))
|
||||
->schema([
|
||||
Hidden::make('imageUrl'),
|
||||
Hidden::make('imageExtension'),
|
||||
TextInput::make('image_url')
|
||||
->label(trans('admin/egg.import.image_url'))
|
||||
->reactive()
|
||||
->autocomplete(false)
|
||||
->debounce(500)
|
||||
->afterStateUpdated(function ($state, Set $set) {
|
||||
if (!$state) {
|
||||
$set('image_url_error', null);
|
||||
$set('imageUrl', null);
|
||||
$set('imageExtension', null);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (!in_array(parse_url($state, PHP_URL_SCHEME), ['http', 'https'], true)) {
|
||||
throw new Exception(trans('admin/egg.import.invalid_url'));
|
||||
}
|
||||
|
||||
if (!filter_var($state, FILTER_VALIDATE_URL)) {
|
||||
throw new Exception(trans('admin/egg.import.invalid_url'));
|
||||
}
|
||||
|
||||
$extension = strtolower(pathinfo(parse_url($state, PHP_URL_PATH), PATHINFO_EXTENSION));
|
||||
|
||||
if (!array_key_exists($extension, Server::IMAGE_FORMATS)) {
|
||||
throw new Exception(trans('admin/egg.import.unsupported_format', ['format' => implode(', ', array_keys(Server::IMAGE_FORMATS))]));
|
||||
}
|
||||
|
||||
$host = parse_url($state, PHP_URL_HOST);
|
||||
$ip = gethostbyname($host);
|
||||
|
||||
if (
|
||||
filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) === false
|
||||
) {
|
||||
throw new Exception(trans('admin/egg.import.no_local_ip'));
|
||||
}
|
||||
|
||||
$set('imageUrl', $state);
|
||||
$set('imageExtension', $extension);
|
||||
$set('image_url_error', null);
|
||||
|
||||
} catch (Exception $e) {
|
||||
$set('image_url_error', $e->getMessage());
|
||||
$set('imageUrl', null);
|
||||
$set('imageExtension', null);
|
||||
}
|
||||
}),
|
||||
TextEntry::make('image_url_error')
|
||||
->hiddenLabel()
|
||||
->visible(fn (Get $get) => $get('image_url_error') !== null)
|
||||
->afterStateHydrated(fn (Get $get) => $get('image_url_error')),
|
||||
Image::make(fn (Get $get) => $get('image_url'), '')
|
||||
->imageSize(150)
|
||||
->visible(fn (Get $get) => $get('image_url') && !$get('image_url_error'))
|
||||
->alignCenter(),
|
||||
]),
|
||||
Tab::make(trans('admin/egg.import.file'))
|
||||
->schema([
|
||||
FileUpload::make('image')
|
||||
->hiddenLabel()
|
||||
->previewable()
|
||||
->openable(false)
|
||||
->downloadable(false)
|
||||
->maxSize(256)
|
||||
->maxFiles(1)
|
||||
->columnSpanFull()
|
||||
->alignCenter()
|
||||
->imageEditor()
|
||||
->image()
|
||||
->disk('public')
|
||||
->directory(Server::ICON_STORAGE_PATH)
|
||||
->acceptedFileTypes([
|
||||
'image/png',
|
||||
'image/jpeg',
|
||||
'image/webp',
|
||||
'image/svg+xml',
|
||||
])
|
||||
->getUploadedFileNameForStorageUsing(function (TemporaryUploadedFile $file, $record) {
|
||||
return $record->uuid . '.' . $file->getClientOriginalExtension();
|
||||
}),
|
||||
]),
|
||||
]),
|
||||
])
|
||||
->action(function (array $data, $record): void {
|
||||
if (!empty($data['imageUrl']) && !empty($data['imageExtension'])) {
|
||||
$this->saveIconFromUrl($data['imageUrl'], $data['imageExtension'], $record);
|
||||
Notification::make()
|
||||
->title(trans('server/setting.server_info.icon.updated'))
|
||||
->success()
|
||||
->send();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!empty($data['image'])) {
|
||||
Notification::make()
|
||||
->title(trans('server/setting.server_info.icon.updated'))
|
||||
->success()
|
||||
->send();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (empty($data['imageUrl']) && empty($data['image'])) {
|
||||
Notification::make()
|
||||
->title(trans('admin/egg.import.no_image'))
|
||||
->warning()
|
||||
->send();
|
||||
}
|
||||
}),
|
||||
DeleteServerIcon::make(),
|
||||
UploadIcon::make(),
|
||||
DeleteIcon::make()
|
||||
->iconStoragePath(Server::getIconStoragePath()),
|
||||
]),
|
||||
Grid::make()
|
||||
->columns(3)
|
||||
@@ -1202,37 +1076,4 @@ class EditServer extends EditRecord
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Save an icon from URL download to a file.
|
||||
*
|
||||
* @throws Exception
|
||||
*/
|
||||
private function saveIconFromUrl(string $imageUrl, string $extension, Server $server): void
|
||||
{
|
||||
$context = stream_context_create([
|
||||
'http' => ['timeout' => 3],
|
||||
'https' => [
|
||||
'timeout' => 3,
|
||||
'verify_peer' => true,
|
||||
'verify_peer_name' => true,
|
||||
],
|
||||
]);
|
||||
|
||||
$normalizedExtension = match ($extension) {
|
||||
'svg+xml', 'svg' => 'svg',
|
||||
'jpeg', 'jpg' => 'jpg',
|
||||
'png' => 'png',
|
||||
'webp' => 'webp',
|
||||
default => throw new Exception(trans('admin/egg.import.unknown_extension')),
|
||||
};
|
||||
|
||||
$data = @file_get_contents($imageUrl, false, $context, 0, 262144); //256KB
|
||||
|
||||
if (empty($data)) {
|
||||
throw new Exception(trans('admin/egg.import.invalid_url'));
|
||||
}
|
||||
|
||||
Storage::disk('public')->put(Server::ICON_STORAGE_PATH . "/$server->uuid.$normalizedExtension", $data);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,7 +70,7 @@ class ListServers extends ListRecords
|
||||
ImageColumn::make('icon')
|
||||
->label('')
|
||||
->imageSize(46)
|
||||
->state(fn (Server $server) => $server->icon ?: $server->egg->image),
|
||||
->state(fn (Server $server) => $server->icon ?: $server->egg->icon),
|
||||
TextColumn::make('condition')
|
||||
->label(trans('server/dashboard.status'))
|
||||
->badge()
|
||||
|
||||
79
app/Filament/Components/Actions/DeleteIcon.php
Normal file
79
app/Filament/Components/Actions/DeleteIcon.php
Normal file
@@ -0,0 +1,79 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Components\Actions;
|
||||
|
||||
use App\Enums\TablerIcon;
|
||||
use App\Models\Traits\HasIcon;
|
||||
use Filament\Actions\Action;
|
||||
use Filament\Notifications\Notification;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
class DeleteIcon extends Action
|
||||
{
|
||||
/** @var string[] */
|
||||
protected ?array $iconFormats = null;
|
||||
|
||||
protected ?string $iconStoragePath = null;
|
||||
|
||||
public static function getDefaultName(): ?string
|
||||
{
|
||||
return 'delete_icon';
|
||||
}
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->visible(fn ($record) => $record->icon);
|
||||
|
||||
$this->hiddenLabel();
|
||||
|
||||
$this->tooltip(trans('admin/egg.import.delete_icon'));
|
||||
|
||||
$this->icon(TablerIcon::Trash);
|
||||
|
||||
$this->color('danger');
|
||||
|
||||
$this->action(function ($record) {
|
||||
foreach ($this->getIconFormats() as $ext) {
|
||||
$path = $this->getIconStoragePath() . "/$record->uuid.$ext";
|
||||
if (Storage::disk('public')->exists($path)) {
|
||||
Storage::disk('public')->delete($path);
|
||||
}
|
||||
}
|
||||
|
||||
Notification::make()
|
||||
->title(trans('admin/egg.import.icon_deleted'))
|
||||
->success()
|
||||
->send();
|
||||
|
||||
$record->refresh();
|
||||
});
|
||||
}
|
||||
|
||||
/** @param string[] $iconFormats */
|
||||
public function iconFormats(?array $iconFormats): static
|
||||
{
|
||||
$this->iconFormats = $iconFormats;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function iconStoragePath(?string $iconStoragePath): static
|
||||
{
|
||||
$this->iconStoragePath = $iconStoragePath;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/** @return string[] */
|
||||
public function getIconFormats(): array
|
||||
{
|
||||
return $this->iconFormats ?? array_keys(HasIcon::$iconFormats);
|
||||
}
|
||||
|
||||
public function getIconStoragePath(): ?string
|
||||
{
|
||||
return $this->iconStoragePath;
|
||||
}
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Components\Actions;
|
||||
|
||||
use App\Enums\TablerIcon;
|
||||
use App\Models\Server;
|
||||
use Filament\Actions\Action;
|
||||
use Filament\Notifications\Notification;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
class DeleteServerIcon extends Action
|
||||
{
|
||||
public static function getDefaultName(): ?string
|
||||
{
|
||||
return 'delete_icon';
|
||||
}
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->visible(fn ($record) => $record->icon);
|
||||
|
||||
$this->hiddenLabel();
|
||||
|
||||
$this->tooltip(trans('admin/server.import_image'));
|
||||
|
||||
$this->icon(TablerIcon::Trash);
|
||||
|
||||
$this->color('danger');
|
||||
|
||||
$this->action(function ($record) {
|
||||
foreach (array_keys(Server::IMAGE_FORMATS) as $ext) {
|
||||
$path = Server::ICON_STORAGE_PATH . "/$record->uuid.$ext";
|
||||
if (Storage::disk('public')->exists($path)) {
|
||||
Storage::disk('public')->delete($path);
|
||||
}
|
||||
}
|
||||
|
||||
Notification::make()
|
||||
->title(trans('server/setting.server_info.icon.deleted'))
|
||||
->success()
|
||||
->send();
|
||||
|
||||
$record->refresh();
|
||||
});
|
||||
}
|
||||
}
|
||||
164
app/Filament/Components/Actions/UploadIcon.php
Normal file
164
app/Filament/Components/Actions/UploadIcon.php
Normal file
@@ -0,0 +1,164 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Components\Actions;
|
||||
|
||||
use App\Enums\TablerIcon;
|
||||
use App\Models\Traits\HasIcon;
|
||||
use Exception;
|
||||
use Filament\Actions\Action;
|
||||
use Filament\Forms\Components\FileUpload;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Infolists\Components\TextEntry;
|
||||
use Filament\Notifications\Notification;
|
||||
use Filament\Schemas\Components\Image;
|
||||
use Filament\Schemas\Components\Tabs;
|
||||
use Filament\Schemas\Components\Tabs\Tab;
|
||||
use Filament\Schemas\Components\Utilities\Get;
|
||||
use Filament\Schemas\Components\Utilities\Set;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Livewire\Features\SupportFileUploads\TemporaryUploadedFile;
|
||||
|
||||
class UploadIcon extends Action
|
||||
{
|
||||
/** @var string[] */
|
||||
protected ?array $iconFormats = null;
|
||||
|
||||
public static function getDefaultName(): ?string
|
||||
{
|
||||
return 'upload_icon';
|
||||
}
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->hiddenLabel();
|
||||
|
||||
$this->tooltip(trans('admin/egg.import.import_icon'));
|
||||
|
||||
$this->icon(TablerIcon::PhotoUp);
|
||||
|
||||
$this->modal();
|
||||
|
||||
$this->modalHeading('');
|
||||
|
||||
$this->modalSubmitActionLabel(trans('admin/egg.import.import_icon'));
|
||||
|
||||
$this->schema([
|
||||
Tabs::make()
|
||||
->contained(false)
|
||||
->tabs([
|
||||
Tab::make(trans('admin/egg.import.url'))
|
||||
->schema([
|
||||
TextInput::make('icon_url')
|
||||
->label(trans('admin/egg.import.icon_url'))
|
||||
->reactive()
|
||||
->autocomplete(false)
|
||||
->debounce(500)
|
||||
->afterStateUpdated(function ($state, Set $set) {
|
||||
if (!$state) {
|
||||
$set('icon_url_error', null);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$this->validateIconUrl($state);
|
||||
|
||||
$set('icon_url_error', null);
|
||||
} catch (Exception $exception) {
|
||||
$set('icon_url_error', $exception->getMessage());
|
||||
}
|
||||
}),
|
||||
TextEntry::make('icon_url_error')
|
||||
->hiddenLabel()
|
||||
->visible(fn (Get $get) => $get('icon_url_error') !== null)
|
||||
->afterStateHydrated(fn (Get $get) => $get('icon_url_error')),
|
||||
Image::make(fn (Get $get) => $get('icon_url'), '')
|
||||
->imageSize(150)
|
||||
->visible(fn (Get $get) => $get('icon_url') && !$get('icon_url_error'))
|
||||
->alignCenter(),
|
||||
]),
|
||||
Tab::make(trans('admin/egg.import.file'))
|
||||
->schema([
|
||||
FileUpload::make('icon')
|
||||
->hiddenLabel()
|
||||
->previewable()
|
||||
->openable(false)
|
||||
->downloadable(false)
|
||||
->maxSize(256)
|
||||
->maxFiles(1)
|
||||
->columnSpanFull()
|
||||
->alignCenter()
|
||||
->imageEditor()
|
||||
->image()
|
||||
->acceptedFileTypes(fn () => $this->getIconFormats())
|
||||
->saveUploadedFileUsing(fn (TemporaryUploadedFile $file, $record) => $record->writeIcon($file->getClientOriginalExtension(), $file->getContent())),
|
||||
]),
|
||||
]),
|
||||
]);
|
||||
|
||||
$this->action(function (array $data, $record) {
|
||||
if (!empty($data['icon_url'])) {
|
||||
$this->validateIconUrl($data['icon_url']);
|
||||
|
||||
$content = Http::timeout(5)->connectTimeout(1)->withoutRedirecting()->get($data['icon_url'])->body();
|
||||
|
||||
if (empty($content)) {
|
||||
throw new Exception(trans('admin/egg.import.invalid_url'));
|
||||
}
|
||||
|
||||
$extension = strtolower(pathinfo(parse_url($data['icon_url'], PHP_URL_PATH), PATHINFO_EXTENSION));
|
||||
|
||||
$record->writeIcon($extension, $content);
|
||||
|
||||
Notification::make()
|
||||
->title(trans('admin/egg.import.icon_updated'))
|
||||
->success()
|
||||
->send();
|
||||
} elseif (!empty($data['icon'])) {
|
||||
Notification::make()
|
||||
->title(trans('admin/egg.import.icon_updated'))
|
||||
->success()
|
||||
->send();
|
||||
} else {
|
||||
Notification::make()
|
||||
->title(trans('admin/egg.import.no_icon'))
|
||||
->warning()
|
||||
->send();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected function validateIconUrl(string $url): void
|
||||
{
|
||||
if (!in_array(parse_url($url, PHP_URL_SCHEME), ['http', 'https'], true)) {
|
||||
throw new Exception(trans('admin/egg.import.invalid_url'));
|
||||
}
|
||||
|
||||
if (!filter_var($url, FILTER_VALIDATE_URL)) {
|
||||
throw new Exception(trans('admin/egg.import.invalid_url'));
|
||||
}
|
||||
|
||||
$host = parse_url($url, PHP_URL_HOST);
|
||||
$ip = gethostbyname($host);
|
||||
|
||||
if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) === false) {
|
||||
throw new Exception(trans('admin/egg.import.no_local_ip'));
|
||||
}
|
||||
}
|
||||
|
||||
/** @param string[] $iconFormats */
|
||||
public function iconFormats(?array $iconFormats): static
|
||||
{
|
||||
$this->iconFormats = $iconFormats;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/** @return string[] */
|
||||
public function getIconFormats(): array
|
||||
{
|
||||
return $this->iconFormats ?? array_values(HasIcon::$iconFormats);
|
||||
}
|
||||
}
|
||||
@@ -5,14 +5,13 @@ namespace App\Filament\Server\Pages;
|
||||
use App\Enums\SubuserPermission;
|
||||
use App\Enums\TablerIcon;
|
||||
use App\Facades\Activity;
|
||||
use App\Filament\Components\Actions\DeleteServerIcon;
|
||||
use App\Filament\Components\Actions\DeleteIcon;
|
||||
use App\Filament\Components\Actions\UploadIcon;
|
||||
use App\Models\Server;
|
||||
use App\Services\Servers\ReinstallServerService;
|
||||
use BackedEnum;
|
||||
use Exception;
|
||||
use Filament\Actions\Action;
|
||||
use Filament\Forms\Components\FileUpload;
|
||||
use Filament\Forms\Components\Hidden;
|
||||
use Filament\Forms\Components\Textarea;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Infolists\Components\TextEntry;
|
||||
@@ -21,14 +20,8 @@ use Filament\Schemas\Components\Fieldset;
|
||||
use Filament\Schemas\Components\Grid;
|
||||
use Filament\Schemas\Components\Image;
|
||||
use Filament\Schemas\Components\Section;
|
||||
use Filament\Schemas\Components\Tabs;
|
||||
use Filament\Schemas\Components\Tabs\Tab;
|
||||
use Filament\Schemas\Components\Utilities\Get;
|
||||
use Filament\Schemas\Components\Utilities\Set;
|
||||
use Filament\Schemas\Schema;
|
||||
use Filament\Support\Enums\Alignment;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Livewire\Features\SupportFileUploads\TemporaryUploadedFile;
|
||||
|
||||
class Settings extends ServerFormPage
|
||||
{
|
||||
@@ -79,140 +72,18 @@ class Settings extends ServerFormPage
|
||||
->afterStateUpdated(fn ($state, Server $server) => $this->updateDescription($state ?? '', $server)),
|
||||
]),
|
||||
Grid::make()
|
||||
->columns(2)
|
||||
->columnStart(6)
|
||||
->schema([
|
||||
Image::make('', 'icon')
|
||||
->hidden(fn ($record) => !$record->icon && !$record->egg->image)
|
||||
->url(fn ($record) => $record->icon ?: $record->egg->image)
|
||||
->hidden(fn ($record) => !$record->icon && !$record->egg->icon)
|
||||
->url(fn ($record) => $record->icon ?: $record->egg->icon)
|
||||
->tooltip(fn ($record) => $record->icon ? '' : trans('server/setting.server_info.icon.tooltip'))
|
||||
->columnSpan(2)
|
||||
->imageSize(150)
|
||||
->columnSpanFull()
|
||||
->alignJustify(),
|
||||
Action::make('uploadIcon')
|
||||
->hiddenLabel()
|
||||
->tooltip(trans('admin/server.import_image'))
|
||||
->icon(TablerIcon::PhotoUp)
|
||||
->modal()
|
||||
->modalSubmitActionLabel(trans('server/setting.server_info.icon.upload'))
|
||||
->schema([
|
||||
Tabs::make()
|
||||
->contained(false)
|
||||
->tabs([
|
||||
Tab::make(trans('admin/egg.import.url'))
|
||||
->schema([
|
||||
Hidden::make('imageUrl'),
|
||||
Hidden::make('imageExtension'),
|
||||
TextInput::make('image_url')
|
||||
->label(trans('admin/egg.import.image_url'))
|
||||
->reactive()
|
||||
->autocomplete(false)
|
||||
->debounce(500)
|
||||
->afterStateUpdated(function ($state, Set $set) {
|
||||
if (!$state) {
|
||||
$set('image_url_error', null);
|
||||
$set('imageUrl', null);
|
||||
$set('imageExtension', null);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (!in_array(parse_url($state, PHP_URL_SCHEME), ['http', 'https'], true)) {
|
||||
throw new Exception(trans('admin/egg.import.invalid_url'));
|
||||
}
|
||||
|
||||
if (!filter_var($state, FILTER_VALIDATE_URL)) {
|
||||
throw new Exception(trans('admin/egg.import.invalid_url'));
|
||||
}
|
||||
|
||||
$extension = strtolower(pathinfo(parse_url($state, PHP_URL_PATH), PATHINFO_EXTENSION));
|
||||
|
||||
if (!array_key_exists($extension, Server::IMAGE_FORMATS)) {
|
||||
throw new Exception(trans('admin/egg.import.unsupported_format', ['format' => implode(', ', array_keys(Server::IMAGE_FORMATS))]));
|
||||
}
|
||||
|
||||
$host = parse_url($state, PHP_URL_HOST);
|
||||
$ip = gethostbyname($host);
|
||||
|
||||
if (
|
||||
filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) === false
|
||||
) {
|
||||
throw new Exception(trans('admin/egg.import.no_local_ip'));
|
||||
}
|
||||
|
||||
$set('imageUrl', $state);
|
||||
$set('imageExtension', $extension);
|
||||
$set('image_url_error', null);
|
||||
|
||||
} catch (Exception $e) {
|
||||
$set('image_url_error', $e->getMessage());
|
||||
$set('imageUrl', null);
|
||||
$set('imageExtension', null);
|
||||
}
|
||||
}),
|
||||
TextEntry::make('image_url_error')
|
||||
->hiddenLabel()
|
||||
->visible(fn (Get $get) => $get('image_url_error') !== null)
|
||||
->afterStateHydrated(fn (Get $get) => $get('image_url_error')),
|
||||
Image::make(fn (Get $get) => $get('image_url'), '')
|
||||
->imageSize(150)
|
||||
->visible(fn (Get $get) => $get('image_url') && !$get('image_url_error'))
|
||||
->alignCenter(),
|
||||
]),
|
||||
Tab::make(trans('admin/egg.import.file'))
|
||||
->schema([
|
||||
FileUpload::make('image')
|
||||
->hiddenLabel()
|
||||
->previewable()
|
||||
->openable(false)
|
||||
->downloadable(false)
|
||||
->maxSize(256)
|
||||
->maxFiles(1)
|
||||
->columnSpanFull()
|
||||
->alignCenter()
|
||||
->imageEditor()
|
||||
->image()
|
||||
->disk('public')
|
||||
->directory(Server::ICON_STORAGE_PATH)
|
||||
->acceptedFileTypes([
|
||||
'image/png',
|
||||
'image/jpeg',
|
||||
'image/webp',
|
||||
'image/svg+xml',
|
||||
])
|
||||
->getUploadedFileNameForStorageUsing(function (TemporaryUploadedFile $file, $record) {
|
||||
return $record->uuid . '.' . $file->getClientOriginalExtension();
|
||||
}),
|
||||
]),
|
||||
]),
|
||||
])
|
||||
->action(function (array $data, $record): void {
|
||||
|
||||
if (!empty($data['imageUrl']) && !empty($data['imageExtension'])) {
|
||||
$this->saveIconFromUrl($data['imageUrl'], $data['imageExtension'], $record);
|
||||
Notification::make()
|
||||
->title(trans('server/setting.server_info.icon.updated'))
|
||||
->success()
|
||||
->send();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!empty($data['image'])) {
|
||||
Notification::make()
|
||||
->title(trans('server/setting.server_info.icon.updated'))
|
||||
->success()
|
||||
->send();
|
||||
}
|
||||
|
||||
if (empty($data['imageUrl']) && empty($data['image'])) {
|
||||
Notification::make()
|
||||
->title(trans('admin/egg.import.no_image'))
|
||||
->warning()
|
||||
->send();
|
||||
}
|
||||
}),
|
||||
DeleteServerIcon::make(),
|
||||
UploadIcon::make(),
|
||||
DeleteIcon::make()
|
||||
->iconStoragePath(Server::getIconStoragePath()),
|
||||
]),
|
||||
TextInput::make('uuid')
|
||||
->label(trans('server/setting.server_info.uuid'))
|
||||
@@ -446,39 +317,6 @@ class Settings extends ServerFormPage
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save an icon from URL download to a file.
|
||||
*
|
||||
* @throws Exception
|
||||
*/
|
||||
private function saveIconFromUrl(string $imageUrl, string $extension, Server $server): void
|
||||
{
|
||||
$context = stream_context_create([
|
||||
'http' => ['timeout' => 3],
|
||||
'https' => [
|
||||
'timeout' => 3,
|
||||
'verify_peer' => true,
|
||||
'verify_peer_name' => true,
|
||||
],
|
||||
]);
|
||||
|
||||
$normalizedExtension = match ($extension) {
|
||||
'svg+xml', 'svg' => 'svg',
|
||||
'jpeg', 'jpg' => 'jpg',
|
||||
'png' => 'png',
|
||||
'webp' => 'webp',
|
||||
default => throw new Exception(trans('admin/egg.import.unknown_extension')),
|
||||
};
|
||||
|
||||
$data = @file_get_contents($imageUrl, false, $context, 0, 262144); //256KB
|
||||
|
||||
if (empty($data)) {
|
||||
throw new Exception(trans('admin/egg.import.invalid_url'));
|
||||
}
|
||||
|
||||
Storage::disk('public')->put(Server::ICON_STORAGE_PATH . "/$server->uuid.$normalizedExtension", $data);
|
||||
}
|
||||
|
||||
public function getTitle(): string
|
||||
{
|
||||
return trans('server/setting.title');
|
||||
|
||||
@@ -5,6 +5,7 @@ namespace App\Models;
|
||||
use App\Contracts\Validatable;
|
||||
use App\Exceptions\Service\Egg\HasChildrenException;
|
||||
use App\Exceptions\Service\HasActiveServersException;
|
||||
use App\Models\Traits\HasIcon;
|
||||
use App\Traits\HasValidation;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
@@ -13,7 +14,6 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\Relations\MorphToMany;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
/**
|
||||
@@ -47,7 +47,7 @@ use Illuminate\Support\Str;
|
||||
* @property-read string $copy_script_container
|
||||
* @property-read string $copy_script_entry
|
||||
* @property-read string|null $copy_script_install
|
||||
* @property-read string|null $image
|
||||
* @property-read string|null $icon
|
||||
* @property-read string|null $inherit_config_files
|
||||
* @property-read string|null $inherit_config_logs
|
||||
* @property-read string|null $inherit_config_startup
|
||||
@@ -94,6 +94,7 @@ use Illuminate\Support\Str;
|
||||
class Egg extends Model implements Validatable
|
||||
{
|
||||
use HasFactory;
|
||||
use HasIcon;
|
||||
use HasValidation;
|
||||
|
||||
/**
|
||||
@@ -107,22 +108,6 @@ class Egg extends Model implements Validatable
|
||||
*/
|
||||
public const EXPORT_VERSION = 'PLCN_v3';
|
||||
|
||||
/**
|
||||
* Path to store egg icons relative to storage path.
|
||||
*/
|
||||
public const ICON_STORAGE_PATH = 'icons/egg';
|
||||
|
||||
/**
|
||||
* Supported image formats: file extension => MIME type
|
||||
*/
|
||||
public const IMAGE_FORMATS = [
|
||||
'png' => 'image/png',
|
||||
'jpg' => 'image/jpeg',
|
||||
'jpeg' => 'image/jpeg',
|
||||
'webp' => 'image/webp',
|
||||
'svg' => 'image/svg+xml',
|
||||
];
|
||||
|
||||
/**
|
||||
* Fields that are not mass assignable.
|
||||
*/
|
||||
@@ -377,16 +362,4 @@ class Egg extends Model implements Validatable
|
||||
{
|
||||
return str($this->name)->kebab()->lower()->trim()->split('/[^\w\-]/')->join('');
|
||||
}
|
||||
|
||||
public function getImageAttribute(): ?string
|
||||
{
|
||||
foreach (array_keys(static::IMAGE_FORMATS) as $ext) {
|
||||
$path = static::ICON_STORAGE_PATH . "/$this->uuid.$ext";
|
||||
if (Storage::disk('public')->exists($path)) {
|
||||
return Storage::disk('public')->url($path);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,10 +7,12 @@ use App\Enums\ContainerStatus;
|
||||
use App\Enums\ServerResourceType;
|
||||
use App\Enums\ServerState;
|
||||
use App\Exceptions\Http\Server\ServerStateConflictException;
|
||||
use App\Models\Traits\HasIcon;
|
||||
use App\Repositories\Daemon\DaemonServerRepository;
|
||||
use App\Services\Subusers\SubuserDeletionService;
|
||||
use App\Traits\HasValidation;
|
||||
use Carbon\CarbonInterface;
|
||||
use Exception;
|
||||
use Filament\Models\Contracts\HasAvatar;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
@@ -29,7 +31,6 @@ use Illuminate\Notifications\Notifiable;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
|
||||
/**
|
||||
@@ -129,6 +130,7 @@ use Psr\Http\Message\ResponseInterface;
|
||||
class Server extends Model implements HasAvatar, Validatable
|
||||
{
|
||||
use HasFactory;
|
||||
use HasIcon;
|
||||
use HasValidation;
|
||||
use Notifiable;
|
||||
|
||||
@@ -138,22 +140,6 @@ class Server extends Model implements HasAvatar, Validatable
|
||||
*/
|
||||
public const RESOURCE_NAME = 'server';
|
||||
|
||||
/**
|
||||
* Path to store server icons relative to storage path.
|
||||
*/
|
||||
public const ICON_STORAGE_PATH = 'icons/server';
|
||||
|
||||
/**
|
||||
* Supported image formats: file extension => MIME type
|
||||
*/
|
||||
public const IMAGE_FORMATS = [
|
||||
'png' => 'image/png',
|
||||
'jpg' => 'image/jpeg',
|
||||
'jpeg' => 'image/jpeg',
|
||||
'webp' => 'image/webp',
|
||||
'svg' => 'image/svg+xml',
|
||||
];
|
||||
|
||||
/**
|
||||
* Default values when creating the model. We want to switch to disabling OOM killer
|
||||
* on server instances unless the user specifies otherwise in the request.
|
||||
@@ -527,20 +513,8 @@ class Server extends Model implements HasAvatar, Validatable
|
||||
);
|
||||
}
|
||||
|
||||
public function getIconAttribute(): ?string
|
||||
{
|
||||
foreach (array_keys(static::IMAGE_FORMATS) as $ext) {
|
||||
$path = static::ICON_STORAGE_PATH . "/$this->uuid.$ext";
|
||||
if (Storage::disk('public')->exists($path)) {
|
||||
return Storage::disk('public')->url($path);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function getFilamentAvatarUrl(): ?string
|
||||
{
|
||||
return $this->icon ?? $this->egg->image;
|
||||
return $this->icon ?? $this->egg->icon;
|
||||
}
|
||||
}
|
||||
|
||||
74
app/Models/Traits/HasIcon.php
Normal file
74
app/Models/Traits/HasIcon.php
Normal file
@@ -0,0 +1,74 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Traits;
|
||||
|
||||
use App\Models\Model;
|
||||
use Exception;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
/**
|
||||
* @mixin Model
|
||||
*/
|
||||
trait HasIcon
|
||||
{
|
||||
/**
|
||||
* Supported icon formats: file extension => MIME type
|
||||
*
|
||||
* @var array<string, string>
|
||||
*/
|
||||
public static array $iconFormats = [
|
||||
'png' => 'image/png',
|
||||
'jpg' => 'image/jpeg',
|
||||
'webp' => 'image/webp',
|
||||
];
|
||||
|
||||
public static function getIconStoragePath(): string
|
||||
{
|
||||
return 'icons/' . static::RESOURCE_NAME;
|
||||
}
|
||||
|
||||
public function getIconAttribute(): ?string
|
||||
{
|
||||
foreach (array_keys(static::$iconFormats) as $ext) {
|
||||
$path = $this->getIconStoragePath() . "/$this->uuid.$ext";
|
||||
if (Storage::disk('public')->exists($path)) {
|
||||
return Storage::disk('public')->url($path);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function writeIcon(string $extension, string $data): string
|
||||
{
|
||||
$normalizedExtension = match (strtolower($extension)) {
|
||||
'jpeg', 'jpg' => 'jpg',
|
||||
'png' => 'png',
|
||||
'webp' => 'webp',
|
||||
default => null,
|
||||
};
|
||||
|
||||
if (is_null($normalizedExtension)) {
|
||||
throw new Exception(trans('admin/egg.import.unknown_extension'));
|
||||
}
|
||||
|
||||
$fileName = static::getIconStoragePath() . "/$this->uuid.$normalizedExtension";
|
||||
|
||||
if (!Storage::disk('public')->put($fileName, $data)) {
|
||||
throw new Exception(trans('admin/egg.import.could_not_write'));
|
||||
}
|
||||
|
||||
foreach (['png', 'jpg', 'jpeg', 'webp', 'svg'] as $ext) {
|
||||
if ($ext === $normalizedExtension) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$path = static::getIconStoragePath() . "/$this->uuid.$ext";
|
||||
if (Storage::disk('public')->exists($path)) {
|
||||
Storage::disk('public')->delete($path);
|
||||
}
|
||||
}
|
||||
|
||||
return $fileName;
|
||||
}
|
||||
}
|
||||
@@ -18,7 +18,7 @@ class EggExporterService
|
||||
public function handle(int $egg, EggFormat $format): string
|
||||
{
|
||||
$egg = Egg::with(['scriptFrom', 'configFrom', 'variables'])->findOrFail($egg);
|
||||
$imageBase64 = $this->getEggImageAsBase64($egg);
|
||||
$iconBase64 = $this->getEggIconAsBase64($egg);
|
||||
|
||||
$struct = [
|
||||
'_comment' => 'DO NOT EDIT: FILE GENERATED AUTOMATICALLY BY PANEL',
|
||||
@@ -31,7 +31,7 @@ class EggExporterService
|
||||
'author' => $egg->author,
|
||||
'uuid' => $egg->uuid,
|
||||
'description' => $egg->description,
|
||||
'image' => $imageBase64,
|
||||
'icon' => $iconBase64,
|
||||
'tags' => $egg->tags,
|
||||
'features' => $egg->features,
|
||||
'docker_images' => $egg->docker_images,
|
||||
@@ -63,16 +63,14 @@ class EggExporterService
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the egg image as base64 for export.
|
||||
* Get the egg icon as base64 for export.
|
||||
*/
|
||||
private function getEggImageAsBase64(Egg $egg): ?string
|
||||
private function getEggIconAsBase64(Egg $egg): ?string
|
||||
{
|
||||
foreach (array_keys(Egg::IMAGE_FORMATS) as $ext) {
|
||||
$path = Egg::ICON_STORAGE_PATH . "/$egg->uuid.$ext";
|
||||
foreach (Egg::$iconFormats as $ext => $mimeType) {
|
||||
$path = Egg::getIconStoragePath() . "/$egg->uuid.$ext";
|
||||
|
||||
if (Storage::disk('public')->exists($path)) {
|
||||
$mimeType = Egg::IMAGE_FORMATS[$ext];
|
||||
|
||||
return 'data:' . $mimeType . ';base64,' . base64_encode(Storage::disk('public')->get($path));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,6 @@ use Illuminate\Http\UploadedFile;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use JsonException;
|
||||
use Ramsey\Uuid\Uuid;
|
||||
use stdClass;
|
||||
@@ -223,6 +222,11 @@ class EggImporterService
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($parsed['image']) && str_starts_with($parsed['image'], 'data:')) {
|
||||
$parsed['icon'] = $parsed['image'];
|
||||
unset($parsed['image']);
|
||||
}
|
||||
|
||||
return $parsed;
|
||||
}
|
||||
|
||||
@@ -231,9 +235,9 @@ class EggImporterService
|
||||
*/
|
||||
protected function fillFromParsed(Egg $model, array $parsed): Egg
|
||||
{
|
||||
// Handle image data if present
|
||||
if (!empty($parsed['image']) && str_starts_with($parsed['image'], 'data:')) {
|
||||
$this->saveEggImageFromBase64($parsed['image'], $model);
|
||||
// Handle icon data if present
|
||||
if (!empty($parsed['icon']) && str_starts_with($parsed['icon'], 'data:')) {
|
||||
$this->saveEggIconFromBase64($parsed['icon'], $model);
|
||||
}
|
||||
|
||||
return $model->forceFill([
|
||||
@@ -256,9 +260,9 @@ class EggImporterService
|
||||
}
|
||||
|
||||
/**
|
||||
* Save an egg image from base64 data to a file.
|
||||
* Save an egg icon from base64 data to a file.
|
||||
*/
|
||||
private function saveEggImageFromBase64(string $base64String, Egg $egg): void
|
||||
private function saveEggIconFromBase64(string $base64String, Egg $egg): void
|
||||
{
|
||||
if (!preg_match('/^data:image\/([\w+]+);base64,(.+)$/', $base64String, $matches)) {
|
||||
return;
|
||||
@@ -267,23 +271,9 @@ class EggImporterService
|
||||
$extension = strtolower($matches[1]);
|
||||
$data = base64_decode($matches[2]);
|
||||
|
||||
if (!$data) {
|
||||
return;
|
||||
if ($data) {
|
||||
$egg->writeIcon($extension, $data);
|
||||
}
|
||||
|
||||
$normalizedExtension = match ($extension) {
|
||||
'svg+xml', 'svg' => 'svg',
|
||||
'jpeg', 'jpg' => 'jpg',
|
||||
'png' => 'png',
|
||||
'webp' => 'webp',
|
||||
default => null,
|
||||
};
|
||||
|
||||
if (is_null($normalizedExtension)) {
|
||||
return;
|
||||
}
|
||||
|
||||
Storage::disk('public')->put(Egg::ICON_STORAGE_PATH . "/$egg->uuid.$normalizedExtension", $data);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -46,10 +46,10 @@ class EggTransformer extends BaseTransformer
|
||||
'name' => $model->name,
|
||||
'author' => $model->author,
|
||||
'description' => $model->description,
|
||||
'image' => $model->image,
|
||||
'icon' => $model->icon,
|
||||
'features' => $model->features,
|
||||
'tags' => $model->tags,
|
||||
'docker_image' => Arr::first($model->docker_images, default: ''), // docker_images, use startup_commands
|
||||
'docker_image' => Arr::first($model->docker_images, default: ''), // deprecated, use docker_images
|
||||
'docker_images' => $model->docker_images,
|
||||
'config' => [
|
||||
'files' => $files,
|
||||
|
||||
@@ -18,14 +18,14 @@ return new class extends Migration
|
||||
$eggs = DB::table('eggs')->whereNotNull('image')->get();
|
||||
foreach ($eggs as $egg) {
|
||||
if (!empty($egg->image) && str_starts_with($egg->image, 'data:')) {
|
||||
$this->convertBase64ToFile($egg->image, $egg->uuid, Egg::ICON_STORAGE_PATH);
|
||||
$this->convertBase64ToFile($egg->image, $egg->uuid, Egg::getIconStoragePath());
|
||||
}
|
||||
}
|
||||
|
||||
$servers = DB::table('servers')->whereNotNull('icon')->get();
|
||||
foreach ($servers as $server) {
|
||||
if (!empty($server->icon) && str_starts_with($server->icon, 'data:')) {
|
||||
$this->convertBase64ToFile($server->icon, $server->uuid, Server::ICON_STORAGE_PATH);
|
||||
$this->convertBase64ToFile($server->icon, $server->uuid, Server::getIconStoragePath());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -13,9 +13,8 @@ return [
|
||||
'import' => [
|
||||
'file' => 'File',
|
||||
'url' => 'URL',
|
||||
'image_url' => 'Image URL',
|
||||
'image_error' => 'Could not fetch image',
|
||||
'image_too_large' => 'Image too large. Limit is 1024KB',
|
||||
'icon_url' => 'Icon URL',
|
||||
'icon_error' => 'Could not fetch icon',
|
||||
'egg_help' => 'This should be the raw .json/.yaml file',
|
||||
'url_help' => 'URLs must point directly to the raw .json/.yaml file',
|
||||
'add_url' => 'New URL',
|
||||
@@ -26,15 +25,16 @@ return [
|
||||
'failed_import_eggs' => 'Failed: :eggs',
|
||||
'github' => 'GitHub',
|
||||
'refresh' => 'Refresh',
|
||||
'import_image' => 'Import Image',
|
||||
'delete_image' => 'Delete Image',
|
||||
'import_icon' => 'Import Icon',
|
||||
'delete_icon' => 'Delete Icon',
|
||||
'no_local_ip' => 'Local IP Addresses are not allowed',
|
||||
'unsupported_format' => 'Unsupported Format. Supported Formats: :formats',
|
||||
'invalid_url' => 'The provided URL is invalid',
|
||||
'unknown_extension' => 'Unknown image extension',
|
||||
'image_deleted' => 'Image Deleted',
|
||||
'no_image' => 'No Image Provided',
|
||||
'image_updated' => 'Image Updated',
|
||||
'unknown_extension' => 'Unknown icon extension',
|
||||
'could_not_write' => 'Could not write icon to disk',
|
||||
'icon_deleted' => 'Icon Deleted',
|
||||
'no_icon' => 'No Icon Provided',
|
||||
'icon_updated' => 'Icon Updated',
|
||||
],
|
||||
'export' => [
|
||||
'modal' => 'How would you like to export :egg ?',
|
||||
|
||||
@@ -7,8 +7,8 @@ return [
|
||||
'no_servers' => 'No Servers',
|
||||
'create' => 'Create Server',
|
||||
'ip_address' => 'IP Address',
|
||||
'import_image' => 'Import Image',
|
||||
'delete_image' => 'Delete Image',
|
||||
'import_icon' => 'Import Icon',
|
||||
'delete_icon' => 'Delete Icon',
|
||||
'ip_address_helper' => 'Usually your machine\'s public IP unless you are port forwarding.',
|
||||
'port' => 'Port',
|
||||
'ports' => 'Ports',
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
@php
|
||||
$backgroundImage = $server->icon ?? $server->egg->image;
|
||||
$backgroundImage = $server->icon ?? $server->egg->icon;
|
||||
$serverEntryColumn = $column ?? \App\Filament\Components\Tables\Columns\ServerEntryColumn::make('server_entry');
|
||||
@endphp
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
@php
|
||||
$actiongroup = \App\Filament\App\Resources\Servers\Pages\ListServers::getPowerActionGroup()->record($server);
|
||||
$backgroundImage = $server->icon ?? $server->egg->image;
|
||||
$backgroundImage = $server->icon ?? $server->egg->icon;
|
||||
|
||||
$serverEntryColumn = $column ?? \App\Filament\Components\Tables\Columns\ServerEntryColumn::make('server_entry');
|
||||
$serverNodeStatistics = $server->node->statistics();
|
||||
|
||||
Reference in New Issue
Block a user