fixed migrations, changed settings to global settings, changed image list to have a "delete all" button instead of "create", fixed printing, added imagick for printing.

This commit is contained in:
2025-08-26 10:39:18 +02:00
parent 44dd0f2867
commit 9b1f6a479f
69 changed files with 17232 additions and 1263 deletions

View File

@@ -124,6 +124,21 @@ class ComfyUi implements ApiPluginInterface
$modelParams = $style->aiModel->parameters ?? [];
$styleParams = $style->parameters ?? [];
// Ensure both parameters are arrays, decode JSON if needed
if (is_string($modelParams)) {
$modelParams = json_decode($modelParams, true);
if (json_last_error() !== JSON_ERROR_NONE) {
$modelParams = [];
}
}
if (is_string($styleParams)) {
$styleParams = json_decode($styleParams, true);
if (json_last_error() !== JSON_ERROR_NONE) {
$styleParams = [];
}
}
if (empty($modelParams) && empty($styleParams)) {
throw new \Exception('ComfyUI workflow (parameters) is missing.');
}

View File

@@ -0,0 +1,37 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
class GetPrinterSetting extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'setting:get-printer';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Get the raw value of the \'selected_printer\' setting.';
/**
* Execute the console command.
*/
public function handle()
{
$setting = \App\Models\Setting::where('key', 'selected_printer')->first();
if ($setting) {
$this->info('Raw value of \'selected_printer\' setting:');
$this->info(json_encode($setting->value, JSON_PRETTY_PRINT)); // Assuming \'value\' column holds the data
} else {
$this->info('\'selected_printer\' setting not found.');
}
}
}

View File

@@ -0,0 +1,96 @@
<?php
namespace App\Filament\Pages;
use App\Models\Setting;
use App\Services\PrinterService;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Toggle;
use Filament\Forms\Concerns\InteractsWithForms;
use Filament\Forms\Contracts\HasForms;
use Filament\Forms\Form;
use Filament\Notifications\Notification;
use Filament\Pages\Page;
use Illuminate\Support\Arr;
class GlobalSettings extends Page implements HasForms
{
use InteractsWithForms;
protected static ?string $navigationIcon = 'heroicon-o-cog';
protected static string $view = 'filament.pages.global-settings';
protected static ?string $navigationGroup = 'Admin';
public ?array $data = [];
public function mount(): void
{
$settings = Setting::all()->pluck('value', 'key')->toArray();
$this->form->fill($settings);
}
public function form(Form $form): Form
{
$printerService = new PrinterService();
$printers = $printerService->getPrinters();
$printerOptions = array_merge($printers, ['__custom__' => __('filament.resource.setting.form.custom_printer')]);
return $form
->schema([
TextInput::make('gallery_heading')
->label(__('filament.resource.setting.form.gallery_heading'))
->required(),
TextInput::make('new_image_timespan_minutes')
->label(__('filament.resource.setting.form.new_image_timespan_minutes'))
->numeric()
->required(),
TextInput::make('image_refresh_interval')
->label(__('filament.resource.setting.form.image_refresh_interval'))
->numeric()
->required(),
TextInput::make('max_number_of_copies')
->label(__('filament.resource.setting.form.max_number_of_copies'))
->numeric()
->required(),
Toggle::make('show_print_button')
->label(__('filament.resource.setting.form.show_print_button')),
Select::make('selected_printer')
->label(__('filament.resource.setting.form.printer'))
->options($printerOptions)
->reactive(),
TextInput::make('custom_printer_address')
->label(__('filament.resource.setting.form.custom_printer_address'))
->visible(fn ($get) => $get('selected_printer') === '__custom__'),
])
->statePath('data');
}
public function save(): void
{
$data = $this->form->getState();
// if a non-custom printer is selected, clear the custom address
if (Arr::get($data, 'selected_printer') !== '__custom__') {
$data['custom_printer_address'] = '';
}
foreach ($data as $key => $value) {
Setting::where('key', $key)->update(['value' => $value]);
}
Notification::make()
->title(__('settings.saved_successfully'))
->success()
->send();
}
protected function getFormActions(): array
{
return [
\Filament\Actions\Action::make('save')
->label(__('settings.save_button'))
->submit('save'),
];
}
}

View File

@@ -3,21 +3,13 @@
namespace App\Filament\Pages;
use Filament\Pages\Page;
use Filament\Tables;
use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Builder;
use Filament\Forms\Contracts\HasForms;
use Filament\Forms\Concerns\InteractsWithForms;
use App\Models\Plugin;
class Plugins extends Page implements Tables\Contracts\HasTable, HasForms
class Plugins extends Page
{
use Tables\Concerns\InteractsWithTable;
use InteractsWithForms;
protected static ?string $navigationIcon = 'heroicon-o-puzzle-piece';
protected static ?string $navigationGroup = 'Plugins';
protected static ?string $navigationGroup = 'Plugins';
protected static ?string $navigationLabel = 'Plugin List';
@@ -27,31 +19,7 @@ class Plugins extends Page implements Tables\Contracts\HasTable, HasForms
protected static ?string $slug = 'list-plugins';
public function table(Table $table): Table
{
return $table
->query(Plugin::query()) // Use a dummy query
->columns([
Tables\Columns\TextColumn::make('name')
->label('Name')
->searchable()
->sortable(),
Tables\Columns\TextColumn::make('identifier')
->label('Identifier'),
Tables\Columns\IconColumn::make('enabled')
->label('Enabled')
->boolean(),
Tables\Columns\IconColumn::make('configured')
->label('Configured')
->boolean()
->tooltip(fn ($record) => $record->configured ? 'Has ApiProvider record' : 'No ApiProvider record'),
Tables\Columns\TextColumn::make('file_path')
->label('File Path')
->toggleable(isToggledHiddenByDefault: true),
]);
}
public $plugins;
public $plugins;
public function mount(): void
{
@@ -67,9 +35,4 @@ class Plugins extends Page implements Tables\Contracts\HasTable, HasForms
{
return __('filament.navigation.plugin_list');
}
public function currentlyValidatingForm(\Filament\Forms\ComponentContainer|null $form): void
{
// No form validation needed for this page
}
}

View File

@@ -32,11 +32,11 @@ class AiModelResource extends Resource
->schema([
Forms\Components\Section::make()
->schema([
Select::make('api_provider_id')
->label(__('filament.resource.ai_model.form.api_provider'))
Select::make('apiProviders')
->label(__('filament.resource.ai_model.form.api_providers'))
->relationship('apiProviders', 'name')
->multiple()
->live()
->nullable()
->afterStateUpdated(function (callable $set) {
$set('model_search_result', null);
}),
@@ -44,20 +44,24 @@ class AiModelResource extends Resource
->label(__('filament.resource.ai_model.form.search_model'))
->searchable()
->live()
->hidden(fn (callable $get) => !static::canSearchModels($get('api_provider_id')))
->hidden(fn (callable $get) => !static::canSearchModelsWithAnyProvider($get('apiProviders')))
->getSearchResultsUsing(function (string $search, callable $get) {
$apiProviderId = $get('api_provider_id');
if (!$apiProviderId) {
$apiProviderIds = $get('apiProviders');
if (empty($apiProviderIds)) {
return [];
}
$pluginInstance = static::getPluginInstance($apiProviderId);
if ($pluginInstance && method_exists($pluginInstance, 'searchModels')) {
$models = $pluginInstance->searchModels($search);
$options = [];
foreach ($models as $model) {
$options[json_encode(['name' => $model['name'], 'id' => $model['id'], 'type' => $model['type'] ?? null])] = $model['name'] . ' (' . $model['id'] . ')';
// Try each API provider until we find one that works
foreach ($apiProviderIds as $apiProviderId) {
$pluginInstance = static::getPluginInstance($apiProviderId);
if ($pluginInstance && method_exists($pluginInstance, 'searchModels')) {
$models = $pluginInstance->searchModels($search);
$options = [];
foreach ($models as $model) {
$options[json_encode(['name' => $model['name'], 'id' => $model['id'], 'type' => $model['type'] ?? null, 'api_provider_id' => $apiProviderId])] = $model['name'] . ' (' . $model['id'] . ')';
}
return $options;
}
return $options;
}
return [];
})
@@ -103,22 +107,28 @@ class AiModelResource extends Resource
]);
}
protected static function canSearchModels(?int $apiProviderId): bool
protected static function canSearchModelsWithAnyProvider(?array $apiProviderIds): bool
{
if (!$apiProviderId) {
if (empty($apiProviderIds)) {
return false;
}
$apiProvider = ApiProvider::find($apiProviderId);
if (!$apiProvider || !$apiProvider->plugin) {
return false;
}
try {
$pluginInstance = PluginLoader::getPlugin($apiProvider->plugin, $apiProvider);
return method_exists($pluginInstance, 'searchModels');
} catch (\Exception $e) {
// Log the exception if needed
return false;
foreach ($apiProviderIds as $apiProviderId) {
$apiProvider = ApiProvider::find($apiProviderId);
if (!$apiProvider || !$apiProvider->plugin) {
continue;
}
try {
$pluginInstance = PluginLoader::getPlugin($apiProvider->plugin, $apiProvider);
if (method_exists($pluginInstance, 'searchModels')) {
return true;
}
} catch (\Exception $e) {
// Log the exception if needed
continue;
}
}
return false;
}
protected static function getPluginInstance(?int $apiProviderId): ?ApiPluginInterface
@@ -138,6 +148,24 @@ class AiModelResource extends Resource
}
}
protected static function canSearchModels(?int $apiProviderId): bool
{
if (!$apiProviderId) {
return false;
}
$apiProvider = ApiProvider::find($apiProviderId);
if (!$apiProvider || !$apiProvider->plugin) {
return false;
}
try {
$pluginInstance = PluginLoader::getPlugin($apiProvider->plugin, $apiProvider);
return method_exists($pluginInstance, 'searchModels');
} catch (\Exception $e) {
// Log the exception if needed
return false;
}
}
public static function table(Table $table): Table
{
return $table
@@ -148,7 +176,7 @@ class AiModelResource extends Resource
Tables\Columns\IconColumn::make('enabled')
->label(__('filament.resource.ai_model.table.enabled'))
->boolean(),
TextColumn::make('apiProviders.name')->label(__('filament.resource.ai_model.table.api_providers'))->searchable()->sortable(),
TextColumn::make('apiProviders.name')->label(__('filament.resource.ai_model.table.api_providers'))->searchable()->sortable()->limit(50),
])
->filters([
//

View File

@@ -54,9 +54,6 @@ class ImageResource extends Resource
Tables\Actions\BulkActionGroup::make([
Tables\Actions\DeleteBulkAction::make(),
]),
])
->emptyStateActions([
Tables\Actions\CreateAction::make(),
]);
}
@@ -71,8 +68,7 @@ class ImageResource extends Resource
{
return [
'index' => Pages\ListImages::route('/'),
'create' => Pages\CreateImage::route('/create'),
'edit' => Pages\EditImage::route('/{record}/edit'),
];
}
}
}

View File

@@ -1,17 +0,0 @@
<?php
namespace App\Filament\Resources\ImageResource\Pages;
use App\Filament\Resources\ImageResource;
use Filament\Actions;
use Filament\Resources\Pages\CreateRecord;
class CreateImage extends CreateRecord
{
protected static string $resource = ImageResource::class;
protected function getRedirectUrl(): string
{
return $this->getResource()::getUrl('index');
}
}

View File

@@ -7,6 +7,8 @@ use Filament\Actions;
use Filament\Resources\Pages\ListRecords;
use Illuminate\Contracts\Pagination\Paginator;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Facades\Storage;
use Filament\Notifications\Notification;
class ListImages extends ListRecords
{
@@ -15,7 +17,25 @@ class ListImages extends ListRecords
protected function getHeaderActions(): array
{
return [
Actions\CreateAction::make(),
Actions\Action::make('delete_all')
->label('Delete All Images')
->icon('heroicon-o-trash')
->color('danger')
->requiresConfirmation()
->action(function () {
// Delete all images from storage
Storage::disk('public')->deleteDirectory('images');
Storage::disk('public')->makeDirectory('images');
// Show success notification
Notification::make()
->title('All images deleted successfully')
->success()
->send();
// Refresh the page
$this->redirect(static::getUrl());
}),
];
}

View File

@@ -1,105 +0,0 @@
<?php
namespace App\Filament\Resources;
use App\Filament\Resources\SettingResource\Pages;
use App\Filament\Resources\SettingResource\RelationManagers;
use App\Models\Setting;
use Filament\Forms;
use Filament\Forms\Form;
use Filament\Resources\Resource;
use Filament\Tables;
use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\SoftDeletingScope;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Fieldset;
class SettingResource extends Resource
{
protected static ?string $model = Setting::class;
protected static ?string $navigationIcon = 'heroicon-o-cog';
protected static ?string $navigationGroup = 'Einstellungen';
public static function form(Form $form): Form
{
return $form
->schema([
Forms\Components\TextInput::make('key')
->label(__('Key'))
->required()
->maxLength(255)
->hiddenOn('edit'),
Forms\Components\Fieldset::make()
->label(fn (?Setting $record) => $record ? __('filament.resource.setting.form.' . $record->key) : __('New Setting'))
->schema([
TextInput::make('value')
->label(__('Bitte Wert eingeben'))
->disableLabel(fn (?Setting $record) => $record && ($record->key !== 'image_refresh_interval' && $record->key !== 'new_image_timespan_minutes' && $record->key !== 'gallery_heading'))
->formatStateUsing(function (?string $state, ?Setting $record) {
if ($record && $record->key === 'image_refresh_interval') {
return (int)$state / 1000;
} else if ($record && $record->key === 'new_image_timespan_minutes') {
return (int)$state;
}
return $state;
})
->dehydrateStateUsing(function (?string $state, ?Setting $record) {
if ($record && $record->key === 'image_refresh_interval') {
return (int)$state * 1000;
} else if ($record && $record->key === 'new_image_timespan_minutes') {
return (int)$state;
}
return $state;
})
])
]);
}
public static function table(Table $table): Table
{
return $table
->columns([
Tables\Columns\TextColumn::make('key')->label(__('Key'))->searchable()->sortable(),
Tables\Columns\TextColumn::make('value')->label(__('Value'))->searchable()->sortable(),
])
->filters([
//
])
->actions([
Tables\Actions\EditAction::make(),
])
->bulkActions([
Tables\Actions\BulkActionGroup::make([
Tables\Actions\DeleteBulkAction::make(),
]),
])
->emptyStateActions([
Tables\Actions\CreateAction::make(),
]);
}
public static function getRelations(): array
{
return [
//
];
}
public static function getPages(): array
{
return [
'index' => Pages\ListSettings::route('/'),
'create' => Pages\CreateSetting::route('/create'),
'edit' => Pages\EditSetting::route('/{record}/edit'),
];
}
public static function getNavigationGroup(): ?string
{
return __('filament.navigation.groups.settings');
}
}

View File

@@ -1,12 +0,0 @@
<?php
namespace App\Filament\Resources\SettingResource\Pages;
use App\Filament\Resources\SettingResource;
use Filament\Actions;
use Filament\Resources\Pages\CreateRecord;
class CreateSetting extends CreateRecord
{
protected static string $resource = SettingResource::class;
}

View File

@@ -1,19 +0,0 @@
<?php
namespace App\Filament\Resources\SettingResource\Pages;
use App\Filament\Resources\SettingResource;
use Filament\Actions;
use Filament\Resources\Pages\EditRecord;
class EditSetting extends EditRecord
{
protected static string $resource = SettingResource::class;
protected function getHeaderActions(): array
{
return [
Actions\DeleteAction::make(),
];
}
}

View File

@@ -1,19 +0,0 @@
<?php
namespace App\Filament\Resources\SettingResource\Pages;
use App\Filament\Resources\SettingResource;
use Filament\Actions;
use Filament\Resources\Pages\ListRecords;
class ListSettings extends ListRecords
{
protected static string $resource = SettingResource::class;
protected function getHeaderActions(): array
{
return [
Actions\CreateAction::make(),
];
}
}

View File

@@ -109,11 +109,22 @@ class ImageController extends Controller
public function styleChangeRequest(Request $request)
{
// Log the incoming request for debugging
\Illuminate\Support\Facades\Log::info('styleChangeRequest called', [
'image_id' => $request->image_id,
'style_id' => $request->style_id,
'all_params' => $request->all()
]);
// Same-origin check
$appUrl = config('app.url');
$referer = $request->headers->get('referer');
if ($referer && parse_url($referer, PHP_URL_HOST) !== parse_url($appUrl, PHP_URL_HOST)) {
\Illuminate\Support\Facades\Log::warning('Unauthorized styleChangeRequest', [
'referer' => $referer,
'app_url' => $appUrl
]);
return response()->json(['error' => 'Unauthorized: Request must originate from the same domain.'], 403);
}
@@ -127,7 +138,7 @@ class ImageController extends Controller
if ($request->style_id) {
$style = Style::with(['aiModel' => function ($query) {
$query->where('enabled', true)->with(['apiProviders' => function ($query) {
$query->where('enabled', true)->with(['primaryApiProvider', 'apiProviders' => function ($query) {
$query->where('enabled', true);
}]);
}])->find($request->style_id);
@@ -136,7 +147,7 @@ class ImageController extends Controller
$defaultStyleSetting = \App\Models\Setting::where('key', 'default_style_id')->first();
if ($defaultStyleSetting && $defaultStyleSetting->value) {
$style = Style::with(['aiModel' => function ($query) {
$query->where('enabled', true)->with(['apiProviders' => function ($query) {
$query->where('enabled', true)->with(['primaryApiProvider', 'apiProviders' => function ($query) {
$query->where('enabled', true);
}]);
}])->find($defaultStyleSetting->value);
@@ -144,14 +155,38 @@ class ImageController extends Controller
}
if (!$style || !$style->aiModel || $style->aiModel->apiProviders->isEmpty()) {
\Illuminate\Support\Facades\Log::warning('Style or provider not found', [
'style' => $style ? $style->toArray() : null,
'ai_model' => $style && $style->aiModel ? $style->aiModel->toArray() : null
]);
return response()->json(['error' => __('api.style_or_provider_not_found')], 404);
}
try {
$apiProvider = $style->aiModel->apiProviders->first(); // Get the first enabled API provider
// Use the primary API provider for this AI model if available
$apiProvider = $style->aiModel->primaryApiProvider;
if (!$apiProvider) {
// Fallback to the first enabled API provider from the many-to-many relationship
$apiProvider = $style->aiModel->apiProviders->where('enabled', true)->first();
}
if (!$apiProvider) {
// If no enabled provider found, try any provider
$apiProvider = $style->aiModel->apiProviders->first();
}
if (!$apiProvider) {
\Illuminate\Support\Facades\Log::error('No API provider found for style', [
'style_id' => $style->id,
'ai_model_id' => $style->aiModel->id
]);
return response()->json(['error' => __('api.style_or_provider_not_found')], 404);
}
\Illuminate\Support\Facades\Log::info('Selected API provider for style change', [
'api_provider_id' => $apiProvider->id,
'api_provider_name' => $apiProvider->name,
'plugin' => $apiProvider->plugin
]);
$plugin = PluginLoader::getPlugin($apiProvider->plugin, $apiProvider);
$result = $plugin->processImageStyleChange($image, $style);
@@ -162,12 +197,22 @@ class ImageController extends Controller
$image->save();
// Return the prompt_id for WebSocket tracking
\Illuminate\Support\Facades\Log::info('Style change request completed', [
'prompt_id' => $result['prompt_id'],
'image_uuid' => $image->uuid
]);
return response()->json([
'message' => 'Style change request sent.',
'prompt_id' => $result['prompt_id'],
'image_uuid' => $image->uuid, // Pass image UUID for frontend tracking
'plugin' => $apiProvider->plugin, // Pass plugin name for frontend handling
]);
} catch (\Exception $e) {
\Illuminate\Support\Facades\Log::error('Error in styleChangeRequest', [
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString()
]);
return response()->json(['error' => $e->getMessage()], 500);
}
}
@@ -230,8 +275,10 @@ class ImageController extends Controller
Log::info('fetchStyledImage called.', ['prompt_id' => $promptId]);
try {
// Find the image associated with the prompt_id, eagerly loading relationships
$image = Image::with(['style.aiModel.apiProviders' => function ($query) {
$query->where('enabled', true);
$image = Image::with(['style.aiModel' => function ($query) {
$query->with(['primaryApiProvider', 'apiProviders' => function ($query) {
$query->where('enabled', true);
}]);
}])->where('comfyui_prompt_id', $promptId)->first();
if (!$image) {
@@ -259,7 +306,16 @@ class ImageController extends Controller
Log::warning('fetchStyledImage: No enabled API Providers found for AI Model.', ['ai_model_id' => $style->aiModel->id]);
return response()->json(['error' => __('api.style_or_provider_not_found')], 404);
}
$apiProvider = $style->aiModel->apiProviders->first();
// Use the primary API provider for this AI model if available
$apiProvider = $style->aiModel->primaryApiProvider;
if (!$apiProvider) {
// Fallback to the first enabled API provider from the many-to-many relationship
$apiProvider = $style->aiModel->apiProviders->where('enabled', true)->first();
}
if (!$apiProvider) {
// If no enabled provider found, try any provider
$apiProvider = $style->aiModel->apiProviders->first();
}
Log::info('fetchStyledImage: API Provider found.', ['api_provider_id' => $apiProvider->id, 'api_provider_name' => $apiProvider->name]);
Log::info('Fetching base64 image from plugin.', ['prompt_id' => $promptId, 'api_provider' => $apiProvider->name]);
@@ -310,9 +366,85 @@ class ImageController extends Controller
}
}
public function getComfyUiUrl()
public function getComfyUiUrl(Request $request)
{
$apiProvider = ApiProvider::where('plugin', 'comfyui')->where('enabled', true)->first();
$styleId = $request->query('style_id');
$imageUuid = $request->query('image_uuid');
$apiProvider = null;
// If style_id is provided, get the API provider for that style
if ($styleId) {
$style = Style::with(['aiModel' => function ($query) {
$query->where('enabled', true)->with(['primaryApiProvider', 'apiProviders' => function ($query) {
$query->where('enabled', true);
}]);
}])->find($styleId);
if ($style && $style->aiModel) {
// Use the primary API provider for this AI model if available
$apiProvider = $style->aiModel->primaryApiProvider;
if (!$apiProvider) {
// Fallback to the first enabled API provider from the many-to-many relationship
$apiProvider = $style->aiModel->apiProviders->where('enabled', true)->first();
}
if (!$apiProvider) {
// If no enabled provider found, try any provider
$apiProvider = $style->aiModel->apiProviders->first();
}
}
}
// If image_uuid is provided, get the API provider for that image's style
elseif ($imageUuid) {
$image = Image::with(['style.aiModel' => function ($query) {
$query->with(['primaryApiProvider', 'apiProviders' => function ($query) {
$query->where('enabled', true);
}]);
}])->where('uuid', $imageUuid)->first();
if ($image && $image->style && $image->style->aiModel) {
// Use the primary API provider for this AI model if available
$apiProvider = $image->style->aiModel->primaryApiProvider;
if (!$apiProvider) {
// Fallback to the first enabled API provider from the many-to-many relationship
$apiProvider = $image->style->aiModel->apiProviders->where('enabled', true)->first();
}
if (!$apiProvider) {
// If no enabled provider found, try any provider
$apiProvider = $image->style->aiModel->apiProviders->first();
}
}
}
// Fallback to the old behavior if no style_id or image_uuid is provided
else {
// Try to get a default style if none is provided
$defaultStyleSetting = \App\Models\Setting::where('key', 'default_style_id')->first();
if ($defaultStyleSetting && $defaultStyleSetting->value) {
$style = Style::with(['aiModel' => function ($query) {
$query->where('enabled', true)->with(['primaryApiProvider', 'apiProviders' => function ($query) {
$query->where('enabled', true);
}]);
}])->find($defaultStyleSetting->value);
if ($style && $style->aiModel) {
// Use the primary API provider for this AI model if available
$apiProvider = $style->aiModel->primaryApiProvider;
if (!$apiProvider) {
// Fallback to the first enabled API provider from the many-to-many relationship
$apiProvider = $style->aiModel->apiProviders->where('enabled', true)->first();
}
if (!$apiProvider) {
// If no enabled provider found, try any provider
$apiProvider = $style->aiModel->apiProviders->first();
}
}
}
// If still no API provider, use the first available ComfyUI provider
if (!$apiProvider) {
$apiProvider = ApiProvider::where('plugin', 'ComfyUi')->where('enabled', true)->first();
}
}
if (!$apiProvider) {
return response()->json(['error' => 'No enabled ComfyUI API provider found.'], 404);

View File

@@ -4,12 +4,11 @@ namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use Symfony\Component\Process\Exception\ProcessFailedException;
use Symfony\Component\Process\Process;
use App\Services\PrinterService;
class PrintController extends Controller
{
public function printImage(Request $request)
public function printImage(Request $request, PrinterService $printerService)
{
$request->validate([
'image_path' => 'required|string',
@@ -18,36 +17,32 @@ class PrintController extends Controller
$imagePath = public_path(str_replace(url('/'), '', $request->input('image_path')));
$quantity = $request->input('quantity');
// Retrieve printer name from global settings using standard Eloquent
$printerName = \App\Models\Setting::where('key', 'selected_printer')->value('value');
if (!$printerName) {
Log::error("PrintController: Default printer name not found in settings.");
return response()->json(['error' => 'Default printer not configured.'], 500);
}
if (!$printerName) {
Log::error("PrintController: Default printer name not found in settings.");
return response()->json(['error' => 'Default printer not configured.'], 500);
}
if (!file_exists($imagePath)) {
Log::error("PrintController: Image file not found at {$imagePath}");
return response()->json(['error' => 'Image file not found.'], 404);
}
// IMPORTANT: Replace this command with one that works in your environment.
// Examples:
// Linux/macOS: $command = ['lpr', '-#', $quantity, $imagePath];
// Windows (assuming a shared printer named 'MyNetworkPrinter'):
// $command = ['print', '/d:\\MyNetworkPrinter', $imagePath];
// You might need to install additional software or configure your system
// to enable command-line printing.
// For a more robust solution, consider a dedicated print server application
// or a commercial print API.
$command = ['echo', "Simulating print of {$quantity} copies of {$imagePath}"]; // Placeholder
$printSuccess = $printerService->printImage($imagePath, $printerName, $quantity);
try {
$process = new Process($command);
$process->run();
if (!$process->isSuccessful()) {
throw new ProcessFailedException($process);
}
Log::info("PrintController: Successfully sent print command for {$imagePath} (x{$quantity})");
if ($printSuccess) {
Log::info("PrintController: Successfully sent print command for {$imagePath} (x{$quantity}) to {$printerName}");
return response()->json(['message' => 'Print command sent successfully.']);
} catch (ProcessFailedException $exception) {
Log::error("PrintController: Print command failed. Error: " . $exception->getMessage());
return response()->json(['error' => 'Failed to send print command.', 'details' => $exception->getMessage()], 500);
} else {
Log::error("PrintController: Failed to send print command for {$imagePath} (x{$quantity}) to {$printerName}");
return response()->json(['error' => 'Failed to send print command.'], 500);
}
}
}

View File

@@ -4,6 +4,7 @@ namespace App\Http\Middleware;
use Illuminate\Http\Request;
use Inertia\Middleware;
use App\Models\Setting;
class HandleInertiaRequests extends Middleware
{
@@ -35,6 +36,7 @@ class HandleInertiaRequests extends Middleware
'user' => $request->user(),
],
'locale' => app()->getLocale(),
'settings' => Setting::all()->pluck('value', 'key'),
'translations' => function () use ($request) {
$currentLocale = app()->getLocale(); // Store current locale
$requestedLocale = $request->input('locale', $currentLocale);
@@ -47,8 +49,6 @@ class HandleInertiaRequests extends Middleware
// Add other translation files as needed
];
dd($lang); // <-- ADDED FOR DEBUGGING
app()->setLocale($currentLocale); // Revert to original locale
return $lang;
},

View File

@@ -20,6 +20,11 @@ class AiModel extends Model
'parameters' => 'array',
];
public function primaryApiProvider()
{
return $this->belongsTo(ApiProvider::class, 'api_provider_id');
}
public function apiProviders()
{
return $this->belongsToMany(ApiProvider::class, 'ai_model_api_provider');

View File

@@ -0,0 +1,119 @@
<?php
namespace App\Services;
use Illuminate\Support\Facades\Log;
class PrinterService
{
public function getPrinters(): array
{
$printers = [];
$os = strtoupper(substr(PHP_OS, 0, 3));
if ($os === 'WIN') {
// Windows implementation
$output = shell_exec('wmic printer get name');
if ($output != "") {
$lines = explode(PHP_EOL, trim($output));
$start = 1; // Skip header on Windows
for ($i = $start; $i < count($lines); $i++) {
// Remove all control characters and non-printable characters, then trim
$printerName = trim(preg_replace('/[[:cntrl:]]/', '', $lines[$i]));
if (!empty($printerName)) {
$printers[$printerName] = $printerName;
}
}
}
} elseif ($os === 'DAR' || $os === 'LIN') {
// macOS and Linux implementation
$output = shell_exec('lpstat -p 2>/dev/null');
if ($output !== null) {
$lines = explode(PHP_EOL, trim($output));
foreach ($lines as $line) {
if (preg_match('/printer\s+(.*?)\s+is/', $line, $matches)) {
$printerName = trim($matches[1]);
if (!empty($printerName)) {
$printers[$printerName] = $printerName;
}
}
}
}
}
return $printers;
}
public function printImage(string $imagePath, string $printerName, int $copies = 1): bool
{
$os = strtoupper(substr(PHP_OS, 0, 3));
if ($os === 'WIN') {
// Windows implementation
$magickPath = base_path('bin/imagick/win64/magick.exe');
if (!file_exists($magickPath)) {
Log::error('PrinterService: ImageMagick executable not found.', ['path' => $magickPath]);
return false;
}
$success = true;
for ($i = 0; $i < $copies; $i++) {
// Ensure imagePath is absolute and properly quoted
$quotedImagePath = escapeshellarg($imagePath);
$quotedPrinterName = escapeshellarg("printer:$printerName"); // "printer:printerName" needs to be one argument
$command = "{$magickPath} {$quotedImagePath} {$quotedPrinterName}";
Log::debug('PrinterService: Executing print command:', ['command' => $command]);
$output = shell_exec($command);
// Log::debug('PrinterService: Executing print command:', ['command' => $command]);
$output = shell_exec($command);
Log::debug('PrinterService: Print command output:', ['output' => $output]);
// Check for errors. shell_exec returns NULL on command failure.
if ($output === null) {
Log::error('PrinterService: Print command failed.', ['command' => $command, 'output' => $output]);
$success = false;
break; // Stop if one copy fails
}
}
return $success;
} elseif ($os === 'DAR' || $os === 'LIN') {
// macOS and Linux implementation
if (!file_exists($imagePath)) {
Log::error('PrinterService: Image file not found.', ['path' => $imagePath]);
return false;
}
// Use the lp command to print the image
$success = true;
for ($i = 0; $i < $copies; $i++) {
$quotedImagePath = escapeshellarg($imagePath);
$quotedPrinterName = escapeshellarg($printerName);
// For image printing on Linux/macOS, we might need to convert to a printable format first
// Using lp command directly with the image file
$command = "lp -d {$quotedPrinterName} {$quotedImagePath} 2>&1";
Log::debug('PrinterService: Executing print command:', ['command' => $command]);
$output = shell_exec($command);
Log::debug('PrinterService: Print command output:', ['output' => $output]);
// Check for errors
if ($output === null || (strpos($output, 'Error') !== false)) {
Log::error('PrinterService: Print command failed.', ['command' => $command, 'output' => $output]);
$success = false;
break; // Stop if one copy fails
}
}
return $success;
} else {
Log::error('PrinterService: Unsupported operating system.');
return false;
}
}
}