Merge pull request 'style-gallery-laravel12' (#1) from style-gallery-laravel12 into main
Reviewed-on: http://192.168.78.2:10880/soeren/ai-stylegallery/pulls/1
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -19,3 +19,4 @@ yarn-error.log
|
|||||||
/.vscode
|
/.vscode
|
||||||
GEMINI.md
|
GEMINI.md
|
||||||
GEMINI.md.prompt
|
GEMINI.md.prompt
|
||||||
|
app/Filament/Resources/Styles/StyleResource.php
|
||||||
|
|||||||
16
.kilocode/mcp.json
Normal file
16
.kilocode/mcp.json
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"laravel-boost": {
|
||||||
|
"command": "php",
|
||||||
|
"args": [
|
||||||
|
"artisan",
|
||||||
|
"boost:mcp"
|
||||||
|
],
|
||||||
|
"transport": "stdio",
|
||||||
|
"alwaysAllow": [
|
||||||
|
"application-info"
|
||||||
|
],
|
||||||
|
"disabled": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
1
.phpunit.result.cache
Normal file
1
.phpunit.result.cache
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"version":2,"defects":{"Tests\\Feature\\Auth\\AuthenticationTest::test_users_can_authenticate_using_the_login_screen":8,"Tests\\Feature\\Auth\\AuthenticationTest::test_users_can_not_authenticate_with_invalid_password":8,"Tests\\Feature\\Auth\\AuthenticationTest::test_users_can_logout":8,"Tests\\Feature\\Auth\\EmailVerificationTest::test_email_verification_screen_can_be_rendered":8,"Tests\\Feature\\Auth\\EmailVerificationTest::test_email_can_be_verified":8,"Tests\\Feature\\Auth\\EmailVerificationTest::test_email_is_not_verified_with_invalid_hash":8,"Tests\\Feature\\Auth\\PasswordConfirmationTest::test_confirm_password_screen_can_be_rendered":8,"Tests\\Feature\\Auth\\PasswordConfirmationTest::test_password_can_be_confirmed":8,"Tests\\Feature\\Auth\\PasswordConfirmationTest::test_password_is_not_confirmed_with_invalid_password":8,"Tests\\Feature\\Auth\\PasswordResetTest::test_reset_password_link_can_be_requested":8,"Tests\\Feature\\Auth\\PasswordResetTest::test_reset_password_screen_can_be_rendered":8,"Tests\\Feature\\Auth\\PasswordResetTest::test_password_can_be_reset_with_valid_token":8,"Tests\\Feature\\Auth\\PasswordUpdateTest::test_password_can_be_updated":8,"Tests\\Feature\\Auth\\PasswordUpdateTest::test_correct_password_must_be_provided_to_update_password":8,"Tests\\Feature\\Auth\\RegistrationTest::test_new_users_can_register":7,"Tests\\Feature\\ProfileTest::test_profile_page_is_displayed":8,"Tests\\Feature\\ProfileTest::test_profile_information_can_be_updated":8,"Tests\\Feature\\ProfileTest::test_email_verification_status_is_unchanged_when_the_email_address_is_unchanged":8,"Tests\\Feature\\ProfileTest::test_user_can_delete_their_account":8,"Tests\\Feature\\ProfileTest::test_correct_password_must_be_provided_to_delete_account":8,"Tests\\Feature\\ExampleTest::test_the_application_returns_a_successful_response":7,"Tests\\Feature\\Filament\\ResourcePagesTest::test_admin_resource_index_pages_render":7,"Tests\\Feature\\Filament\\ResourcePagesTest::test_admin_resource_create_pages_render":7,"Tests\\Feature\\Filament\\ResourcePagesTest::test_admin_resource_edit_pages_render":7},"times":{"Tests\\Unit\\ExampleTest::test_that_true_is_true":0.001,"Tests\\Feature\\Auth\\AuthenticationTest::test_login_screen_can_be_rendered":2.652,"Tests\\Feature\\Auth\\AuthenticationTest::test_users_can_authenticate_using_the_login_screen":0.844,"Tests\\Feature\\Auth\\AuthenticationTest::test_users_can_not_authenticate_with_invalid_password":0.003,"Tests\\Feature\\Auth\\AuthenticationTest::test_users_can_logout":0.003,"Tests\\Feature\\Auth\\EmailVerificationTest::test_email_verification_screen_can_be_rendered":0.004,"Tests\\Feature\\Auth\\EmailVerificationTest::test_email_can_be_verified":0.003,"Tests\\Feature\\Auth\\EmailVerificationTest::test_email_is_not_verified_with_invalid_hash":0.004,"Tests\\Feature\\Auth\\PasswordConfirmationTest::test_confirm_password_screen_can_be_rendered":0.003,"Tests\\Feature\\Auth\\PasswordConfirmationTest::test_password_can_be_confirmed":0.004,"Tests\\Feature\\Auth\\PasswordConfirmationTest::test_password_is_not_confirmed_with_invalid_password":0.011,"Tests\\Feature\\Auth\\PasswordResetTest::test_reset_password_link_screen_can_be_rendered":0.685,"Tests\\Feature\\Auth\\PasswordResetTest::test_reset_password_link_can_be_requested":0.079,"Tests\\Feature\\Auth\\PasswordResetTest::test_reset_password_screen_can_be_rendered":0.004,"Tests\\Feature\\Auth\\PasswordResetTest::test_password_can_be_reset_with_valid_token":0.003,"Tests\\Feature\\Auth\\PasswordUpdateTest::test_password_can_be_updated":0.003,"Tests\\Feature\\Auth\\PasswordUpdateTest::test_correct_password_must_be_provided_to_update_password":0.003,"Tests\\Feature\\Auth\\RegistrationTest::test_registration_screen_can_be_rendered":0.653,"Tests\\Feature\\Auth\\RegistrationTest::test_new_users_can_register":2.449,"Tests\\Feature\\ExampleTest::test_the_application_returns_a_successful_response":3.546,"Tests\\Feature\\ProfileTest::test_profile_page_is_displayed":0.003,"Tests\\Feature\\ProfileTest::test_profile_information_can_be_updated":0.008,"Tests\\Feature\\ProfileTest::test_email_verification_status_is_unchanged_when_the_email_address_is_unchanged":0.005,"Tests\\Feature\\ProfileTest::test_user_can_delete_their_account":0.004,"Tests\\Feature\\ProfileTest::test_correct_password_must_be_provided_to_delete_account":0.004,"Tests\\Feature\\Filament\\ResourcePagesTest::test_admin_resource_index_pages_render":6.542,"Tests\\Feature\\Filament\\ResourcePagesTest::test_admin_resource_create_pages_render":3.055,"Tests\\Feature\\Filament\\ResourcePagesTest::test_admin_resource_edit_pages_render":3.132}}
|
||||||
515
AGENTS.md
Normal file
515
AGENTS.md
Normal file
@@ -0,0 +1,515 @@
|
|||||||
|
<laravel-boost-guidelines>
|
||||||
|
=== foundation rules ===
|
||||||
|
|
||||||
|
# Laravel Boost Guidelines
|
||||||
|
|
||||||
|
The Laravel Boost guidelines are specifically curated by Laravel maintainers for this application. These guidelines should be followed closely to enhance the user's satisfaction building Laravel applications.
|
||||||
|
|
||||||
|
## Foundational Context
|
||||||
|
This application is a Laravel application and its main Laravel ecosystems package & versions are below. You are an expert with them all. Ensure you abide by these specific packages & versions.
|
||||||
|
|
||||||
|
- php - 8.3.24
|
||||||
|
- filament/filament (FILAMENT) - v3
|
||||||
|
- inertiajs/inertia-laravel (INERTIA) - v1
|
||||||
|
- laravel/breeze (BREEZE) - v2
|
||||||
|
- laravel/framework (LARAVEL) - v12
|
||||||
|
- laravel/prompts (PROMPTS) - v0
|
||||||
|
- laravel/sanctum (SANCTUM) - v4
|
||||||
|
- livewire/livewire (LIVEWIRE) - v3
|
||||||
|
- tightenco/ziggy (ZIGGY) - v2
|
||||||
|
- laravel/mcp (MCP) - v0
|
||||||
|
- laravel/pint (PINT) - v1
|
||||||
|
- laravel/sail (SAIL) - v1
|
||||||
|
- phpunit/phpunit (PHPUNIT) - v12
|
||||||
|
- laravel-echo (ECHO) - v2
|
||||||
|
- @inertiajs/vue3 (INERTIA) - v1
|
||||||
|
- tailwindcss (TAILWINDCSS) - v3
|
||||||
|
- vue (VUE) - v3
|
||||||
|
|
||||||
|
## Conventions
|
||||||
|
- You must follow all existing code conventions used in this application. When creating or editing a file, check sibling files for the correct structure, approach, naming.
|
||||||
|
- Use descriptive names for variables and methods. For example, `isRegisteredForDiscounts`, not `discount()`.
|
||||||
|
- Check for existing components to reuse before writing a new one.
|
||||||
|
|
||||||
|
## Verification Scripts
|
||||||
|
- Do not create verification scripts or tinker when tests cover that functionality and prove it works. Unit and feature tests are more important.
|
||||||
|
|
||||||
|
## Application Structure & Architecture
|
||||||
|
- Stick to existing directory structure - don't create new base folders without approval.
|
||||||
|
- Do not change the application's dependencies without approval.
|
||||||
|
|
||||||
|
## Frontend Bundling
|
||||||
|
- If the user doesn't see a frontend change reflected in the UI, it could mean they need to run `npm run build`, `npm run dev`, or `composer run dev`. Ask them.
|
||||||
|
|
||||||
|
## Replies
|
||||||
|
- Be concise in your explanations - focus on what's important rather than explaining obvious details.
|
||||||
|
|
||||||
|
## Documentation Files
|
||||||
|
- You must only create documentation files if explicitly requested by the user.
|
||||||
|
|
||||||
|
|
||||||
|
=== boost rules ===
|
||||||
|
|
||||||
|
## Laravel Boost
|
||||||
|
- Laravel Boost is an MCP server that comes with powerful tools designed specifically for this application. Use them.
|
||||||
|
|
||||||
|
## Artisan
|
||||||
|
- Use the `list-artisan-commands` tool when you need to call an Artisan command to double check the available parameters.
|
||||||
|
|
||||||
|
## URLs
|
||||||
|
- Whenever you share a project URL with the user you should use the `get-absolute-url` tool to ensure you're using the correct scheme, domain / IP, and port.
|
||||||
|
|
||||||
|
## Tinker / Debugging
|
||||||
|
- You should use the `tinker` tool when you need to execute PHP to debug code or query Eloquent models directly.
|
||||||
|
- Use the `database-query` tool when you only need to read from the database.
|
||||||
|
|
||||||
|
## Reading Browser Logs With the `browser-logs` Tool
|
||||||
|
- You can read browser logs, errors, and exceptions using the `browser-logs` tool from Boost.
|
||||||
|
- Only recent browser logs will be useful - ignore old logs.
|
||||||
|
|
||||||
|
## Searching Documentation (Critically Important)
|
||||||
|
- Boost comes with a powerful `search-docs` tool you should use before any other approaches. This tool automatically passes a list of installed packages and their versions to the remote Boost API, so it returns only version-specific documentation specific for the user's circumstance. You should pass an array of packages to filter on if you know you need docs for particular packages.
|
||||||
|
- The 'search-docs' tool is perfect for all Laravel related packages, including Laravel, Inertia, Livewire, Filament, Tailwind, Pest, Nova, Nightwatch, etc.
|
||||||
|
- You must use this tool to search for Laravel-ecosystem documentation before falling back to other approaches.
|
||||||
|
- Search the documentation before making code changes to ensure we are taking the correct approach.
|
||||||
|
- Use multiple, broad, simple, topic based queries to start. For example: `['rate limiting', 'routing rate limiting', 'routing']`.
|
||||||
|
- Do not add package names to queries - package information is already shared. For example, use `test resource table`, not `filament 4 test resource table`.
|
||||||
|
|
||||||
|
### Available Search Syntax
|
||||||
|
- You can and should pass multiple queries at once. The most relevant results will be returned first.
|
||||||
|
|
||||||
|
1. Simple Word Searches with auto-stemming - query=authentication - finds 'authenticate' and 'auth'
|
||||||
|
2. Multiple Words (AND Logic) - query=rate limit - finds knowledge containing both "rate" AND "limit"
|
||||||
|
3. Quoted Phrases (Exact Position) - query="infinite scroll" - Words must be adjacent and in that order
|
||||||
|
4. Mixed Queries - query=middleware "rate limit" - "middleware" AND exact phrase "rate limit"
|
||||||
|
5. Multiple Queries - queries=["authentication", "middleware"] - ANY of these terms
|
||||||
|
|
||||||
|
|
||||||
|
=== php rules ===
|
||||||
|
|
||||||
|
## PHP
|
||||||
|
|
||||||
|
- Always use curly braces for control structures, even if it has one line.
|
||||||
|
|
||||||
|
### Constructors
|
||||||
|
- Use PHP 8 constructor property promotion in `__construct()`.
|
||||||
|
- <code-snippet>public function __construct(public GitHub $github) { }</code-snippet>
|
||||||
|
- Do not allow empty `__construct()` methods with zero parameters.
|
||||||
|
|
||||||
|
### Type Declarations
|
||||||
|
- Always use explicit return type declarations for methods and functions.
|
||||||
|
- Use appropriate PHP type hints for method parameters.
|
||||||
|
|
||||||
|
<code-snippet name="Explicit Return Types and Method Params" lang="php">
|
||||||
|
protected function isAccessible(User $user, ?string $path = null): bool
|
||||||
|
{
|
||||||
|
...
|
||||||
|
}
|
||||||
|
</code-snippet>
|
||||||
|
|
||||||
|
## Comments
|
||||||
|
- Prefer PHPDoc blocks over comments. Never use comments within the code itself unless there is something _very_ complex going on.
|
||||||
|
|
||||||
|
## PHPDoc Blocks
|
||||||
|
- Add useful array shape type definitions for arrays when appropriate.
|
||||||
|
|
||||||
|
## Enums
|
||||||
|
- Typically, keys in an Enum should be TitleCase. For example: `FavoritePerson`, `BestLake`, `Monthly`.
|
||||||
|
|
||||||
|
|
||||||
|
=== inertia-laravel/core rules ===
|
||||||
|
|
||||||
|
## Inertia Core
|
||||||
|
|
||||||
|
- Inertia.js components should be placed in the `resources/js/Pages` directory unless specified differently in the JS bundler (vite.config.js).
|
||||||
|
- Use `Inertia::render()` for server-side routing instead of traditional Blade views.
|
||||||
|
- Use `search-docs` for accurate guidance on all things Inertia.
|
||||||
|
|
||||||
|
<code-snippet lang="php" name="Inertia::render Example">
|
||||||
|
// routes/web.php example
|
||||||
|
Route::get('/users', function () {
|
||||||
|
return Inertia::render('Users/Index', [
|
||||||
|
'users' => User::all()
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
</code-snippet>
|
||||||
|
|
||||||
|
|
||||||
|
=== inertia-laravel/v1 rules ===
|
||||||
|
|
||||||
|
## Inertia v1
|
||||||
|
|
||||||
|
- Inertia v1 does _not_ come with these features. Do not recommend using these Inertia v2 features directly.
|
||||||
|
- Polling
|
||||||
|
- Prefetching
|
||||||
|
- Deferred props
|
||||||
|
- Infinite scrolling using merging props and `WhenVisible`
|
||||||
|
- Lazy loading data on scroll
|
||||||
|
|
||||||
|
|
||||||
|
=== laravel/core rules ===
|
||||||
|
|
||||||
|
## Do Things the Laravel Way
|
||||||
|
|
||||||
|
- Use `php artisan make:` commands to create new files (i.e. migrations, controllers, models, etc.). You can list available Artisan commands using the `list-artisan-commands` tool.
|
||||||
|
- If you're creating a generic PHP class, use `artisan make:class`.
|
||||||
|
- Pass `--no-interaction` to all Artisan commands to ensure they work without user input. You should also pass the correct `--options` to ensure correct behavior.
|
||||||
|
|
||||||
|
### Database
|
||||||
|
- Always use proper Eloquent relationship methods with return type hints. Prefer relationship methods over raw queries or manual joins.
|
||||||
|
- Use Eloquent models and relationships before suggesting raw database queries
|
||||||
|
- Avoid `DB::`; prefer `Model::query()`. Generate code that leverages Laravel's ORM capabilities rather than bypassing them.
|
||||||
|
- Generate code that prevents N+1 query problems by using eager loading.
|
||||||
|
- Use Laravel's query builder for very complex database operations.
|
||||||
|
|
||||||
|
### Model Creation
|
||||||
|
- When creating new models, create useful factories and seeders for them too. Ask the user if they need any other things, using `list-artisan-commands` to check the available options to `php artisan make:model`.
|
||||||
|
|
||||||
|
### APIs & Eloquent Resources
|
||||||
|
- For APIs, default to using Eloquent API Resources and API versioning unless existing API routes do not, then you should follow existing application convention.
|
||||||
|
|
||||||
|
### Controllers & Validation
|
||||||
|
- Always create Form Request classes for validation rather than inline validation in controllers. Include both validation rules and custom error messages.
|
||||||
|
- Check sibling Form Requests to see if the application uses array or string based validation rules.
|
||||||
|
|
||||||
|
### Queues
|
||||||
|
- Use queued jobs for time-consuming operations with the `ShouldQueue` interface.
|
||||||
|
|
||||||
|
### Authentication & Authorization
|
||||||
|
- Use Laravel's built-in authentication and authorization features (gates, policies, Sanctum, etc.).
|
||||||
|
|
||||||
|
### URL Generation
|
||||||
|
- When generating links to other pages, prefer named routes and the `route()` function.
|
||||||
|
|
||||||
|
### Configuration
|
||||||
|
- Use environment variables only in configuration files - never use the `env()` function directly outside of config files. Always use `config('app.name')`, not `env('APP_NAME')`.
|
||||||
|
|
||||||
|
### Testing
|
||||||
|
- When creating models for tests, use the factories for the models. Check if the factory has custom states that can be used before manually setting up the model.
|
||||||
|
- Faker: Use methods such as `$this->faker->word()` or `fake()->randomDigit()`. Follow existing conventions whether to use `$this->faker` or `fake()`.
|
||||||
|
- When creating tests, make use of `php artisan make:test [options] <name>` to create a feature test, and pass `--unit` to create a unit test. Most tests should be feature tests.
|
||||||
|
|
||||||
|
### Vite Error
|
||||||
|
- If you receive an "Illuminate\Foundation\ViteException: Unable to locate file in Vite manifest" error, you can run `npm run build` or ask the user to run `npm run dev` or `composer run dev`.
|
||||||
|
|
||||||
|
|
||||||
|
=== laravel/v12 rules ===
|
||||||
|
|
||||||
|
## Laravel 12
|
||||||
|
|
||||||
|
- Use the `search-docs` tool to get version specific documentation.
|
||||||
|
- Since Laravel 11, Laravel has a new streamlined file structure which this project uses.
|
||||||
|
|
||||||
|
### Laravel 12 Structure
|
||||||
|
- No middleware files in `app/Http/Middleware/`.
|
||||||
|
- `bootstrap/app.php` is the file to register middleware, exceptions, and routing files.
|
||||||
|
- `bootstrap/providers.php` contains application specific service providers.
|
||||||
|
- **No app\Console\Kernel.php** - use `bootstrap/app.php` or `routes/console.php` for console configuration.
|
||||||
|
- **Commands auto-register** - files in `app/Console/Commands/` are automatically available and do not require manual registration.
|
||||||
|
|
||||||
|
### Database
|
||||||
|
- When modifying a column, the migration must include all of the attributes that were previously defined on the column. Otherwise, they will be dropped and lost.
|
||||||
|
- Laravel 11 allows limiting eagerly loaded records natively, without external packages: `$query->latest()->limit(10);`.
|
||||||
|
|
||||||
|
### Models
|
||||||
|
- Casts can and likely should be set in a `casts()` method on a model rather than the `$casts` property. Follow existing conventions from other models.
|
||||||
|
|
||||||
|
|
||||||
|
=== livewire/core rules ===
|
||||||
|
|
||||||
|
## Livewire Core
|
||||||
|
- Use the `search-docs` tool to find exact version specific documentation for how to write Livewire & Livewire tests.
|
||||||
|
- Use the `php artisan make:livewire [Posts\CreatePost]` artisan command to create new components
|
||||||
|
- State should live on the server, with the UI reflecting it.
|
||||||
|
- All Livewire requests hit the Laravel backend, they're like regular HTTP requests. Always validate form data, and run authorization checks in Livewire actions.
|
||||||
|
|
||||||
|
## Livewire Best Practices
|
||||||
|
- Livewire components require a single root element.
|
||||||
|
- Use `wire:loading` and `wire:dirty` for delightful loading states.
|
||||||
|
- Add `wire:key` in loops:
|
||||||
|
|
||||||
|
```blade
|
||||||
|
@foreach ($items as $item)
|
||||||
|
<div wire:key="item-{{ $item->id }}">
|
||||||
|
{{ $item->name }}
|
||||||
|
</div>
|
||||||
|
@endforeach
|
||||||
|
```
|
||||||
|
|
||||||
|
- Prefer lifecycle hooks like `mount()`, `updatedFoo()` for initialization and reactive side effects:
|
||||||
|
|
||||||
|
<code-snippet name="Lifecycle hook examples" lang="php">
|
||||||
|
public function mount(User $user) { $this->user = $user; }
|
||||||
|
public function updatedSearch() { $this->resetPage(); }
|
||||||
|
</code-snippet>
|
||||||
|
|
||||||
|
|
||||||
|
## Testing Livewire
|
||||||
|
|
||||||
|
<code-snippet name="Example Livewire component test" lang="php">
|
||||||
|
Livewire::test(Counter::class)
|
||||||
|
->assertSet('count', 0)
|
||||||
|
->call('increment')
|
||||||
|
->assertSet('count', 1)
|
||||||
|
->assertSee(1)
|
||||||
|
->assertStatus(200);
|
||||||
|
</code-snippet>
|
||||||
|
|
||||||
|
|
||||||
|
<code-snippet name="Testing a Livewire component exists within a page" lang="php">
|
||||||
|
$this->get('/posts/create')
|
||||||
|
->assertSeeLivewire(CreatePost::class);
|
||||||
|
</code-snippet>
|
||||||
|
|
||||||
|
|
||||||
|
=== livewire/v3 rules ===
|
||||||
|
|
||||||
|
## Livewire 3
|
||||||
|
|
||||||
|
### Key Changes From Livewire 2
|
||||||
|
- These things changed in Livewire 2, but may not have been updated in this application. Verify this application's setup to ensure you conform with application conventions.
|
||||||
|
- Use `wire:model.live` for real-time updates, `wire:model` is now deferred by default.
|
||||||
|
- Components now use the `App\Livewire` namespace (not `App\Http\Livewire`).
|
||||||
|
- Use `$this->dispatch()` to dispatch events (not `emit` or `dispatchBrowserEvent`).
|
||||||
|
- Use the `components.layouts.app` view as the typical layout path (not `layouts.app`).
|
||||||
|
|
||||||
|
### New Directives
|
||||||
|
- `wire:show`, `wire:transition`, `wire:cloak`, `wire:offline`, `wire:target` are available for use. Use the documentation to find usage examples.
|
||||||
|
|
||||||
|
### Alpine
|
||||||
|
- Alpine is now included with Livewire, don't manually include Alpine.js.
|
||||||
|
- Plugins included with Alpine: persist, intersect, collapse, and focus.
|
||||||
|
|
||||||
|
### Lifecycle Hooks
|
||||||
|
- You can listen for `livewire:init` to hook into Livewire initialization, and `fail.status === 419` for the page expiring:
|
||||||
|
|
||||||
|
<code-snippet name="livewire:load example" lang="js">
|
||||||
|
document.addEventListener('livewire:init', function () {
|
||||||
|
Livewire.hook('request', ({ fail }) => {
|
||||||
|
if (fail && fail.status === 419) {
|
||||||
|
alert('Your session expired');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Livewire.hook('message.failed', (message, component) => {
|
||||||
|
console.error(message);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</code-snippet>
|
||||||
|
|
||||||
|
|
||||||
|
=== pint/core rules ===
|
||||||
|
|
||||||
|
## Laravel Pint Code Formatter
|
||||||
|
|
||||||
|
- You must run `vendor/bin/pint --dirty` before finalizing changes to ensure your code matches the project's expected style.
|
||||||
|
- Do not run `vendor/bin/pint --test`, simply run `vendor/bin/pint` to fix any formatting issues.
|
||||||
|
|
||||||
|
|
||||||
|
=== phpunit/core rules ===
|
||||||
|
|
||||||
|
## PHPUnit Core
|
||||||
|
|
||||||
|
- This application uses PHPUnit for testing. All tests must be written as PHPUnit classes. Use `php artisan make:test --phpunit <name>` to create a new test.
|
||||||
|
- If you see a test using "Pest", convert it to PHPUnit.
|
||||||
|
- Every time a test has been updated, run that singular test.
|
||||||
|
- When the tests relating to your feature are passing, ask the user if they would like to also run the entire test suite to make sure everything is still passing.
|
||||||
|
- Tests should test all of the happy paths, failure paths, and weird paths.
|
||||||
|
- You must not remove any tests or test files from the tests directory without approval. These are not temporary or helper files, these are core to the application.
|
||||||
|
|
||||||
|
### Running Tests
|
||||||
|
- Run the minimal number of tests, using an appropriate filter, before finalizing.
|
||||||
|
- To run all tests: `php artisan test`.
|
||||||
|
- To run all tests in a file: `php artisan test tests/Feature/ExampleTest.php`.
|
||||||
|
- To filter on a particular test name: `php artisan test --filter=testName` (recommended after making a change to a related file).
|
||||||
|
|
||||||
|
|
||||||
|
=== inertia-vue/core rules ===
|
||||||
|
|
||||||
|
## Inertia + Vue
|
||||||
|
|
||||||
|
- Vue components must have a single root element.
|
||||||
|
- Use `router.visit()` or `<Link>` for navigation instead of traditional links.
|
||||||
|
|
||||||
|
<code-snippet name="Inertia Client Navigation" lang="vue">
|
||||||
|
|
||||||
|
import { Link } from '@inertiajs/vue3'
|
||||||
|
<Link href="/">Home</Link>
|
||||||
|
|
||||||
|
</code-snippet>
|
||||||
|
|
||||||
|
|
||||||
|
=== inertia-vue/v1/forms rules ===
|
||||||
|
|
||||||
|
## Inertia + Vue Forms
|
||||||
|
|
||||||
|
- For form handling in Inertia pages, use `router.post` and related methods. Do not use regular forms.
|
||||||
|
|
||||||
|
|
||||||
|
<code-snippet lang="vue" name="Inertia Vue Form Example">
|
||||||
|
<script setup>
|
||||||
|
import { reactive } from 'vue'
|
||||||
|
import { router } from '@inertiajs/vue3'
|
||||||
|
import { usePage } from '@inertiajs/vue3'
|
||||||
|
|
||||||
|
const page = usePage()
|
||||||
|
|
||||||
|
const form = reactive({
|
||||||
|
first_name: null,
|
||||||
|
last_name: null,
|
||||||
|
email: null,
|
||||||
|
})
|
||||||
|
|
||||||
|
function submit() {
|
||||||
|
router.post('/users', form)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<h1>Create {{ page.modelName }}</h1>
|
||||||
|
<form @submit.prevent="submit">
|
||||||
|
<label for="first_name">First name:</label>
|
||||||
|
<input id="first_name" v-model="form.first_name" />
|
||||||
|
<label for="last_name">Last name:</label>
|
||||||
|
<input id="last_name" v-model="form.last_name" />
|
||||||
|
<label for="email">Email:</label>
|
||||||
|
<input id="email" v-model="form.email" />
|
||||||
|
<button type="submit">Submit</button>
|
||||||
|
</form>
|
||||||
|
</template>
|
||||||
|
</code-snippet>
|
||||||
|
|
||||||
|
|
||||||
|
=== tailwindcss/core rules ===
|
||||||
|
|
||||||
|
## Tailwind Core
|
||||||
|
|
||||||
|
- Use Tailwind CSS classes to style HTML, check and use existing tailwind conventions within the project before writing your own.
|
||||||
|
- Offer to extract repeated patterns into components that match the project's conventions (i.e. Blade, JSX, Vue, etc..)
|
||||||
|
- Think through class placement, order, priority, and defaults - remove redundant classes, add classes to parent or child carefully to limit repetition, group elements logically
|
||||||
|
- You can use the `search-docs` tool to get exact examples from the official documentation when needed.
|
||||||
|
|
||||||
|
### Spacing
|
||||||
|
- When listing items, use gap utilities for spacing, don't use margins.
|
||||||
|
|
||||||
|
<code-snippet name="Valid Flex Gap Spacing Example" lang="html">
|
||||||
|
<div class="flex gap-8">
|
||||||
|
<div>Superior</div>
|
||||||
|
<div>Michigan</div>
|
||||||
|
<div>Erie</div>
|
||||||
|
</div>
|
||||||
|
</code-snippet>
|
||||||
|
|
||||||
|
|
||||||
|
### Dark Mode
|
||||||
|
- If existing pages and components support dark mode, new pages and components must support dark mode in a similar way, typically using `dark:`.
|
||||||
|
|
||||||
|
|
||||||
|
=== tailwindcss/v3 rules ===
|
||||||
|
|
||||||
|
## Tailwind 3
|
||||||
|
|
||||||
|
- Always use Tailwind CSS v3 - verify you're using only classes supported by this version.
|
||||||
|
|
||||||
|
|
||||||
|
=== tests rules ===
|
||||||
|
|
||||||
|
## Test Enforcement
|
||||||
|
|
||||||
|
- Every change must be programmatically tested. Write a new test or update an existing test, then run the affected tests to make sure they pass.
|
||||||
|
- Run the minimum number of tests needed to ensure code quality and speed. Use `php artisan test` with a specific filename or filter.
|
||||||
|
|
||||||
|
|
||||||
|
=== filament/filament rules ===
|
||||||
|
|
||||||
|
## Filament
|
||||||
|
- Filament is used by this application, check how and where to follow existing application conventions.
|
||||||
|
- Filament is a Server-Driven UI (SDUI) framework for Laravel. It allows developers to define user interfaces in PHP using structured configuration objects. It is built on top of Livewire, Alpine.js, and Tailwind CSS.
|
||||||
|
- You can use the `search-docs` tool to get information from the official Filament documentation when needed. This is very useful for Artisan command arguments, specific code examples, testing functionality, relationship management, and ensuring you're following idiomatic practices.
|
||||||
|
- Utilize static `make()` methods for consistent component initialization.
|
||||||
|
|
||||||
|
### Artisan
|
||||||
|
- You must use the Filament specific Artisan commands to create new files or components for Filament. You can find these with the `list-artisan-commands` tool, or with `php artisan` and the `--help` option.
|
||||||
|
- Inspect the required options, always pass `--no-interaction`, and valid arguments for other options when applicable.
|
||||||
|
|
||||||
|
### Filament's Core Features
|
||||||
|
- Actions: Handle doing something within the application, often with a button or link. Actions encapsulate the UI, the interactive modal window, and the logic that should be executed when the modal window is submitted. They can be used anywhere in the UI and are commonly used to perform one-time actions like deleting a record, sending an email, or updating data in the database based on modal form input.
|
||||||
|
- Forms: Dynamic forms rendered within other features, such as resources, action modals, table filters, and more.
|
||||||
|
- Infolists: Read-only lists of data.
|
||||||
|
- Notifications: Flash notifications displayed to users within the application.
|
||||||
|
- Panels: The top-level container in Filament that can include all other features like pages, resources, forms, tables, notifications, actions, infolists, and widgets.
|
||||||
|
- Resources: Static classes that are used to build CRUD interfaces for Eloquent models. Typically live in `app/Filament/Resources`.
|
||||||
|
- Schemas: Represent components that define the structure and behavior of the UI, such as forms, tables, or lists.
|
||||||
|
- Tables: Interactive tables with filtering, sorting, pagination, and more.
|
||||||
|
- Widgets: Small component included within dashboards, often used for displaying data in charts, tables, or as a stat.
|
||||||
|
|
||||||
|
### Relationships
|
||||||
|
- Determine if you can use the `relationship()` method on form components when you need `options` for a select, checkbox, repeater, or when building a `Fieldset`:
|
||||||
|
|
||||||
|
<code-snippet name="Relationship example for Form Select" lang="php">
|
||||||
|
Forms\Components\Select::make('user_id')
|
||||||
|
->label('Author')
|
||||||
|
->relationship('author')
|
||||||
|
->required(),
|
||||||
|
</code-snippet>
|
||||||
|
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
- It's important to test Filament functionality for user satisfaction.
|
||||||
|
- Ensure that you are authenticated to access the application within the test.
|
||||||
|
- Filament uses Livewire, so start assertions with `livewire()` or `Livewire::test()`.
|
||||||
|
|
||||||
|
### Example Tests
|
||||||
|
|
||||||
|
<code-snippet name="Filament Table Test" lang="php">
|
||||||
|
livewire(ListUsers::class)
|
||||||
|
->assertCanSeeTableRecords($users)
|
||||||
|
->searchTable($users->first()->name)
|
||||||
|
->assertCanSeeTableRecords($users->take(1))
|
||||||
|
->assertCanNotSeeTableRecords($users->skip(1))
|
||||||
|
->searchTable($users->last()->email)
|
||||||
|
->assertCanSeeTableRecords($users->take(-1))
|
||||||
|
->assertCanNotSeeTableRecords($users->take($users->count() - 1));
|
||||||
|
</code-snippet>
|
||||||
|
|
||||||
|
<code-snippet name="Filament Create Resource Test" lang="php">
|
||||||
|
livewire(CreateUser::class)
|
||||||
|
->fillForm([
|
||||||
|
'name' => 'Howdy',
|
||||||
|
'email' => 'howdy@example.com',
|
||||||
|
])
|
||||||
|
->call('create')
|
||||||
|
->assertNotified()
|
||||||
|
->assertRedirect();
|
||||||
|
|
||||||
|
assertDatabaseHas(User::class, [
|
||||||
|
'name' => 'Howdy',
|
||||||
|
'email' => 'howdy@example.com',
|
||||||
|
]);
|
||||||
|
</code-snippet>
|
||||||
|
|
||||||
|
<code-snippet name="Testing Multiple Panels (setup())" lang="php">
|
||||||
|
use Filament\Facades\Filament;
|
||||||
|
|
||||||
|
Filament::setCurrentPanel('app');
|
||||||
|
</code-snippet>
|
||||||
|
|
||||||
|
<code-snippet name="Calling an Action in a Test" lang="php">
|
||||||
|
livewire(EditInvoice::class, [
|
||||||
|
'invoice' => $invoice,
|
||||||
|
])->callAction('send');
|
||||||
|
|
||||||
|
expect($invoice->refresh())->isSent()->toBeTrue();
|
||||||
|
</code-snippet>
|
||||||
|
|
||||||
|
|
||||||
|
## Version 3 Changes To Focus On
|
||||||
|
- Resources are located in `app/Filament/Resources/` directory.
|
||||||
|
- Resource pages (List, Create, Edit) are auto-generated within the resource's directory - e.g., `app/Filament/Resources/PostResource/Pages/`.
|
||||||
|
- Forms use the `Forms\Components` namespace for form fields.
|
||||||
|
- Tables use the `Tables\Columns` namespace for table columns.
|
||||||
|
- A new `Filament\Forms\Components\RichEditor` component is available.
|
||||||
|
- Form and table schemas now use fluent method chaining.
|
||||||
|
- Added `php artisan filament:optimize` command for production optimization.
|
||||||
|
- Requires implementing `FilamentUser` contract for production access control.
|
||||||
|
</laravel-boost-guidelines>
|
||||||
404
PRP.md
Normal file
404
PRP.md
Normal file
@@ -0,0 +1,404 @@
|
|||||||
|
# Project Reconstruction Plan (PRP) for AI StyleGallery
|
||||||
|
|
||||||
|
This document outlines the architecture, functionality, and implementation details of the "AI StyleGallery" web application. It is intended to serve as a comprehensive guide for another AI agent or LLM to understand and potentially reconstruct a similar system.
|
||||||
|
|
||||||
|
## 1. Application Overview
|
||||||
|
|
||||||
|
**Primary Goal:** AI StyleGallery is a web application designed to allow users to transform their uploaded images by applying various AI-generated artistic styles. Users can manage their image gallery, apply styles via external AI services, and save or discard the results.
|
||||||
|
|
||||||
|
**Core Functionality:**
|
||||||
|
- **Image Upload:** Users can upload their own images to the gallery.
|
||||||
|
- **Style Application:** Users can select an image and choose from a predefined set of AI styles to apply.
|
||||||
|
- **AI Processing Integration:** The application interacts with external AI web services (e.g., ComfyUI, RunwareAI) to perform image transformations.
|
||||||
|
- **Gallery Management:** Users can view their original and styled images, and decide to keep (make permanent) or discard (delete) styled images.
|
||||||
|
- **Admin Panel:** An administrative interface for managing AI models, API providers, styles, users, and roles.
|
||||||
|
|
||||||
|
## 2. Core Technologies & Stack
|
||||||
|
|
||||||
|
* **Languages:** PHP 8.3, JavaScript
|
||||||
|
* **Frameworks & Runtimes:** Laravel 12.21.0, Vue.js 3.5.18, Inertia.js 1.3.0, Livewire 3.6.4, Vite 5.4.19
|
||||||
|
* **Databases:** SQLite. Redis is used for caching.
|
||||||
|
* **Key PHP Libraries/Dependencies:**
|
||||||
|
* Filament 3.3.34 (for the admin panel)
|
||||||
|
* Guzzle 7.9.3 (for HTTP requests)
|
||||||
|
* Laravel Sanctum 4.2.0 (for API authentication)
|
||||||
|
* Laravel Breeze 2.3.8 (authentication scaffolding)
|
||||||
|
* Laravel Pint 1.24.0 (code formatter)
|
||||||
|
* Laravel Prompts 0.3.6 (CLI prompts)
|
||||||
|
* Laravel Sail 1.44.0 (Docker development environment)
|
||||||
|
* Laravel Serializable Closure 2.0.4
|
||||||
|
* Laravel Tinker 2.10.1
|
||||||
|
* nesbot/carbon 3.10.2 (date/time library)
|
||||||
|
* predis/predis 3.1.0 (Redis client)
|
||||||
|
* phpunit/php-code-coverage 12.3.2
|
||||||
|
* phpunit/php-file-iterator 6.0.0
|
||||||
|
* phpunit/php-invoker 6.0.0
|
||||||
|
* phpunit/php-text-template 5.0.0
|
||||||
|
* phpunit/php-timer 8.0.0
|
||||||
|
* phpunit/phpunit 12.3.0
|
||||||
|
* ramsey/uuid 4.9.0 (UUID generation)
|
||||||
|
* spatie/laravel-package-tools 1.92.7
|
||||||
|
* tightenco/ziggy 2.5.3 (Laravel routes in JS)
|
||||||
|
* symfony/clock 7.3.0
|
||||||
|
* symfony/console 7.3.2
|
||||||
|
* symfony/css-selector 7.3.0
|
||||||
|
* symfony/deprecation-contracts 3.6.0
|
||||||
|
* symfony/error-handler 7.3.2
|
||||||
|
* symfony/event-dispatcher 7.3.0
|
||||||
|
* symfony/event-dispatcher-contracts 3.6.0
|
||||||
|
* symfony/finder 7.3.2
|
||||||
|
* symfony/html-sanitizer 7.3.2
|
||||||
|
* symfony/http-foundation 7.3.2
|
||||||
|
* symfony/http-kernel 7.3.2
|
||||||
|
* symfony/mailer 7.3.2
|
||||||
|
* symfony/mime 7.3.2
|
||||||
|
* symfony/polyfill-ctype 1.32.0
|
||||||
|
* symfony/polyfill-intl-grapheme 1.32.0
|
||||||
|
* symfony/polyfill-intl-idn 1.32.0
|
||||||
|
* symfony/polyfill-intl-normalizer 1.32.0
|
||||||
|
* symfony/polyfill-mbstring 1.32.0
|
||||||
|
* symfony/polyfill-php80 1.32.0
|
||||||
|
* symfony/polyfill-php83 1.32.0
|
||||||
|
* symfony/polyfill-uuid 1.32.0
|
||||||
|
* symfony/process 7.3.0
|
||||||
|
* symfony/routing 7.3.2
|
||||||
|
* symfony/service-contracts 3.6.0
|
||||||
|
* symfony/string 7.3.2
|
||||||
|
* symfony/translation 7.3.2
|
||||||
|
* symfony/translation-contracts 3.6.0
|
||||||
|
* symfony/uid 7.3.1
|
||||||
|
* symfony/var-dumper 7.3.2
|
||||||
|
* symfony/yaml 7.3.2
|
||||||
|
* **Key JavaScript Libraries/Dependencies:**
|
||||||
|
* axios 1.11.0 (for frontend HTTP requests)
|
||||||
|
* tailwindcss 3.4.17 (for styling)
|
||||||
|
* @inertiajs/vue3 1.3.0
|
||||||
|
* @vitejs/plugin-vue 5.2.4
|
||||||
|
* @tailwindcss/forms 0.5.10
|
||||||
|
* autoprefixer 10.4.21
|
||||||
|
* postcss 8.5.6
|
||||||
|
* laravel-echo 2.1.7 (WebSocket client)
|
||||||
|
* pusher-js 8.4.0 (Pusher client for WebSockets)
|
||||||
|
* @fortawesome/fontawesome-svg-core 7.0.0
|
||||||
|
* @fortawesome/free-solid-svg-icons 7.0.0
|
||||||
|
* @fortawesome/vue-fontawesome 3.1.1
|
||||||
|
* vanilla-lazyload 19.1.3 (lazy loading images)
|
||||||
|
* **Package Manager(s):** Composer for PHP, npm for JavaScript.
|
||||||
|
|
||||||
|
## 2. UI Structure (Frontend / Backend)
|
||||||
|
|
||||||
|
|
||||||
|
### 2.1. Frontend (Vue.js 3 with Inertia.js)
|
||||||
|
|
||||||
|
The frontend is a Single Page Application (SPA) built with Vue.js and Inertia.js, providing a dynamic user experience.
|
||||||
|
|
||||||
|
- **Gallery View (`resources/js/Pages/Home.vue`):**
|
||||||
|
- Displays a grid of images (both original and styled).
|
||||||
|
- Images are fetched from `/api/images`.
|
||||||
|
- Includes pagination for large galleries.
|
||||||
|
- Periodically polls `/api/images` to refresh the gallery and show newly styled images.
|
||||||
|
- **Image Context Menu (`resources/js/Components/ImageContextMenu.vue`):**
|
||||||
|
- Appears when a user taps on an image in the gallery.
|
||||||
|
- Provides options: "Drucken" (Print), "Stil ändern" (Change Style), "Schließen" (Close).
|
||||||
|
- Dynamically adjusts its position based on the tap location.
|
||||||
|
- Layout: Image preview (60% width) and options list (40% width).
|
||||||
|
- **Style Selector (`resources/js/Components/StyleSelector.vue`):**
|
||||||
|
- Displayed when "Stil ändern" is selected from the context menu.
|
||||||
|
- Fetches and lists available AI styles from `/api/styles`.
|
||||||
|
- Allows users to select a style.
|
||||||
|
- Lazy loads style preview images using `IntersectionObserver` for performance.
|
||||||
|
- Includes a "back" arrow to return to the main context menu.
|
||||||
|
- **Styled Image Display (Implicit):**
|
||||||
|
- Styled images appear in the main gallery after processing.
|
||||||
|
- **User Authentication/Profile:**
|
||||||
|
- Login/Logout functionality (handled by Laravel Breeze/Fortify and Inertia.js).
|
||||||
|
- User profile management (via Laravel's built-in features and Filament admin panel).
|
||||||
|
|
||||||
|
### 2.2. Backend (Laravel 12 with Filament Admin Panel)
|
||||||
|
|
||||||
|
The backend is a monolithic Laravel application, with Filament providing a powerful administrative interface.
|
||||||
|
|
||||||
|
- **Filament Admin Panel:**
|
||||||
|
- Accessible via `/admin`.
|
||||||
|
- Provides CRUD (Create, Read, Update, Delete) operations for the following resources:
|
||||||
|
- **AI Models (`app/Filament/Resources/AiModelResource.php`):** Manage AI models, their IDs, types, associated API providers, and specific parameters (JSON).
|
||||||
|
- **API Providers (`app/Filament/Resources/ApiProviderResource.php`):** Configure connections to external AI services (API URL, authentication tokens/credentials, plugin type).
|
||||||
|
- **Styles (`app/Filament/Resources/StyleResource.php`):** Define AI styles, including their title, prompt, description, preview image, parameters (JSON), and associated AI model.
|
||||||
|
- **Images (via direct database interaction and synchronization):** While there's an `ImageResource`, image management is largely automated through synchronization.
|
||||||
|
- **Users (`app/Filament/Resources/UserResource.php`):** Manage user accounts and assign roles.
|
||||||
|
- **Roles (`app/Filament/Resources/RoleResource.php`):** Define user roles and permissions.
|
||||||
|
- **Settings (`app/Filament/Resources/SettingResource.php`):** Manage application-wide settings.
|
||||||
|
|
||||||
|
## 3. Resources/Entities (Models)
|
||||||
|
|
||||||
|
The application's core data structures are represented by Eloquent models:
|
||||||
|
|
||||||
|
- **`App\Models\User`**: Represents a user account.
|
||||||
|
- `id` (PK)
|
||||||
|
- `name`
|
||||||
|
- `email`
|
||||||
|
- `password`
|
||||||
|
- `role_id` (FK to `roles` table)
|
||||||
|
- `two_factor_secret`, `two_factor_recovery_codes`, `two_factor_confirmed_at` (for 2FA)
|
||||||
|
- `settings` (JSON)
|
||||||
|
- **`App\Models\Image`**: Stores metadata for both original and styled images.
|
||||||
|
- `id` (PK)
|
||||||
|
- `path` (string, file path relative to storage disk)
|
||||||
|
- `uuid` (string, unique identifier for tracking)
|
||||||
|
- `original_image_id` (FK to `images` table, for styled images)
|
||||||
|
- `style_id` (FK to `styles` table, for styled images)
|
||||||
|
- `is_temp` (boolean, true for temporary styled images)
|
||||||
|
- `is_public` (boolean, true for publicly visible images)
|
||||||
|
- `comfyui_prompt_id` (string, for tracking ComfyUI jobs)
|
||||||
|
- `created_at`, `updated_at`
|
||||||
|
- **`App\Models\Style`**: Defines an AI style.
|
||||||
|
- `id` (PK)
|
||||||
|
- `title` (string)
|
||||||
|
- `prompt` (text, the base prompt for the AI model)
|
||||||
|
- `description` (text)
|
||||||
|
- `preview_image` (string, path to style preview image)
|
||||||
|
- `parameters` (JSON, additional parameters for the AI plugin, cast to array)
|
||||||
|
- `ai_model_id` (FK to `ai_models` table)
|
||||||
|
- `enabled` (boolean)
|
||||||
|
- `created_at`, `updated_at`
|
||||||
|
- **`App\Models\AiModel`**: Represents a specific AI model (e.g., Stable Diffusion v1.5).
|
||||||
|
- `id` (PK)
|
||||||
|
- `name` (string)
|
||||||
|
- `model_id` (string, identifier used by the AI service)
|
||||||
|
- `model_type` (string, e.g., "Stable Diffusion")
|
||||||
|
- `enabled` (boolean)
|
||||||
|
- `parameters` (JSON, additional parameters for the AI plugin, cast to array)
|
||||||
|
- `created_at`, `updated_at`
|
||||||
|
- **`App\Models\ApiProvider`**: Configures connection details for an external AI service.
|
||||||
|
- `id` (PK)
|
||||||
|
- `name` (string, e.g., "ComfyUI API")
|
||||||
|
- `api_url` (string, base URL of the AI service)
|
||||||
|
- `username` (string, nullable)
|
||||||
|
- `password` (string, nullable)
|
||||||
|
- `token` (string, nullable)
|
||||||
|
- `plugin` (string, identifies the plugin to use, e.g., "comfyui", "runwareai")
|
||||||
|
- `enabled` (boolean)
|
||||||
|
- `created_at`, `updated_at`
|
||||||
|
- **`App\Models\Role`**: Defines user roles for access control.
|
||||||
|
- `id` (PK)
|
||||||
|
- `name` (string, e.g., "admin", "user")
|
||||||
|
- `created_at`, `updated_at`
|
||||||
|
- **`App\Models\Setting`**: Stores application-wide key-value settings.
|
||||||
|
- `id` (PK)
|
||||||
|
- `key` (string)
|
||||||
|
- `value` (text)
|
||||||
|
- `created_at`, `updated_at`
|
||||||
|
|
||||||
|
## 4. Database Structure (Schema)
|
||||||
|
|
||||||
|
Key tables and their relevant columns:
|
||||||
|
|
||||||
|
- **`users`**
|
||||||
|
- `id`: `bigint unsigned auto_increment primary key`
|
||||||
|
- `name`: `varchar(255)`
|
||||||
|
- `email`: `varchar(255) unique`
|
||||||
|
- `password`: `varchar(255)`
|
||||||
|
- `role_id`: `bigint unsigned null` (FK to `roles.id`)
|
||||||
|
- `two_factor_secret`: `text null`
|
||||||
|
- `two_factor_recovery_codes`: `text null`
|
||||||
|
- `two_factor_confirmed_at`: `timestamp null`
|
||||||
|
- `settings`: `json null`
|
||||||
|
- `created_at`: `timestamp null`
|
||||||
|
- `updated_at`: `timestamp null`
|
||||||
|
|
||||||
|
- **`images`**
|
||||||
|
- `id`: `bigint unsigned auto_increment primary key`
|
||||||
|
- `path`: `varchar(255)`
|
||||||
|
- `uuid`: `char(36) unique`
|
||||||
|
- `original_image_id`: `bigint unsigned null` (FK to `images.id`)
|
||||||
|
- `style_id`: `bigint unsigned null` (FK to `styles.id`)
|
||||||
|
- `is_temp`: `tinyint(1) default 0`
|
||||||
|
- `is_public`: `tinyint(1) default 1`
|
||||||
|
- `comfyui_prompt_id`: `varchar(255) null`
|
||||||
|
- `created_at`: `timestamp null`
|
||||||
|
- `updated_at`: `timestamp null`
|
||||||
|
|
||||||
|
- **`styles`**
|
||||||
|
- `id`: `bigint unsigned auto_increment primary key`
|
||||||
|
- `title`: `varchar(255)`
|
||||||
|
- `prompt`: `longtext`
|
||||||
|
- `description`: `longtext`
|
||||||
|
- `preview_image`: `varchar(255)`
|
||||||
|
- `parameters`: `json null`
|
||||||
|
- `ai_model_id`: `bigint unsigned` (FK to `ai_models.id`)
|
||||||
|
- `enabled`: `tinyint(1) default 1`
|
||||||
|
- `created_at`: `timestamp null`
|
||||||
|
- `updated_at`: `timestamp null`
|
||||||
|
|
||||||
|
- **`ai_models`**
|
||||||
|
- `id`: `bigint unsigned auto_increment primary key`
|
||||||
|
- `name`: `varchar(255)`
|
||||||
|
- `model_id`: `varchar(255)`
|
||||||
|
- `model_type`: `varchar(255) null`
|
||||||
|
- `enabled`: `tinyint(1) default 1`
|
||||||
|
- `parameters`: `json null`
|
||||||
|
- `created_at`: `timestamp null`
|
||||||
|
- `updated_at`: `timestamp null`
|
||||||
|
|
||||||
|
- **`api_providers`**
|
||||||
|
- `id`: `bigint unsigned auto_increment primary key`
|
||||||
|
- `name`: `varchar(255)`
|
||||||
|
- `api_url`: `varchar(255)`
|
||||||
|
- `username`: `varchar(255) null`
|
||||||
|
- `password`: `varchar(255) null`
|
||||||
|
- `token`: `varchar(255) null`
|
||||||
|
- `plugin`: `varchar(255)`
|
||||||
|
- `enabled`: `tinyint(1) default 1`
|
||||||
|
- `created_at`: `timestamp null`
|
||||||
|
- `updated_at`: `timestamp null`
|
||||||
|
|
||||||
|
- **`roles`**
|
||||||
|
- `id`: `bigint unsigned auto_increment primary key`
|
||||||
|
- `name`: `varchar(255) unique`
|
||||||
|
- `created_at`: `timestamp null`
|
||||||
|
- `updated_at`: `timestamp null`
|
||||||
|
|
||||||
|
- `api_provider_id`: `bigint unsigned` (FK to `api_providers.id`)
|
||||||
|
|
||||||
|
- **`settings`**
|
||||||
|
- `id`: `bigint unsigned auto_increment primary key`
|
||||||
|
- `key`: `varchar(255) unique`
|
||||||
|
- `value`: `longtext`
|
||||||
|
- `created_at`: `timestamp null`
|
||||||
|
- `updated_at`: `timestamp null`
|
||||||
|
|
||||||
|
## 5. User Interactions
|
||||||
|
|
||||||
|
### 5.1. Frontend User Interactions
|
||||||
|
|
||||||
|
- **Image Upload:** Users can upload images via a dedicated interface (not explicitly detailed in provided context, but implied by gallery functionality).
|
||||||
|
- **Image Selection:** Tapping on an image in the gallery opens a context menu.
|
||||||
|
- **Context Menu Actions:**
|
||||||
|
- "Drucken" (Print): Placeholder for printing functionality.
|
||||||
|
- "Stil ändern" (Change Style): Navigates to the style selection interface.
|
||||||
|
- "Schließen" (Close): Closes the context menu.
|
||||||
|
- **Style Selection:**
|
||||||
|
- Users browse a list of available styles.
|
||||||
|
- Selecting a style initiates the AI image transformation process.
|
||||||
|
- "Back" arrow: Returns to the image context menu.
|
||||||
|
- **Gallery Refresh:** The gallery automatically updates to show newly processed images (polling every 5 seconds).
|
||||||
|
- **Authentication:** Users can log in and out.
|
||||||
|
|
||||||
|
### 5.2. Backend (Admin Panel) User Interactions
|
||||||
|
|
||||||
|
- **Resource Management:** Full CRUD operations for AI Models, API Providers, Styles, Users, and Roles.
|
||||||
|
- **Toggle Status:** Enable/disable AI Models, API Providers, and Styles.
|
||||||
|
- **Duplication:** Duplicate existing AI Models and Styles to quickly create new ones.
|
||||||
|
- **Settings Management:** Update application-wide settings.
|
||||||
|
|
||||||
|
## 6. External System Interactions
|
||||||
|
|
||||||
|
### 6.1. AI Web Services
|
||||||
|
|
||||||
|
The application integrates with external AI services for image processing. The specific plugin used is determined by the `plugin` field in the `ApiProvider` model.
|
||||||
|
|
||||||
|
- **ComfyUI (via `App\Api\Plugins\ComfyUi.php`):**
|
||||||
|
- **Image Upload:** Sends image data (base64 encoded) to the ComfyUI server's `/upload/image` endpoint.
|
||||||
|
- **Prompt Queuing:** Sends the constructed workflow JSON to the ComfyUI server's `/prompt` endpoint.
|
||||||
|
- **Result Fetching:** Polls the ComfyUI server's `/history/{prompt_id}` endpoint to check the status and retrieve the base64 encoded styled image.
|
||||||
|
- **RunwareAI (via `App\Api\Plugins\RunwareAi.php`):**
|
||||||
|
- **Image Upload:** Sends image data (base64 encoded) to the RunwareAI API.
|
||||||
|
- **Style Change Request:** Sends a request with prompt, seed image UUID, model ID, and merged parameters to the RunwareAI API for image inference.
|
||||||
|
|
||||||
|
### 6.2. Authentication
|
||||||
|
|
||||||
|
- **Laravel Sanctum:** Used for API authentication, securing routes that require user login (e.g., keeping/deleting images, admin panel access).
|
||||||
|
|
||||||
|
## 7. Call Structure and Access Rights
|
||||||
|
|
||||||
|
### 7.1. API Routes (`routes/api.php`)
|
||||||
|
|
||||||
|
- **Publicly Accessible Routes:**
|
||||||
|
- `GET /api/images`: Retrieves a list of images (public and temporary for unauthenticated users, all for authenticated users). Handled by `ImageController@index`.
|
||||||
|
- `GET /api/styles`: Retrieves a list of available styles. Handled by `StyleController@index`.
|
||||||
|
- `POST /api/images/style-change`: Initiates an AI style change request. Handled by `ImageController@styleChangeRequest`.
|
||||||
|
- `GET /api/comfyui-url`: Retrieves the ComfyUI API URL. Handled by `ImageController@getComfyUiUrl`.
|
||||||
|
- `GET /api/images/fetch-styled/{prompt_id}`: Fetches the result of a styled image request. Handled by `ImageController@fetchStyledImage`.
|
||||||
|
- **Authenticated Routes (requires `auth:sanctum` middleware):**
|
||||||
|
- `GET /api/user`: Retrieves authenticated user details.
|
||||||
|
- `POST /api/admin/navigation-state`: Stores admin navigation state. Handled by `NavigationStateController@store`.
|
||||||
|
- `POST /api/images/keep`: Marks a temporary styled image as permanent. Handled by `ImageController@keepImage`.
|
||||||
|
- `DELETE /api/images/{image}`: Deletes an image. Handled by `ImageController@deleteImage`.
|
||||||
|
- `GET /api/images/status`: Retrieves the status of an image (placeholder for future use). Handled by `ImageController@getStatus`.
|
||||||
|
|
||||||
|
### 7.2. Web Routes (`routes/web.php`)
|
||||||
|
|
||||||
|
- Standard Laravel web routes for serving the Inertia.js frontend. The main application entry point is typically handled by a route that returns an Inertia response (e.g., `Route::get('/', ...)`).
|
||||||
|
|
||||||
|
### 7.3. Access Rights
|
||||||
|
|
||||||
|
- **API Authentication:** Laravel Sanctum is used to protect API routes. Users must be authenticated to access certain functionalities.
|
||||||
|
- **Filament Admin Panel:** Access to the `/admin` panel is controlled by Filament's built-in authentication and authorization system, typically based on user roles.
|
||||||
|
- **Image Visibility:**
|
||||||
|
- `is_public` flag on `Image` model: Controls whether an image is visible to unauthenticated users.
|
||||||
|
- `is_temp` flag on `Image` model: Identifies temporary styled images. Unauthenticated users can see their own temporary images.
|
||||||
|
- **Resource Status:**
|
||||||
|
- `enabled` flag on `AiModel`, `ApiProvider`, and `Style` models: Controls whether these resources are active and usable within the application.
|
||||||
|
|
||||||
|
## 8. Detailed Description of Key Logic/Flows
|
||||||
|
|
||||||
|
### 8.1. Image Styling Process
|
||||||
|
|
||||||
|
1. **User Action (Frontend):** A user selects an image in the gallery and chooses a style from the `StyleSelector` component.
|
||||||
|
2. **Frontend Request:** The `Home.vue` component sends an `axios.post` request to `/api/images/style-change`, including the `image_id` and `style_id`.
|
||||||
|
3. **Backend (ImageController@styleChangeRequest):**
|
||||||
|
- Retrieves the `Image` and `Style` models based on the provided IDs.
|
||||||
|
- Retrieves the associated `AiModel` and `ApiProvider` from the `Style` model.
|
||||||
|
- Loads the appropriate AI plugin (e.g., `ComfyUi`, `RunwareAi`) using `PluginLoader::getPlugin()`.
|
||||||
|
- Calls the `processImageStyleChange()` method on the loaded plugin, passing the `Image` and `Style` models.
|
||||||
|
4. **AI Plugin (`ComfyUi.php` or `RunwareAi.php`):**
|
||||||
|
- **Image Upload:** The plugin first uploads the original image to the external AI service.
|
||||||
|
- **Parameter Merging:**
|
||||||
|
- It retrieves `parameters` from both the `AiModel` and the `Style` models.
|
||||||
|
- These parameters (which are JSON objects) are deeply merged using `array_replace_recursive()`. This ensures that specific parameters from the style can override or extend parameters defined at the model level.
|
||||||
|
- **Prompt Construction:** The plugin constructs the final prompt/workflow for the AI service.
|
||||||
|
- For ComfyUI, it takes the merged parameters (workflow JSON) and performs string replacements for placeholders like `__PROMPT__` (from `Style->prompt`), `__FILENAME__` (uploaded image filename), and `__MODEL_ID__` (from `AiModel->model_id`).
|
||||||
|
- Crucially, values for placeholders are JSON-encoded and then stripped of outer quotes to ensure safe injection into the JSON workflow, preventing syntax errors.
|
||||||
|
- **API Call:** The plugin sends the prepared request (e.g., prompt to ComfyUI, inference request to RunwareAI) to the external AI service.
|
||||||
|
- **Response Handling:** The plugin waits for a response from the AI service (e.g., `prompt_id` from ComfyUI, base64 image data from RunwareAI).
|
||||||
|
5. **Backend (ImageController@styleChangeRequest - continued):**
|
||||||
|
- Receives the initial response from the plugin (e.g., `prompt_id`).
|
||||||
|
- Updates the `Image` record with the `comfyui_prompt_id` and `style_id` for tracking.
|
||||||
|
- Returns a JSON response to the frontend with the `prompt_id` and `image_uuid`.
|
||||||
|
6. **Frontend (Home.vue - Polling for Result):**
|
||||||
|
- Upon receiving the `prompt_id`, the frontend starts polling `/api/images/fetch-styled/{prompt_id}`.
|
||||||
|
7. **Backend (ImageController@fetchStyledImage):**
|
||||||
|
- Retrieves the `Image` record using the `comfyui_prompt_id`.
|
||||||
|
- Loads the relevant AI plugin.
|
||||||
|
- Calls a method on the plugin (e.g., `waitForResult` for ComfyUI) to retrieve the final base64 encoded styled image data.
|
||||||
|
- Decodes the base64 image, saves it to `public/storage/uploads` (e.g., `styled_UUID.png`).
|
||||||
|
- Creates a new `Image` record in the database for the styled image, linking it to the original image and style, and setting `is_temp` to `true`.
|
||||||
|
- Returns a JSON response with the styled image's details.
|
||||||
|
8. **Frontend (Home.vue - Gallery Refresh):**
|
||||||
|
- Upon successful fetching of the styled image, the frontend calls `fetchImages()` to refresh the gallery, which now includes the newly created styled image.
|
||||||
|
|
||||||
|
### 8.2. Image Synchronization
|
||||||
|
|
||||||
|
- The `ImageController@index` method performs a synchronization between the `public/storage/uploads` directory and the `images` table in the database.
|
||||||
|
- It adds new image files found on disk to the database.
|
||||||
|
- It removes database entries for images that no longer exist on disk.
|
||||||
|
- This ensures the gallery always reflects the actual files in storage.
|
||||||
|
|
||||||
|
### 8.3. Parameter Merging Logic
|
||||||
|
|
||||||
|
- The `parameters` fields in `Style` and `AiModel` are stored as JSON in the database and automatically cast to PHP arrays by Eloquent.
|
||||||
|
- In the AI plugins (`ComfyUi.php`, `RunwareAi.php`), when constructing the AI service request:
|
||||||
|
- `$modelParams = $style->aiModel->parameters ?? [];`
|
||||||
|
- `$styleParams = $style->parameters ?? [];`
|
||||||
|
- `$mergedParams = array_replace_recursive($modelParams, $styleParams);`
|
||||||
|
- This `array_replace_recursive` function is crucial for deep merging, allowing style-specific parameters to override or extend model-level parameters.
|
||||||
|
- Placeholders (`__PROMPT__`, `__FILENAME__`, `__MODEL_ID__`) within the merged JSON are replaced with actual values. These values are first JSON-encoded and then trimmed of their outer quotes to ensure they are safely inserted into the JSON structure without breaking it.
|
||||||
|
|
||||||
|
### 8.4. File Storage Configuration
|
||||||
|
|
||||||
|
- The application uses Laravel's filesystem configuration.
|
||||||
|
- The `public` disk is configured in `config/filesystems.php` to point directly to `public_path('storage')`.
|
||||||
|
- Images uploaded via Filament (e.g., style previews) are stored in `public/storage/style_previews`.
|
||||||
|
- The `public/storage` directory is a symbolic link to `storage/app/public` (though the configuration was adjusted to point directly to `public/storage` for the `public` disk).
|
||||||
|
|
||||||
|
This detailed overview should provide a solid foundation for understanding and rebuilding the AI StyleGallery application.
|
||||||
46
Styles Flux kontext v1.md
Normal file
46
Styles Flux kontext v1.md
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
Transform the people in the image into a pastel drawing with gentle textures and soft hues.
|
||||||
|
Transform the people in the image into an abstract painting with vibrant swirls and emotional energy.
|
||||||
|
Transform the people in the image into pixel art characters with blocky shapes and a retro video game feel.
|
||||||
|
Transform the people in the image into low-poly 3D models with geometric surfaces and flat shading.
|
||||||
|
Transform the people in the image into chrome sculptures with reflective surfaces and metallic shine.
|
||||||
|
Transform the people in the image into blueprint-style figures with line work and measurement details.
|
||||||
|
Transform the people in the image into characters from a Studio Ghibli film with soft shading and magical warmth.
|
||||||
|
Transform the people in the image into a comic book scene with halftone shading and dynamic poses.
|
||||||
|
Transform the people in the image into characters from a 3D Pixar-style movie with expressive faces and big eyes.
|
||||||
|
Transform the people in the image into characters from The Simpsons with yellow skin and over-the-top expressions.
|
||||||
|
Transform the people in the image into LEGO-style figures with plastic texture and toy-like features.
|
||||||
|
Transform the people in the image into South Park-style cutout characters with flat shapes and goofy faces.
|
||||||
|
Transform the people in the image into medieval knights and nobles in a fantasy setting with ornate costumes. make the image bright
|
||||||
|
Transform the people in the image into elves and wizards from a magical forest with glowing runes
|
||||||
|
Transform the people in the image into steampunk characters with gears, goggles, and Victorian flair.
|
||||||
|
Transform the people in the image into ancient stone statues with cracks and moss growing on them
|
||||||
|
Transform the people in the image into a group of zombies with cartoony decay and goofy expressions. make the image bright.
|
||||||
|
Transform the people in the image into carved wooden figures with natural grain patterns, warm tones, and handmade imperfections.
|
||||||
|
Transform the people in the image into graffiti-style characters on a concrete wall, with spray paint textures, vibrant strokes, and urban flair.
|
||||||
|
Transform the people in the image into 3D-printed sculptures with plastic textures, visible print lines, and geometric precision.
|
||||||
|
Transform the people in the image into painted cardboard cutouts with flat surfaces, hand-drawn outlines, and theater-prop appearance.
|
||||||
|
Transform the people in the image into figures like on an ancient Greek vase, rendered in black-figure ceramic painting with elegant poses.
|
||||||
|
Transform the people in the image into medieval illuminated manuscript figures with gold leaf accents, stylized faces, and vibrant borders.
|
||||||
|
Transform the people in the image into Aztec stone carvings with geometric patterns, mythological motifs, and ancient symbolism.
|
||||||
|
Transform the people in the image into cracked porcelain dolls with aged glaze, fine fractures, and haunting fragility.
|
||||||
|
Transform the people in the image into cardboard paper dolls with tabbed joints, illustrated features, and moveable parts.
|
||||||
|
Transform the people in the image into stylized cut-paper collages, with torn edges, colorful flat textures, and assembled human shapes.
|
||||||
|
Transform the people in the image into 80s disco icons with glitter outfits, exaggerated hair, shiny dance floors, and neon lighting.
|
||||||
|
Transform the people in the image into soap opera villains mid-dramatic twist, with wind machines, sparkles, and over-the-top reactions.
|
||||||
|
Transform the people in the image into enchanted talking teapots, cups, and kitchen objects with faces and arms, ready to sing.
|
||||||
|
Transform the image into a sunny day in front of the Eiffel Tower in Paris, with the people posing like tourists, café tables nearby and pigeons flying around.
|
||||||
|
Transform the image into a night scene in Tokyo’s Shibuya Crossing, with neon lights, animated billboards, and a bustling futuristic city backdrop.
|
||||||
|
remove the background and Transform the image into a street performance in New York’s Times Square, with giant screens, yellow taxis, and amazed onlookers.
|
||||||
|
remove the background and Transform the image into a group photo in front of the Colosseum in Rome, with sunny weather, tourists with cameras, and ancient stone textures. Keep all the face details from the original
|
||||||
|
remove the background and Transform the image into a sunset beach party in Hawaii, with palm trees, ukuleles, tiki torches, and ocean waves.
|
||||||
|
remove the background and Transform the image into a hike through a misty rainforest with lush green foliage, vines, and curious animals peeking from the trees.
|
||||||
|
remove the background and Transform the image into a field of sunflowers stretching to the horizon, with bees buzzing and the people dressed in summer clothes.
|
||||||
|
remove the background and Transform the image into a magical floating island in the sky, with waterfalls cascading into the clouds and glowing plants surrounding the group.
|
||||||
|
remove the background and Transform the image into a dreamlike moonbase picnic with Earth visible above, moon buggies nearby, and silver spacesuits.
|
||||||
|
remove the background and Transform the image into a carnival at night, with colorful tents, cotton candy, blinking lights, and joyful movement.
|
||||||
|
remove the background and Transform the image into a retro 80s arcade hall, with glowing cabinets, pixelated screens, and classic synth lighting.
|
||||||
|
remove the background and Transform the image into a Moroccan market at sunset, with patterned rugs, spices in the air, and hanging lamps lighting the scene.
|
||||||
|
remove the background and Transform the image into a steampunk city plaza with brass towers, flying machines, and the group dressed in goggles and gears.
|
||||||
|
remove the background and Transform the image into a colorful Holi festival in India, with powder paint in the air and everyone covered in bright colors.
|
||||||
|
remove the background and Transform the image into a vintage movie set from the 1920s, with sepia tones, cameras on tracks, and dramatic film star poses.
|
||||||
|
insert into styles (title,prompt,description,preview_image,created_at,ai_model_id,enabled), VALUES(*titel*, *prompt*, *beschreibung*,'uploads/ComfyUI_*laufende Nummer mit 4 führenden Nullen*_.png, NOW(), 8, 1);
|
||||||
@@ -11,5 +11,8 @@ interface ApiPluginInterface
|
|||||||
public function disable(): bool;
|
public function disable(): bool;
|
||||||
public function getStatus(string $imageUUID): array;
|
public function getStatus(string $imageUUID): array;
|
||||||
public function getProgress(string $imageUUID): array;
|
public function getProgress(string $imageUUID): array;
|
||||||
public function processImageStyleChange(string $imagePath, string $prompt, string $modelId, ?string $parameters = null): array;
|
public function processImageStyleChange(\App\Models\Image $image, \App\Models\Style $style): array;
|
||||||
|
public function getStyledImage(string $promptId): string;
|
||||||
|
public function testConnection(array $data): bool;
|
||||||
|
public function searchModels(string $searchTerm): array;
|
||||||
}
|
}
|
||||||
@@ -65,30 +65,40 @@ class ComfyUi implements ApiPluginInterface
|
|||||||
|
|
||||||
public function getProgress(string $imageUUID): array
|
public function getProgress(string $imageUUID): array
|
||||||
{
|
{
|
||||||
$this->logDebug('Getting progress for image.', ['image_uuid' => $imageUUID]);
|
$this->logDebug('Progress updates are handled via WebSocket.', ['image_uuid' => $imageUUID]);
|
||||||
// Implement ComfyUI specific progress check
|
return ['progress' => 0]; // Progress is now handled by WebSocket
|
||||||
return ['progress' => 0];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function processImageStyleChange(string $imagePath, string $prompt, string $modelId, ?string $parameters = null): array
|
private function getHistory(string $promptId): array
|
||||||
{
|
{
|
||||||
$this->logInfo('Starting ComfyUI style change process.', ['image_path' => $imagePath]);
|
// This method is no longer used for progress polling, but might be used for final result retrieval
|
||||||
|
$apiUrl = rtrim($this->apiProvider->api_url, '/');
|
||||||
|
$timeout = 60; // seconds
|
||||||
|
$this->logDebug('ComfyUI History API URL:', ['url' => $apiUrl . '/history/' . $promptId, 'timeout' => $timeout]);
|
||||||
|
$response = Http::timeout($timeout)->get($apiUrl . '/history/' . $promptId);
|
||||||
|
if ($response->failed()) {
|
||||||
|
throw new \Exception('Failed to get history from ComfyUI');
|
||||||
|
}
|
||||||
|
return $response->json();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function processImageStyleChange(\App\Models\Image $image, \App\Models\Style $style): array
|
||||||
|
{
|
||||||
|
$this->logInfo('Starting ComfyUI style change process.', ['image_id' => $image->id, 'style_id' => $style->id]);
|
||||||
|
|
||||||
// 1. Upload image to ComfyUI
|
// 1. Upload image to ComfyUI
|
||||||
$uploadResponse = $this->uploadImage($imagePath);
|
$uploadResponse = $this->uploadImage(public_path('storage/' . $image->path));
|
||||||
$filename = $uploadResponse['name'];
|
$filename = $uploadResponse['name'];
|
||||||
|
|
||||||
// 2. Construct the prompt
|
// 2. Construct the prompt
|
||||||
$promptData = $this->constructPrompt($prompt, $filename, $modelId, $parameters);
|
$promptData = $this->constructPrompt($style, $filename);
|
||||||
|
|
||||||
// 3. Queue the prompt
|
// 3. Queue the prompt
|
||||||
$queueResponse = $this->queuePrompt($promptData);
|
$queueResponse = $this->queuePrompt($promptData);
|
||||||
$promptId = $queueResponse['prompt_id'];
|
$promptId = $queueResponse['prompt_id'];
|
||||||
|
|
||||||
// 4. Wait for and get the result
|
// Return the prompt_id for frontend WebSocket tracking
|
||||||
$result = $this->waitForResult($promptId);
|
return ['prompt_id' => $promptId];
|
||||||
|
|
||||||
return ['base64Data' => $result];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private function uploadImage(string $imagePath): array
|
private function uploadImage(string $imagePath): array
|
||||||
@@ -96,7 +106,7 @@ class ComfyUi implements ApiPluginInterface
|
|||||||
$this->logInfo('Uploading image to ComfyUI.', ['image_path' => $imagePath]);
|
$this->logInfo('Uploading image to ComfyUI.', ['image_path' => $imagePath]);
|
||||||
$response = Http::attach(
|
$response = Http::attach(
|
||||||
'image', file_get_contents($imagePath), basename($imagePath)
|
'image', file_get_contents($imagePath), basename($imagePath)
|
||||||
)->timeout(120)->post(rtrim($this->apiProvider->api_url, '/') . '/upload/image', [
|
)->timeout(60)->post(rtrim($this->apiProvider->api_url, '/') . '/upload/image', [
|
||||||
'type' => 'input',
|
'type' => 'input',
|
||||||
'overwrite' => 'false',
|
'overwrite' => 'false',
|
||||||
]);
|
]);
|
||||||
@@ -109,24 +119,60 @@ class ComfyUi implements ApiPluginInterface
|
|||||||
return $response->json();
|
return $response->json();
|
||||||
}
|
}
|
||||||
|
|
||||||
private function constructPrompt(string $prompt, string $filename, string $modelId, ?string $parameters): array
|
private function constructPrompt(\App\Models\Style $style, string $filename): array
|
||||||
{
|
{
|
||||||
if (empty($parameters)) {
|
$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.');
|
throw new \Exception('ComfyUI workflow (parameters) is missing.');
|
||||||
}
|
}
|
||||||
|
|
||||||
$workflow = $parameters;
|
// Use array_replace_recursive for a deep merge
|
||||||
$workflow = str_replace('__PROMPT__', $prompt, $workflow);
|
$mergedParams = array_replace_recursive($modelParams, $styleParams);
|
||||||
$workflow = str_replace('__FILENAME__', $filename, $workflow);
|
$workflow = json_encode($mergedParams);
|
||||||
$workflow = str_replace('__MODEL_ID__', $modelId, $workflow);
|
|
||||||
|
|
||||||
return json_decode($workflow, true);
|
// Properly escape the values for JSON injection
|
||||||
|
$prompt = substr(json_encode($style->prompt, JSON_UNESCAPED_SLASHES), 1, -1);
|
||||||
|
$filename_escaped = substr(json_encode($filename, JSON_UNESCAPED_SLASHES), 1, -1);
|
||||||
|
$modelId_escaped = substr(json_encode($style->aiModel->model_id, JSON_UNESCAPED_SLASHES), 1, -1);
|
||||||
|
|
||||||
|
$workflow = str_replace('__PROMPT__', $prompt, $workflow);
|
||||||
|
$workflow = str_replace('__FILENAME__', $filename_escaped, $workflow);
|
||||||
|
$workflow = str_replace('__MODEL_ID__', $modelId_escaped, $workflow);
|
||||||
|
|
||||||
|
$decodedWorkflow = json_decode($workflow, true);
|
||||||
|
|
||||||
|
if (json_last_error() !== JSON_ERROR_NONE) {
|
||||||
|
$this->logError('Failed to decode workflow JSON after placeholder replacement.', [
|
||||||
|
'json_error' => json_last_error_msg(),
|
||||||
|
'workflow_string' => $workflow
|
||||||
|
]);
|
||||||
|
throw new \Exception('Failed to construct valid ComfyUI workflow JSON: ' . json_last_error_msg());
|
||||||
|
}
|
||||||
|
|
||||||
|
return $decodedWorkflow;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function queuePrompt(array $promptData): array
|
private function queuePrompt(array $promptData): array
|
||||||
{
|
{
|
||||||
$this->logInfo('Queueing prompt in ComfyUI.');
|
$this->logInfo('Queueing prompt in ComfyUI.');
|
||||||
$response = Http::timeout(120)->post(rtrim($this->apiProvider->api_url, '/') . '/prompt', ['prompt' => $promptData]);
|
$response = Http::timeout(60)->post(rtrim($this->apiProvider->api_url, '/') . '/prompt', ['prompt' => $promptData]);
|
||||||
|
|
||||||
if ($response->failed()) {
|
if ($response->failed()) {
|
||||||
$this->logError('Failed to queue prompt in ComfyUI.', ['response' => $response->body()]);
|
$this->logError('Failed to queue prompt in ComfyUI.', ['response' => $response->body()]);
|
||||||
@@ -136,30 +182,87 @@ class ComfyUi implements ApiPluginInterface
|
|||||||
return $response->json();
|
return $response->json();
|
||||||
}
|
}
|
||||||
|
|
||||||
private function waitForResult(string $promptId): string
|
public function waitForResult(string $promptId): string
|
||||||
{
|
{
|
||||||
$this->logInfo('Waiting for ComfyUI result.', ['prompt_id' => $promptId]);
|
set_time_limit(120); // Set maximum execution time for this function
|
||||||
while (true) {
|
$this->logInfo('waitForResult: Waiting for ComfyUI result.', ['prompt_id' => $promptId]);
|
||||||
$response = Http::timeout(120)->get($this->apiProvider->api_url . '/history/' . $promptId);
|
$startTime = microtime(true);
|
||||||
$data = $response->json();
|
$timeout = 180; // seconds
|
||||||
|
|
||||||
if (!empty($data[$promptId]['outputs'])) {
|
while (true) {
|
||||||
$outputs = $data[$promptId]['outputs'];
|
if (microtime(true) - $startTime > $timeout) {
|
||||||
// Assuming the first output with an image is the one we want
|
$this->logError('waitForResult: ComfyUI result polling timed out.', ['prompt_id' => $promptId]);
|
||||||
foreach ($outputs as $output) {
|
throw new \Exception('ComfyUI result polling timed out.');
|
||||||
if (isset($output['images'][0]['type']) && $output['images'][0]['type'] === 'output') {
|
|
||||||
$imageUrl = sprintf('%s/view?filename=%s&subfolder=%s&type=output',
|
|
||||||
$this->apiProvider->api_url,
|
|
||||||
$output['images'][0]['filename'],
|
|
||||||
$output['images'][0]['subfolder']
|
|
||||||
);
|
|
||||||
$image_data = file_get_contents($imageUrl);
|
|
||||||
return base64_encode($image_data);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
sleep(2); // Wait for 2 seconds before polling again
|
try {
|
||||||
|
$response = Http::timeout(60)->get(rtrim($this->apiProvider->api_url, '/') . '/history/' . $promptId);
|
||||||
|
$this->logDebug('waitForResult: History API response status.', ['status' => $response->status(), 'prompt_id' => $promptId]);
|
||||||
|
|
||||||
|
if ($response->failed()) {
|
||||||
|
$this->logError('waitForResult: Failed to get history from ComfyUI.', ['prompt_id' => $promptId, 'response' => $response->body()]);
|
||||||
|
throw new \Exception('Failed to get history from ComfyUI');
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = $response->json();
|
||||||
|
$this->logDebug('waitForResult: History API response data.', ['data' => $data, 'prompt_id' => $promptId]);
|
||||||
|
|
||||||
|
if (isset($data[$promptId]['outputs'])) {
|
||||||
|
$outputs = $data[$promptId]['outputs'];
|
||||||
|
$this->logInfo('waitForResult: Found outputs in history.', ['prompt_id' => $promptId, 'outputs_count' => count($outputs)]);
|
||||||
|
|
||||||
|
foreach ($outputs as $output) {
|
||||||
|
if (isset($output['images'][0]['type']) && $output['images'][0]['type'] === 'output') {
|
||||||
|
$imageUrl = sprintf('%s/view?filename=%s&subfolder=%s&type=output',
|
||||||
|
rtrim($this->apiProvider->api_url, '/'),
|
||||||
|
$output['images'][0]['filename'],
|
||||||
|
$output['images'][0]['subfolder']
|
||||||
|
);
|
||||||
|
$this->logInfo('waitForResult: Constructed image URL.', ['imageUrl' => $imageUrl, 'prompt_id' => $promptId]);
|
||||||
|
|
||||||
|
$imageResponse = Http::timeout(60)->get($imageUrl);
|
||||||
|
$this->logDebug('waitForResult: Image fetch response status.', ['status' => $imageResponse->status(), 'imageUrl' => $imageUrl]);
|
||||||
|
|
||||||
|
if ($imageResponse->failed()) {
|
||||||
|
$this->logError('waitForResult: Failed to retrieve image data from ComfyUI.', ['imageUrl' => $imageUrl, 'response' => $imageResponse->body()]);
|
||||||
|
throw new \Exception('Failed to retrieve image data from ComfyUI');
|
||||||
|
}
|
||||||
|
$this->logInfo('waitForResult: Successfully retrieved image data.', ['prompt_id' => $promptId]);
|
||||||
|
return base64_encode($imageResponse->body());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$this->logDebug('waitForResult: No outputs found yet for prompt.', ['prompt_id' => $promptId]);
|
||||||
|
}
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$this->logError('waitForResult: Exception caught during polling.', ['prompt_id' => $promptId, 'error' => $e->getMessage(), 'trace' => $e->getTraceAsString()]);
|
||||||
|
throw $e; // Re-throw the exception to be caught by ImageController
|
||||||
|
}
|
||||||
|
|
||||||
|
usleep(500000); // Wait for 0.5 seconds before polling again
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
public function testConnection(array $data): bool
|
||||||
|
{
|
||||||
|
$apiUrl = rtrim($data['api_url'], '/');
|
||||||
|
try {
|
||||||
|
$response = Http::timeout(5)->get($apiUrl . '/queue');
|
||||||
|
return $response->successful();
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$this->logError('ComfyUI connection test failed.', ['error' => $e->getMessage()]);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function searchModels(string $searchTerm): array
|
||||||
|
{
|
||||||
|
$this->logInfo('ComfyUI does not support model search. Returning empty list.', ['searchTerm' => $searchTerm]);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getStyledImage(string $promptId): string
|
||||||
|
{
|
||||||
|
return $this->waitForResult($promptId);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -56,6 +56,11 @@ class RunwareAi implements ApiPluginInterface
|
|||||||
}
|
}
|
||||||
return $result;
|
return $result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getStyledImage(string $promptId): string
|
||||||
|
{
|
||||||
|
throw new \Exception('RunwareAi does not support fetching styled images by prompt ID.');
|
||||||
|
}
|
||||||
|
|
||||||
public function getStatus(string $imageUUID): array
|
public function getStatus(string $imageUUID): array
|
||||||
{
|
{
|
||||||
@@ -71,10 +76,10 @@ class RunwareAi implements ApiPluginInterface
|
|||||||
return ['progress' => 0];
|
return ['progress' => 0];
|
||||||
}
|
}
|
||||||
|
|
||||||
public function processImageStyleChange(string $imagePath, string $prompt, string $modelId, ?string $parameters = null): array
|
public function processImageStyleChange(\App\Models\Image $image, \App\Models\Style $style): array
|
||||||
{
|
{
|
||||||
// Step 1: Upload the original image
|
// Step 1: Upload the original image
|
||||||
$uploadResult = $this->upload($imagePath);
|
$uploadResult = $this->upload(public_path('storage/' . $image->path));
|
||||||
|
|
||||||
if (!isset($uploadResult['data'][0]['imageUUID'])) {
|
if (!isset($uploadResult['data'][0]['imageUUID'])) {
|
||||||
throw new \Exception('Image upload to AI service failed or returned no UUID.');
|
throw new \Exception('Image upload to AI service failed or returned no UUID.');
|
||||||
@@ -82,7 +87,7 @@ class RunwareAi implements ApiPluginInterface
|
|||||||
$seedImageUUID = $uploadResult['data'][0]['imageUUID'];
|
$seedImageUUID = $uploadResult['data'][0]['imageUUID'];
|
||||||
|
|
||||||
// Step 2: Request style change using the uploaded image's UUID
|
// Step 2: Request style change using the uploaded image's UUID
|
||||||
$result = $this->styleChangeRequest($prompt, $seedImageUUID, $modelId, $parameters);
|
$result = $this->styleChangeRequest($style, $seedImageUUID);
|
||||||
|
|
||||||
if (!isset($result['base64Data'])) {
|
if (!isset($result['base64Data'])) {
|
||||||
throw new \Exception('AI service did not return base64 image data.');
|
throw new \Exception('AI service did not return base64 image data.');
|
||||||
@@ -91,6 +96,105 @@ class RunwareAi implements ApiPluginInterface
|
|||||||
return $result;
|
return $result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testConnection(array $data): bool
|
||||||
|
{
|
||||||
|
$apiUrl = rtrim($data['api_url'], '/');
|
||||||
|
$token = $data['token'] ?? null;
|
||||||
|
|
||||||
|
if (!$apiUrl || !$token) {
|
||||||
|
$this->logError('RunwareAI connection test failed: API URL or Token missing.');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$response = Http::withHeaders([
|
||||||
|
'Content-Type' => 'application/json',
|
||||||
|
'Accept' => 'application/json',
|
||||||
|
])->timeout(5)->post($apiUrl, [
|
||||||
|
'taskType' => 'authentication',
|
||||||
|
'apiKey' => $token,
|
||||||
|
'taskUUID' => (string) Str::uuid(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$responseData = $response->json();
|
||||||
|
|
||||||
|
if ($response->successful() && isset($responseData['data']) && !isset($responseData['error'])) {
|
||||||
|
$this->logInfo('RunwareAI connection test successful: Authentication successful.', [
|
||||||
|
'status' => $response->status(),
|
||||||
|
'response' => $responseData,
|
||||||
|
]);
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
$errorMessage = $responseData['error'] ?? 'Unknown error';
|
||||||
|
$this->logError('RunwareAI connection test failed: Authentication failed.', [
|
||||||
|
'status' => $response->status(),
|
||||||
|
'response' => $responseData,
|
||||||
|
'error_message' => $errorMessage,
|
||||||
|
]);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$this->logError('RunwareAI connection test failed: Exception caught.', ['error' => $e->getMessage()]);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public function searchModels(string $searchTerm): array
|
||||||
|
{
|
||||||
|
$this->logInfo('Attempting model search on RunwareAI.', ['searchTerm' => $searchTerm]);
|
||||||
|
if (!$this->apiProvider->api_url || !$this->apiProvider->token) {
|
||||||
|
$this->logError('RunwareAI API URL or Token not configured for model search.', ['provider_name' => $this->apiProvider->name]);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$apiUrl = rtrim($this->apiProvider->api_url, '/');
|
||||||
|
$token = $this->apiProvider->token;
|
||||||
|
|
||||||
|
try {
|
||||||
|
$response = Http::withHeaders([
|
||||||
|
'Authorization' => 'Bearer ' . $token,
|
||||||
|
'Accept' => 'application/json',
|
||||||
|
'Content-Type' => 'application/json',
|
||||||
|
])->timeout(10)->post($apiUrl, [
|
||||||
|
[
|
||||||
|
'taskType' => 'modelSearch',
|
||||||
|
'search' => $searchTerm,
|
||||||
|
'type' => 'base',
|
||||||
|
'category' => 'checkpoint',
|
||||||
|
'limit' => 100,
|
||||||
|
'taskUUID' => (string) Str::uuid(),
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
|
||||||
|
$responseData = $response->json();
|
||||||
|
|
||||||
|
if ($response->successful() && isset($responseData['data'][0]['results']) && !isset($responseData['error'])) {
|
||||||
|
$models = [];
|
||||||
|
foreach ($responseData['data'][0]['results'] as $model) {
|
||||||
|
$models[] = [
|
||||||
|
'name' => $model['name'] ?? 'Unknown',
|
||||||
|
'id' => $model['air'] ?? 'Unknown',
|
||||||
|
'type' => $model['type'] ?? null,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
$this->logInfo('Model search successful on RunwareAI.', ['searchTerm' => $searchTerm, 'modelsFound' => count($models)]);
|
||||||
|
return $models;
|
||||||
|
} else {
|
||||||
|
$errorMessage = $responseData['error'] ?? 'Unknown error';
|
||||||
|
$this->logError('Model search failed on RunwareAI: Unsuccessful response.', [
|
||||||
|
'status' => $response->status(),
|
||||||
|
'response' => $responseData,
|
||||||
|
'error_message' => $errorMessage,
|
||||||
|
]);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$this->logError('Model search failed on RunwareAI: Exception caught.', ['error' => $e->getMessage()]);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private function upload(string $imagePath): array
|
private function upload(string $imagePath): array
|
||||||
{
|
{
|
||||||
$this->logInfo('Attempting to upload image to RunwareAI.', ['image_path' => $imagePath]);
|
$this->logInfo('Attempting to upload image to RunwareAI.', ['image_path' => $imagePath]);
|
||||||
@@ -127,9 +231,9 @@ class RunwareAi implements ApiPluginInterface
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private function styleChangeRequest(string $prompt, string $seedImageUUID, string $modelId, ?string $parameters = null): array
|
private function styleChangeRequest(\App\Models\Style $style, string $seedImageUUID): array
|
||||||
{
|
{
|
||||||
$this->logInfo('Attempting style change request to RunwareAI.', ['prompt' => $prompt, 'seed_image_uuid' => $seedImageUUID]);
|
$this->logInfo('Attempting style change request to RunwareAI.', ['style_id' => $style->id, 'seed_image_uuid' => $seedImageUUID]);
|
||||||
if (!$this->apiProvider->api_url || !$this->apiProvider->token) {
|
if (!$this->apiProvider->api_url || !$this->apiProvider->token) {
|
||||||
$this->logError('RunwareAI API URL or Token not configured for style change.', ['provider_name' => $this->apiProvider->name]);
|
$this->logError('RunwareAI API URL or Token not configured for style change.', ['provider_name' => $this->apiProvider->name]);
|
||||||
throw new \Exception('RunwareAI API URL or Token not configured.');
|
throw new \Exception('RunwareAI API URL or Token not configured.');
|
||||||
@@ -139,17 +243,20 @@ class RunwareAi implements ApiPluginInterface
|
|||||||
$token = $this->apiProvider->token;
|
$token = $this->apiProvider->token;
|
||||||
$taskUUID = (string) Str::uuid();
|
$taskUUID = (string) Str::uuid();
|
||||||
|
|
||||||
|
$modelParams = $style->aiModel->parameters ?? [];
|
||||||
|
$styleParams = $style->parameters ?? [];
|
||||||
|
$mergedParams = array_replace_recursive($modelParams, $styleParams);
|
||||||
|
|
||||||
$data = [
|
$data = [
|
||||||
'taskType' => 'imageInference',
|
'taskType' => 'imageInference',
|
||||||
'taskUUID' => $taskUUID,
|
'taskUUID' => $taskUUID,
|
||||||
'positivePrompt' => $prompt,
|
'positivePrompt' => $style->prompt,
|
||||||
'seedImage' => $seedImageUUID,
|
'seedImage' => $seedImageUUID,
|
||||||
'outputType' => 'base64Data',
|
'outputType' => 'base64Data',
|
||||||
'model' => $modelId,
|
'model' => $style->aiModel->model_id
|
||||||
];
|
];
|
||||||
|
|
||||||
$decodedParameters = json_decode($parameters, true) ?? [];
|
foreach ($mergedParams as $key => $value) {
|
||||||
foreach ($decodedParameters as $key => $value) {
|
|
||||||
$data[$key] = $value;
|
$data[$key] = $value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
44
app/Console/Commands/GetPrinterSetting.php
Normal file
44
app/Console/Commands/GetPrinterSetting.php
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Console\Commands;
|
||||||
|
|
||||||
|
use App\Settings\GeneralSettings;
|
||||||
|
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(GeneralSettings $settings)
|
||||||
|
{
|
||||||
|
$value = $settings->selected_printer === '__custom__'
|
||||||
|
? $settings->custom_printer_address
|
||||||
|
: $settings->selected_printer;
|
||||||
|
|
||||||
|
if ($value) {
|
||||||
|
$this->info('Raw value of \'selected_printer\' setting:');
|
||||||
|
$this->info($value);
|
||||||
|
|
||||||
|
return Command::SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->info('\'selected_printer\' setting not found.');
|
||||||
|
|
||||||
|
return Command::FAILURE;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Console;
|
|
||||||
|
|
||||||
use Illuminate\Console\Scheduling\Schedule;
|
|
||||||
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
|
|
||||||
|
|
||||||
class Kernel extends ConsoleKernel
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Define the application's command schedule.
|
|
||||||
*/
|
|
||||||
protected function schedule(Schedule $schedule): void
|
|
||||||
{
|
|
||||||
// $schedule->command('inspire')->hourly();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Register the commands for the application.
|
|
||||||
*/
|
|
||||||
protected function commands(): void
|
|
||||||
{
|
|
||||||
$this->load(__DIR__.'/Commands');
|
|
||||||
|
|
||||||
require base_path('routes/console.php');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
102
app/Filament/Pages/GlobalSettings.php
Normal file
102
app/Filament/Pages/GlobalSettings.php
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Filament\Pages;
|
||||||
|
|
||||||
|
use App\Services\PrinterService;
|
||||||
|
use App\Settings\GeneralSettings;
|
||||||
|
use BackedEnum;
|
||||||
|
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\Notifications\Notification;
|
||||||
|
use Filament\Pages\Page;
|
||||||
|
use Filament\Schemas\Schema;
|
||||||
|
use Illuminate\Support\Arr;
|
||||||
|
use UnitEnum;
|
||||||
|
|
||||||
|
class GlobalSettings extends Page implements HasForms
|
||||||
|
{
|
||||||
|
use InteractsWithForms;
|
||||||
|
|
||||||
|
protected static string|BackedEnum|null $navigationIcon = 'heroicon-o-cog';
|
||||||
|
|
||||||
|
protected string $view = 'filament.pages.global-settings';
|
||||||
|
|
||||||
|
protected static string|UnitEnum|null $navigationGroup = 'Admin';
|
||||||
|
|
||||||
|
public ?array $data = [];
|
||||||
|
|
||||||
|
public function mount(GeneralSettings $settings): void
|
||||||
|
{
|
||||||
|
$this->form->fill($settings->toArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function form(Schema $schema): Schema
|
||||||
|
{
|
||||||
|
$printerService = new PrinterService;
|
||||||
|
$printers = $printerService->getPrinters();
|
||||||
|
$printerOptions = array_merge($printers, ['__custom__' => __('filament.resource.setting.form.custom_printer')]);
|
||||||
|
|
||||||
|
return $schema
|
||||||
|
->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(GeneralSettings $settings): void
|
||||||
|
{
|
||||||
|
$data = $this->form->getState();
|
||||||
|
|
||||||
|
if (Arr::get($data, 'selected_printer') !== '__custom__') {
|
||||||
|
$data['custom_printer_address'] = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$data['new_image_timespan_minutes'] = (int) Arr::get($data, 'new_image_timespan_minutes', 0);
|
||||||
|
$data['image_refresh_interval'] = (int) Arr::get($data, 'image_refresh_interval', 0);
|
||||||
|
$data['max_number_of_copies'] = (int) Arr::get($data, 'max_number_of_copies', 0);
|
||||||
|
$data['show_print_button'] = (bool) Arr::get($data, 'show_print_button', false);
|
||||||
|
$data['custom_printer_address'] = $data['custom_printer_address'] ?: null;
|
||||||
|
|
||||||
|
$settings->fill($data)->save();
|
||||||
|
|
||||||
|
Notification::make()
|
||||||
|
->title(__('settings.saved_successfully'))
|
||||||
|
->success()
|
||||||
|
->send();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getFormActions(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
\Filament\Actions\Action::make('save')
|
||||||
|
->label(__('settings.save_button'))
|
||||||
|
->submit('save'),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,23 +2,25 @@
|
|||||||
|
|
||||||
namespace App\Filament\Pages;
|
namespace App\Filament\Pages;
|
||||||
|
|
||||||
|
use BackedEnum;
|
||||||
use Filament\Forms\Components\FileUpload;
|
use Filament\Forms\Components\FileUpload;
|
||||||
use Filament\Forms\Concerns\InteractsWithForms;
|
use Filament\Forms\Concerns\InteractsWithForms;
|
||||||
use Filament\Forms\Contracts\HasForms;
|
use Filament\Forms\Contracts\HasForms;
|
||||||
use Filament\Forms\Form;
|
|
||||||
use Filament\Notifications\Notification;
|
use Filament\Notifications\Notification;
|
||||||
use Filament\Pages\Page;
|
use Filament\Pages\Page;
|
||||||
|
use Filament\Schemas\Schema;
|
||||||
use Illuminate\Support\Facades\File;
|
use Illuminate\Support\Facades\File;
|
||||||
|
use UnitEnum;
|
||||||
|
|
||||||
class InstallPluginPage extends Page implements HasForms
|
class InstallPluginPage extends Page implements HasForms
|
||||||
{
|
{
|
||||||
use InteractsWithForms;
|
use InteractsWithForms;
|
||||||
|
|
||||||
protected static ?string $navigationIcon = 'heroicon-o-cloud-arrow-up';
|
protected static string|BackedEnum|null $navigationIcon = 'heroicon-o-cloud-arrow-up';
|
||||||
|
|
||||||
protected static string $view = 'filament.pages.install-plugin-page';
|
protected string $view = 'filament.pages.install-plugin-page';
|
||||||
|
|
||||||
protected static ?string $navigationGroup = 'Plugins';
|
protected static string|UnitEnum|null $navigationGroup = 'Plugins';
|
||||||
|
|
||||||
protected static ?string $title = 'Install Plugin';
|
protected static ?string $title = 'Install Plugin';
|
||||||
|
|
||||||
@@ -29,9 +31,9 @@ class InstallPluginPage extends Page implements HasForms
|
|||||||
$this->form->fill();
|
$this->form->fill();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function form(Form $form): Form
|
public function form(Schema $schema): Schema
|
||||||
{
|
{
|
||||||
return $form
|
return $schema
|
||||||
->schema([
|
->schema([
|
||||||
FileUpload::make('plugin_file')
|
FileUpload::make('plugin_file')
|
||||||
->label('Plugin File (.php)')
|
->label('Plugin File (.php)')
|
||||||
@@ -43,16 +45,26 @@ class InstallPluginPage extends Page implements HasForms
|
|||||||
->statePath('data');
|
->statePath('data');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static function getNavigationGroup(): ?string
|
||||||
|
{
|
||||||
|
return __('filament.navigation.groups.plugins');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getTitle(): string
|
||||||
|
{
|
||||||
|
return __('filament.navigation.install_plugin');
|
||||||
|
}
|
||||||
|
|
||||||
public function submit(): void
|
public function submit(): void
|
||||||
{
|
{
|
||||||
$data = $this->form->getState();
|
$data = $this->form->getState();
|
||||||
|
|
||||||
$uploadedFile = $data['plugin_file'];
|
$uploadedFile = $data['plugin_file'];
|
||||||
$filename = File::basename($uploadedFile);
|
$filename = File::basename($uploadedFile);
|
||||||
$destinationPath = app_path('Api/Plugins/' . $filename);
|
$destinationPath = app_path('Api/Plugins/'.$filename);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
File::move(storage_path('app/temp_plugins/' . $filename), $destinationPath);
|
File::move(storage_path('app/temp_plugins/'.$filename), $destinationPath);
|
||||||
|
|
||||||
Notification::make()
|
Notification::make()
|
||||||
->title('Plugin installed successfully')
|
->title('Plugin installed successfully')
|
||||||
|
|||||||
@@ -1,133 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Filament\Pages;
|
|
||||||
|
|
||||||
use App\Api\Plugins\ApiPluginInterface;
|
|
||||||
use App\Filament\Resources\PluginResource\Plugin;
|
|
||||||
use Filament\Notifications\Notification;
|
|
||||||
use Filament\Pages\Page;
|
|
||||||
use Filament\Tables\Columns\IconColumn;
|
|
||||||
use Filament\Tables\Columns\TextColumn;
|
|
||||||
use Filament\Tables\Concerns\InteractsWithTable;
|
|
||||||
use Filament\Tables\Contracts\HasTable;
|
|
||||||
use Filament\Tables\Table;
|
|
||||||
use Illuminate\Database\Eloquent\Collection;
|
|
||||||
use Illuminate\Support\Facades\File;
|
|
||||||
use App\Models\ApiProvider;
|
|
||||||
use Filament\Tables\Actions\Action;
|
|
||||||
|
|
||||||
class ListPlugins extends Page implements HasTable
|
|
||||||
{
|
|
||||||
use InteractsWithTable;
|
|
||||||
|
|
||||||
protected static ?string $navigationIcon = 'heroicon-o-puzzle-piece';
|
|
||||||
|
|
||||||
protected static string $view = 'filament.pages.list-plugins';
|
|
||||||
|
|
||||||
protected static ?string $navigationGroup = 'Plugins';
|
|
||||||
|
|
||||||
protected static ?string $title = 'Plugins';
|
|
||||||
|
|
||||||
public function table(Table $table): Table
|
|
||||||
{
|
|
||||||
$plugins = new Collection();
|
|
||||||
$path = app_path('Api/Plugins');
|
|
||||||
|
|
||||||
if (File::exists($path)) {
|
|
||||||
$files = File::files($path);
|
|
||||||
foreach ($files as $file) {
|
|
||||||
$filename = $file->getFilenameWithoutExtension();
|
|
||||||
$class = 'App\\Api\\Plugins\\' . $filename;
|
|
||||||
|
|
||||||
if (class_exists($class) && in_array(ApiPluginInterface::class, class_implements($class))) {
|
|
||||||
try {
|
|
||||||
$apiProvider = ApiProvider::where('plugin', $filename)->first();
|
|
||||||
if(!$apiProvider) continue;
|
|
||||||
$instance = new $class($apiProvider);
|
|
||||||
$plugins->add(new Plugin([
|
|
||||||
'id' => $instance->getIdentifier(),
|
|
||||||
'name' => $instance->getName(),
|
|
||||||
'identifier' => $instance->getIdentifier(),
|
|
||||||
'enabled' => $instance->isEnabled(),
|
|
||||||
'file_path' => $file->getPathname(),
|
|
||||||
]));
|
|
||||||
} catch (\Exception $e) {
|
|
||||||
// Log error or handle gracefully if a plugin cannot be instantiated
|
|
||||||
// For now, we'll just skip it
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return $table
|
|
||||||
->query(Plugin::query()->setCollection($plugins))
|
|
||||||
->columns([
|
|
||||||
TextColumn::make('name')
|
|
||||||
->label('Name')
|
|
||||||
->searchable()
|
|
||||||
->sortable(),
|
|
||||||
TextColumn::make('identifier')
|
|
||||||
->label('Identifier'),
|
|
||||||
IconColumn::make('enabled')
|
|
||||||
->label('Enabled')
|
|
||||||
->boolean(),
|
|
||||||
TextColumn::make('file_path')
|
|
||||||
->label('File Path')
|
|
||||||
->toggleable(isToggledHiddenByDefault: true),
|
|
||||||
])
|
|
||||||
->actions([
|
|
||||||
Action::make('toggle_enabled')
|
|
||||||
->label(fn ($record) => $record->enabled ? 'Disable' : 'Enable')
|
|
||||||
->icon(fn ($record) => $record->enabled ? 'heroicon-o-x-circle' : 'heroicon-o-check-circle')
|
|
||||||
->action(function ($record) {
|
|
||||||
try {
|
|
||||||
$apiProvider = ApiProvider::where('plugin', $record->identifier)->first();
|
|
||||||
if(!$apiProvider) throw new \Exception('ApiProvider not found');
|
|
||||||
$pluginClass = 'App\\Api\\Plugins\\' . $record->identifier;
|
|
||||||
$plugin = new $pluginClass($apiProvider);
|
|
||||||
if ($record->enabled) {
|
|
||||||
$plugin->disable();
|
|
||||||
} else {
|
|
||||||
$plugin->enable();
|
|
||||||
}
|
|
||||||
Notification::make()
|
|
||||||
->title('Plugin status updated')
|
|
||||||
->success()
|
|
||||||
->send();
|
|
||||||
} catch (\Exception $e) {
|
|
||||||
Notification::make()
|
|
||||||
->title('Error updating plugin status')
|
|
||||||
->body($e->getMessage())
|
|
||||||
->danger()
|
|
||||||
->send();
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
|
|
||||||
Action::make('delete')
|
|
||||||
->label('Delete')
|
|
||||||
->icon('heroicon-o-trash')
|
|
||||||
->color('danger')
|
|
||||||
->requiresConfirmation()
|
|
||||||
->action(function ($record) {
|
|
||||||
try {
|
|
||||||
File::delete($record->file_path);
|
|
||||||
Notification::make()
|
|
||||||
->title('Plugin deleted successfully')
|
|
||||||
->success()
|
|
||||||
->send();
|
|
||||||
} catch (\Exception $e) {
|
|
||||||
Notification::make()
|
|
||||||
->title('Error deleting plugin')
|
|
||||||
->body($e->getMessage())
|
|
||||||
->danger()
|
|
||||||
->send();
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
])
|
|
||||||
->bulkActions([
|
|
||||||
// No bulk actions for now
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Removed getTableRecords() as data is now provided via query()
|
|
||||||
}
|
|
||||||
95
app/Filament/Pages/Plugin.php
Normal file
95
app/Filament/Pages/Plugin.php
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Filament\Pages;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use App\Api\Plugins\ApiPluginInterface;
|
||||||
|
use App\Models\ApiProvider;
|
||||||
|
use Illuminate\Support\Facades\File;
|
||||||
|
use Exception;
|
||||||
|
use ReflectionClass;
|
||||||
|
|
||||||
|
class Plugin extends Model
|
||||||
|
{
|
||||||
|
protected $table = null; // No actual table
|
||||||
|
|
||||||
|
protected $guarded = []; // Allow mass assignment for all attributes
|
||||||
|
|
||||||
|
public $incrementing = false;
|
||||||
|
|
||||||
|
protected $keyType = 'string';
|
||||||
|
|
||||||
|
protected $primaryKey = 'id';
|
||||||
|
|
||||||
|
public function __construct(array $attributes = [])
|
||||||
|
{
|
||||||
|
parent::__construct($attributes);
|
||||||
|
foreach ($attributes as $key => $value) {
|
||||||
|
$this->$key = $value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getAllPlugins()
|
||||||
|
{
|
||||||
|
$plugins = [];
|
||||||
|
$path = app_path('Api/Plugins');
|
||||||
|
|
||||||
|
if (File::exists($path)) {
|
||||||
|
$files = File::files($path);
|
||||||
|
foreach ($files as $file) {
|
||||||
|
$filename = $file->getFilenameWithoutExtension();
|
||||||
|
if (in_array($filename, ['ApiPluginInterface', 'LoggablePlugin', 'PluginLoader'])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$class = 'App\Api\Plugins\\' . $filename;
|
||||||
|
|
||||||
|
if (class_exists($class) && in_array(ApiPluginInterface::class, class_implements($class))) {
|
||||||
|
try {
|
||||||
|
// Check if there's an ApiProvider for this plugin
|
||||||
|
$apiProvider = ApiProvider::where('plugin', $filename)->first();
|
||||||
|
$hasApiProvider = $apiProvider !== null;
|
||||||
|
|
||||||
|
// Get plugin information without instantiating the class
|
||||||
|
// This avoids issues with plugins requiring ApiProvider in constructor
|
||||||
|
$reflection = new ReflectionClass($class);
|
||||||
|
$instance = $reflection->newInstanceWithoutConstructor();
|
||||||
|
|
||||||
|
$plugins[] = new self([
|
||||||
|
'id' => $instance->getIdentifier(),
|
||||||
|
'name' => $instance->getName(),
|
||||||
|
'identifier' => $instance->getIdentifier(),
|
||||||
|
'enabled' => $hasApiProvider && $apiProvider->enabled,
|
||||||
|
'file_path' => $file->getPathname(),
|
||||||
|
'has_api_provider' => $hasApiProvider,
|
||||||
|
'configured' => $hasApiProvider
|
||||||
|
]);
|
||||||
|
} catch (Exception $e) {
|
||||||
|
// Log error or handle as needed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $plugins;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function newCollection(array $models = [])
|
||||||
|
{
|
||||||
|
return new \Illuminate\Database\Eloquent\Collection($models);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function newQuery()
|
||||||
|
{
|
||||||
|
// Create a new query builder instance
|
||||||
|
$query = new \Illuminate\Database\Eloquent\Builder(
|
||||||
|
new \Illuminate\Database\Query\Builder(
|
||||||
|
\Illuminate\Support\Facades\DB::connection()->getQueryGrammar(),
|
||||||
|
\Illuminate\Support\Facades\DB::connection()->getPostProcessor()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Set the model for the query builder
|
||||||
|
$query->setModel($this);
|
||||||
|
|
||||||
|
return $query;
|
||||||
|
}
|
||||||
|
}
|
||||||
64
app/Filament/Pages/Plugins.php
Normal file
64
app/Filament/Pages/Plugins.php
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Filament\Pages;
|
||||||
|
|
||||||
|
use App\Models\Plugin;
|
||||||
|
use BackedEnum;
|
||||||
|
use Filament\Pages\Page;
|
||||||
|
use Filament\Tables;
|
||||||
|
use Filament\Tables\Columns\IconColumn;
|
||||||
|
use Filament\Tables\Columns\TextColumn;
|
||||||
|
use Filament\Tables\Table;
|
||||||
|
use UnitEnum;
|
||||||
|
|
||||||
|
class Plugins extends Page implements Tables\Contracts\HasTable
|
||||||
|
{
|
||||||
|
use Tables\Concerns\InteractsWithTable;
|
||||||
|
|
||||||
|
protected static string|BackedEnum|null $navigationIcon = 'heroicon-o-puzzle-piece';
|
||||||
|
|
||||||
|
protected static string|UnitEnum|null $navigationGroup = 'Plugins';
|
||||||
|
|
||||||
|
protected static ?string $navigationLabel = 'Plugin List';
|
||||||
|
|
||||||
|
protected static ?string $title = 'Plugins';
|
||||||
|
|
||||||
|
protected string $view = 'filament.pages.plugins';
|
||||||
|
|
||||||
|
protected static ?string $slug = 'list-plugins';
|
||||||
|
|
||||||
|
public static function getNavigationGroup(): ?string
|
||||||
|
{
|
||||||
|
return __('filament.navigation.groups.plugins');
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getNavigationLabel(): string
|
||||||
|
{
|
||||||
|
return __('filament.navigation.plugin_list');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function table(Table $table): Table
|
||||||
|
{
|
||||||
|
return $table
|
||||||
|
->columns([
|
||||||
|
TextColumn::make('name')
|
||||||
|
->label(__('Name'))
|
||||||
|
->searchable()
|
||||||
|
->sortable(),
|
||||||
|
TextColumn::make('identifier')
|
||||||
|
->label(__('Identifier'))
|
||||||
|
->searchable()
|
||||||
|
->sortable(),
|
||||||
|
IconColumn::make('enabled')
|
||||||
|
->label(__('Enabled'))
|
||||||
|
->boolean(),
|
||||||
|
IconColumn::make('configured')
|
||||||
|
->label(__('Configured'))
|
||||||
|
->boolean(),
|
||||||
|
])
|
||||||
|
->records(fn () => Plugin::getAllPlugins())
|
||||||
|
->paginated(false)
|
||||||
|
->emptyStateHeading(__('No plugins found'))
|
||||||
|
->emptyStateDescription(__('Drop PHP plugin files into app/Api/Plugins (excluding ApiPluginInterface.php).'));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,102 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Filament\Resources;
|
|
||||||
|
|
||||||
use App\Filament\Resources\AiModelResource\Pages;
|
|
||||||
use App\Filament\Resources\AiModelResource\RelationManagers;
|
|
||||||
use App\Models\AiModel;
|
|
||||||
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\Tables\Columns\TextColumn;
|
|
||||||
use Filament\Forms\Components\Select;
|
|
||||||
use Filament\Tables\Actions\Action;
|
|
||||||
|
|
||||||
class AiModelResource extends Resource
|
|
||||||
{
|
|
||||||
protected static ?string $model = AiModel::class;
|
|
||||||
|
|
||||||
protected static ?string $navigationIcon = 'heroicon-o-rectangle-stack';
|
|
||||||
|
|
||||||
public static function form(Form $form): Form
|
|
||||||
{
|
|
||||||
return $form
|
|
||||||
->schema([
|
|
||||||
TextInput::make('name')
|
|
||||||
->label(__('filament.resource.ai_model.form.name'))
|
|
||||||
->required()
|
|
||||||
->maxLength(255),
|
|
||||||
TextInput::make('model_id')
|
|
||||||
->label(__('filament.resource.ai_model.form.model_id'))
|
|
||||||
->required()
|
|
||||||
->maxLength(255),
|
|
||||||
TextInput::make('model_type')
|
|
||||||
->nullable()
|
|
||||||
->maxLength(255),
|
|
||||||
Forms\Components\Toggle::make('enabled')
|
|
||||||
->label(__('filament.resource.ai_model.form.enabled'))
|
|
||||||
->default(true),
|
|
||||||
Select::make('apiProviders')
|
|
||||||
->relationship('apiProviders', 'name')
|
|
||||||
->multiple()
|
|
||||||
->preload()
|
|
||||||
->searchable(false)
|
|
||||||
->label(__('filament.resource.ai_model.form.api_providers')),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function table(Table $table): Table
|
|
||||||
{
|
|
||||||
return $table
|
|
||||||
->columns([
|
|
||||||
TextColumn::make('name')->label(__('filament.resource.ai_model.table.name'))->searchable()->sortable(),
|
|
||||||
TextColumn::make('model_id')->label(__('filament.resource.ai_model.table.model_id'))->searchable()->sortable(),
|
|
||||||
TextColumn::make('model_type')->label(__('filament.resource.ai_model.table.model_type'))->searchable()->sortable(),
|
|
||||||
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(),
|
|
||||||
])
|
|
||||||
->filters([
|
|
||||||
//
|
|
||||||
])
|
|
||||||
->actions([
|
|
||||||
Tables\Actions\EditAction::make(),
|
|
||||||
Action::make('duplicate')
|
|
||||||
->label(__('filament.resource.style.action.duplicate'))
|
|
||||||
->icon('heroicon-o-document-duplicate')
|
|
||||||
->action(function (AiModel $record, $livewire) {
|
|
||||||
$livewire->redirect(AiModelResource::getUrl('create', ['sourceRecord' => $record->id]));
|
|
||||||
}),
|
|
||||||
])
|
|
||||||
->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\ListAiModels::route('/'),
|
|
||||||
'create' => Pages\CreateAiModel::route('/create'),
|
|
||||||
'edit' => Pages\EditAiModel::route('/{record}/edit'),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
160
app/Filament/Resources/AiModels/AiModelResource.php
Normal file
160
app/Filament/Resources/AiModels/AiModelResource.php
Normal file
@@ -0,0 +1,160 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Filament\Resources\AiModels;
|
||||||
|
|
||||||
|
use BackedEnum;
|
||||||
|
use App\Filament\Resources\AiModels\Pages;
|
||||||
|
use App\Filament\Resources\AiModels\RelationManagers;
|
||||||
|
use App\Models\AiModel;
|
||||||
|
use Filament\Schemas\Schema;
|
||||||
|
use Filament\Resources\Resource;
|
||||||
|
use Filament\Tables;
|
||||||
|
use Filament\Tables\Table;
|
||||||
|
use Filament\Tables\Columns\TextColumn;
|
||||||
|
use Filament\Tables\Columns\IconColumn;
|
||||||
|
use Illuminate\Database\Eloquent\SoftDeletingScope;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use App\Models\ApiProvider;
|
||||||
|
use App\Api\Plugins\PluginLoader;
|
||||||
|
use App\Api\Plugins\ApiPluginInterface;
|
||||||
|
use Filament\Actions\Action;
|
||||||
|
use Filament\Actions\BulkActionGroup;
|
||||||
|
use Filament\Actions\CreateAction;
|
||||||
|
use Filament\Actions\DeleteBulkAction;
|
||||||
|
use Filament\Actions\EditAction;
|
||||||
|
use Filament\Forms\Components\TextInput;
|
||||||
|
use Filament\Forms\Components\Toggle;
|
||||||
|
use Filament\Forms\Components\Textarea;
|
||||||
|
|
||||||
|
class AiModelResource extends Resource
|
||||||
|
{
|
||||||
|
protected static ?string $model = AiModel::class;
|
||||||
|
|
||||||
|
// protected static ?string $navigationIcon = 'heroicon-o-rectangle-stack';
|
||||||
|
|
||||||
|
public static function form(Schema $schema): Schema
|
||||||
|
{
|
||||||
|
return $schema
|
||||||
|
->components([
|
||||||
|
TextInput::make('name')
|
||||||
|
->required(),
|
||||||
|
TextInput::make('model_id')
|
||||||
|
->required(),
|
||||||
|
TextInput::make('model_type')
|
||||||
|
->nullable(),
|
||||||
|
Toggle::make('enabled')
|
||||||
|
->default(true),
|
||||||
|
Textarea::make('parameters')
|
||||||
|
->nullable()
|
||||||
|
->rows(15),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static function canSearchModelsWithAnyProvider(?array $apiProviderIds): bool
|
||||||
|
{
|
||||||
|
if (empty($apiProviderIds)) {
|
||||||
|
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
|
||||||
|
{
|
||||||
|
if (!$apiProviderId) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
$apiProvider = ApiProvider::find($apiProviderId);
|
||||||
|
if (!$apiProvider || !$apiProvider->plugin) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return PluginLoader::getPlugin($apiProvider->plugin, $apiProvider);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
// Log the exception if needed
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
->columns([
|
||||||
|
TextColumn::make('name')->searchable()->sortable(),
|
||||||
|
TextColumn::make('model_id')->searchable()->sortable(),
|
||||||
|
TextColumn::make('model_type')->searchable()->sortable(),
|
||||||
|
IconColumn::make('enabled')
|
||||||
|
->boolean(),
|
||||||
|
TextColumn::make('primaryApiProvider.name')->searchable()->sortable()->limit(50),
|
||||||
|
])
|
||||||
|
->filters([
|
||||||
|
//
|
||||||
|
])
|
||||||
|
->actions([
|
||||||
|
EditAction::make(),
|
||||||
|
Action::make('duplicate')
|
||||||
|
->label('Duplicate')
|
||||||
|
->icon('heroicon-o-document-duplicate')
|
||||||
|
->action(function (Model $record, $livewire) {
|
||||||
|
$livewire->redirect(static::getUrl('create', ['sourceRecord' => $record->id]));
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
->bulkActions([
|
||||||
|
BulkActionGroup::make([
|
||||||
|
DeleteBulkAction::make(),
|
||||||
|
]),
|
||||||
|
])
|
||||||
|
->emptyStateActions([
|
||||||
|
CreateAction::make(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getRelations(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
//
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getPages(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'index' => Pages\ListAiModels::route('/'),
|
||||||
|
'create' => Pages\CreateAiModel::route('/create'),
|
||||||
|
'edit' => Pages\EditAiModel::route('/{record}/edit'),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Filament\Resources\AiModelResource\Pages;
|
namespace App\Filament\Resources\AiModels\Pages;
|
||||||
|
|
||||||
use App\Filament\Resources\AiModelResource;
|
use App\Filament\Resources\AiModels\AiModelResource;
|
||||||
use Filament\Actions;
|
use Filament\Actions;
|
||||||
use Filament\Resources\Pages\CreateRecord;
|
use Filament\Resources\Pages\CreateRecord;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Filament\Resources\AiModelResource\Pages;
|
namespace App\Filament\Resources\AiModels\Pages;
|
||||||
|
|
||||||
use App\Filament\Resources\AiModelResource;
|
use App\Filament\Resources\AiModels\AiModelResource;
|
||||||
use Filament\Actions;
|
use Filament\Actions;
|
||||||
use Filament\Resources\Pages\EditRecord;
|
use Filament\Resources\Pages\EditRecord;
|
||||||
|
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Filament\Resources\AiModelResource\Pages;
|
namespace App\Filament\Resources\AiModels\Pages;
|
||||||
|
|
||||||
use App\Filament\Resources\AiModelResource;
|
use App\Filament\Resources\AiModels\AiModelResource;
|
||||||
use Filament\Actions;
|
use Filament\Actions;
|
||||||
use Filament\Resources\Pages\ListRecords;
|
use Filament\Resources\Pages\ListRecords;
|
||||||
use Illuminate\Contracts\Pagination\Paginator;
|
use Illuminate\Contracts\Pagination\Paginator;
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Filament\Resources\ApiProviderResource\Pages;
|
|
||||||
|
|
||||||
use App\Filament\Resources\ApiProviderResource;
|
|
||||||
use Filament\Actions;
|
|
||||||
use Filament\Resources\Pages\CreateRecord;
|
|
||||||
|
|
||||||
class CreateApiProvider extends CreateRecord
|
|
||||||
{
|
|
||||||
protected static string $resource = ApiProviderResource::class;
|
|
||||||
|
|
||||||
protected function getRedirectUrl(): string
|
|
||||||
{
|
|
||||||
return $this->getResource()::getUrl('index');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,14 +1,18 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Filament\Resources;
|
namespace App\Filament\Resources\ApiProviders;
|
||||||
|
|
||||||
use App\Filament\Resources\ApiProviderResource\Pages;
|
use App\Filament\Resources\ApiProviders\Pages;
|
||||||
use App\Filament\Resources\ApiProviderResource\RelationManagers;
|
use App\Filament\Resources\ApiProviders\RelationManagers;
|
||||||
use App\Models\ApiProvider;
|
use App\Models\ApiProvider;
|
||||||
use Filament\Forms;
|
use Filament\Schemas;
|
||||||
use Filament\Forms\Form;
|
use Filament\Schemas\Schema;
|
||||||
use Filament\Resources\Resource;
|
use Filament\Resources\Resource;
|
||||||
use Filament\Tables;
|
use Filament\Tables;
|
||||||
|
use Filament\Actions\BulkActionGroup;
|
||||||
|
use Filament\Actions\CreateAction;
|
||||||
|
use Filament\Actions\DeleteBulkAction;
|
||||||
|
use Filament\Actions\EditAction;
|
||||||
use Filament\Tables\Table;
|
use Filament\Tables\Table;
|
||||||
use Illuminate\Database\Eloquent\Builder;
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
use Illuminate\Database\Eloquent\SoftDeletingScope;
|
use Illuminate\Database\Eloquent\SoftDeletingScope;
|
||||||
@@ -17,51 +21,43 @@ use Filament\Tables\Columns\TextColumn;
|
|||||||
use Filament\Forms\Components\Select;
|
use Filament\Forms\Components\Select;
|
||||||
use Illuminate\Support\Facades\File;
|
use Illuminate\Support\Facades\File;
|
||||||
use App\Api\Plugins\ApiPluginInterface;
|
use App\Api\Plugins\ApiPluginInterface;
|
||||||
|
|
||||||
use Filament\Forms\Components\Toggle;
|
use Filament\Forms\Components\Toggle;
|
||||||
use Filament\Tables\Columns\IconColumn;
|
use Filament\Tables\Columns\IconColumn;
|
||||||
|
use BackedEnum;
|
||||||
|
|
||||||
class ApiProviderResource extends Resource
|
class ApiProviderResource extends Resource
|
||||||
{
|
{
|
||||||
protected static ?string $model = ApiProvider::class;
|
protected static ?string $model = ApiProvider::class;
|
||||||
|
|
||||||
protected static ?string $navigationIcon = 'heroicon-o-rectangle-stack';
|
protected static BackedEnum|string|null $navigationIcon = 'heroicon-o-rectangle-stack';
|
||||||
|
|
||||||
public static function form(Form $form): Form
|
public static function form(Schema $schema): Schema
|
||||||
{
|
{
|
||||||
$plugins = self::getAvailablePlugins();
|
$plugins = self::getAvailablePlugins();
|
||||||
|
return $schema
|
||||||
return $form
|
->components([
|
||||||
->schema([
|
|
||||||
TextInput::make('name')
|
TextInput::make('name')
|
||||||
->label(__('filament.resource.api_provider.form.name'))
|
|
||||||
->required()
|
->required()
|
||||||
->maxLength(255),
|
->maxLength(255),
|
||||||
Toggle::make('enabled')
|
Toggle::make('enabled')
|
||||||
->label(__('filament.resource.api_provider.form.enabled'))
|
|
||||||
->default(true),
|
->default(true),
|
||||||
TextInput::make('api_url')
|
TextInput::make('api_url')
|
||||||
->label(__('filament.resource.api_provider.form.api_url'))
|
|
||||||
->required()
|
->required()
|
||||||
->url()
|
->url()
|
||||||
->maxLength(255),
|
->maxLength(255),
|
||||||
TextInput::make('username')
|
TextInput::make('username')
|
||||||
->label(__('filament.resource.api_provider.form.username'))
|
|
||||||
->nullable()
|
->nullable()
|
||||||
->maxLength(255),
|
->maxLength(255),
|
||||||
TextInput::make('password')
|
TextInput::make('password')
|
||||||
->label(__('filament.resource.api_provider.form.password'))
|
|
||||||
->password()
|
->password()
|
||||||
->nullable()
|
->nullable()
|
||||||
->maxLength(255),
|
->maxLength(255),
|
||||||
TextInput::make('token')
|
TextInput::make('token')
|
||||||
->label(__('filament.resource.api_provider.form.token'))
|
|
||||||
->nullable()
|
->nullable()
|
||||||
->maxLength(255),
|
->maxLength(255),
|
||||||
Select::make('plugin')
|
Select::make('plugin')
|
||||||
->options($plugins)
|
->options($plugins)
|
||||||
->nullable()
|
->nullable(),
|
||||||
->label(__('filament.resource.api_provider.form.plugin')),
|
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -69,38 +65,25 @@ class ApiProviderResource extends Resource
|
|||||||
{
|
{
|
||||||
return $table
|
return $table
|
||||||
->columns([
|
->columns([
|
||||||
TextColumn::make('name')->label(__('filament.resource.api_provider.table.name'))->searchable()->sortable(),
|
TextColumn::make('name')->searchable()->sortable(),
|
||||||
IconColumn::make('enabled')
|
IconColumn::make('enabled')
|
||||||
->label(__('filament.resource.api_provider.table.enabled'))
|
|
||||||
->boolean(),
|
->boolean(),
|
||||||
TextColumn::make('api_url')->label(__('filament.resource.api_provider.table.api_url'))->searchable(),
|
TextColumn::make('api_url')->searchable(),
|
||||||
TextColumn::make('plugin')->label(__('filament.resource.api_provider.table.plugin'))->searchable()->sortable(),
|
TextColumn::make('plugin')->searchable()->sortable(),
|
||||||
])
|
])
|
||||||
->filters([
|
->filters([
|
||||||
//
|
//
|
||||||
])
|
])
|
||||||
->actions([
|
->actions([
|
||||||
Tables\Actions\EditAction::make(),
|
EditAction::make(),
|
||||||
])
|
])
|
||||||
->bulkActions([
|
->bulkActions([
|
||||||
Tables\Actions\BulkActionGroup::make([
|
BulkActionGroup::make([
|
||||||
Tables\Actions\DeleteBulkAction::make(),
|
DeleteBulkAction::make(),
|
||||||
Tables\Actions\BulkAction::make('enable')
|
|
||||||
->label(__('filament.resource.api_provider.action.enable_selected'))
|
|
||||||
->icon('heroicon-o-check-circle')
|
|
||||||
->action(function (\Illuminate\Support\Collection $records) {
|
|
||||||
$records->each->update(['enabled' => true]);
|
|
||||||
}),
|
|
||||||
Tables\Actions\BulkAction::make('disable')
|
|
||||||
->label(__('filament.resource.api_provider.action.disable_selected'))
|
|
||||||
->icon('heroicon-o-x-circle')
|
|
||||||
->action(function (\Illuminate\Support\Collection $records) {
|
|
||||||
$records->each->update(['enabled' => false]);
|
|
||||||
}),
|
|
||||||
]),
|
]),
|
||||||
])
|
])
|
||||||
->emptyStateActions([
|
->emptyStateActions([
|
||||||
Tables\Actions\CreateAction::make(),
|
CreateAction::make(),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -119,26 +102,23 @@ class ApiProviderResource extends Resource
|
|||||||
'edit' => Pages\EditApiProvider::route('/{record}/edit'),
|
'edit' => Pages\EditApiProvider::route('/{record}/edit'),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
protected static function getAvailablePlugins(): array
|
protected static function getAvailablePlugins(): array
|
||||||
{
|
{
|
||||||
$plugins = [];
|
$plugins = [];
|
||||||
$path = app_path('Api/Plugins');
|
$path = app_path('Api/Plugins');
|
||||||
$files = File::files($path);
|
$files = File::files($path);
|
||||||
|
|
||||||
foreach ($files as $file) {
|
foreach ($files as $file) {
|
||||||
$filename = $file->getFilenameWithoutExtension();
|
$filename = $file->getFilenameWithoutExtension();
|
||||||
if (in_array($filename, ['ApiPluginInterface', 'PluginLoader'])) {
|
if (in_array($filename, ['ApiPluginInterface', 'PluginLoader'])) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
$class = "App\Api\Plugins\\" . $filename;
|
||||||
$class = 'App\\Api\\Plugins\\' . $filename;
|
|
||||||
if (class_exists($class) && in_array(ApiPluginInterface::class, class_implements($class))) {
|
if (class_exists($class) && in_array(ApiPluginInterface::class, class_implements($class))) {
|
||||||
// Do not instantiate here, just get identifier and name if possible statically or by convention
|
|
||||||
// For now, we'll use filename as identifier and name
|
|
||||||
$plugins[$filename] = $filename;
|
$plugins[$filename] = $filename;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return $plugins;
|
return $plugins;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Filament\Resources\ApiProviders\Pages;
|
||||||
|
|
||||||
|
use App\Filament\Resources\ApiProviders\ApiProviderResource;
|
||||||
|
use Filament\Actions;
|
||||||
|
use Filament\Resources\Pages\CreateRecord;
|
||||||
|
use Livewire\Attributes\On;
|
||||||
|
|
||||||
|
class CreateApiProvider extends CreateRecord
|
||||||
|
{
|
||||||
|
public $testResultState = 'none';
|
||||||
|
|
||||||
|
protected static string $resource = ApiProviderResource::class;
|
||||||
|
|
||||||
|
#[On('testConnectionFinished')]
|
||||||
|
public function onTestConnectionFinished($result)
|
||||||
|
{
|
||||||
|
$this->testResultState = $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getRedirectUrl(): string
|
||||||
|
{
|
||||||
|
return $this->getResource()::getUrl('index');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,15 +1,24 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Filament\Resources\ApiProviderResource\Pages;
|
namespace App\Filament\Resources\ApiProviders\Pages;
|
||||||
|
|
||||||
use App\Filament\Resources\ApiProviderResource;
|
use App\Filament\Resources\ApiProviders\ApiProviderResource;
|
||||||
use Filament\Actions;
|
use Filament\Actions;
|
||||||
use Filament\Resources\Pages\EditRecord;
|
use Filament\Resources\Pages\EditRecord;
|
||||||
|
use Livewire\Attributes\On;
|
||||||
|
|
||||||
class EditApiProvider extends EditRecord
|
class EditApiProvider extends EditRecord
|
||||||
{
|
{
|
||||||
|
public $testResultState = 'none';
|
||||||
|
|
||||||
protected static string $resource = ApiProviderResource::class;
|
protected static string $resource = ApiProviderResource::class;
|
||||||
|
|
||||||
|
#[On('testConnectionFinished')]
|
||||||
|
public function onTestConnectionFinished($result)
|
||||||
|
{
|
||||||
|
$this->testResultState = $result;
|
||||||
|
}
|
||||||
|
|
||||||
protected function getHeaderActions(): array
|
protected function getHeaderActions(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Filament\Resources\ApiProviderResource\Pages;
|
namespace App\Filament\Resources\ApiProviders\Pages;
|
||||||
|
|
||||||
use App\Filament\Resources\ApiProviderResource;
|
use App\Filament\Resources\ApiProviders\ApiProviderResource;
|
||||||
use Filament\Actions;
|
use Filament\Actions;
|
||||||
use Filament\Resources\Pages\ListRecords;
|
use Filament\Resources\Pages\ListRecords;
|
||||||
use Illuminate\Contracts\Pagination\Paginator;
|
use Illuminate\Contracts\Pagination\Paginator;
|
||||||
@@ -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');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,38 +1,43 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Filament\Resources;
|
namespace App\Filament\Resources\Images;
|
||||||
|
|
||||||
use App\Filament\Resources\ImageResource\Pages;
|
use BackedEnum;
|
||||||
use App\Filament\Resources\ImageResource\RelationManagers;
|
use App\Filament\Resources\Images\Pages;
|
||||||
|
use App\Filament\Resources\Images\RelationManagers;
|
||||||
use App\Models\Image;
|
use App\Models\Image;
|
||||||
use Filament\Forms;
|
use Filament\Schemas;
|
||||||
use Filament\Forms\Form;
|
use Filament\Schemas\Schema;
|
||||||
use Filament\Resources\Resource;
|
use Filament\Resources\Resource;
|
||||||
use Filament\Tables;
|
use Filament\Tables;
|
||||||
use Filament\Tables\Table;
|
use Filament\Tables\Table;
|
||||||
use Illuminate\Database\Eloquent\Builder;
|
|
||||||
use Illuminate\Database\Eloquent\SoftDeletingScope;
|
use Illuminate\Database\Eloquent\SoftDeletingScope;
|
||||||
use Filament\Forms\Components\TextInput;
|
use Filament\Forms\Components\TextInput;
|
||||||
use Filament\Tables\Columns\TextColumn;
|
use Filament\Tables\Columns\TextColumn;
|
||||||
|
use Filament\Tables\Columns\ImageColumn;
|
||||||
use Filament\Forms\Components\FileUpload;
|
use Filament\Forms\Components\FileUpload;
|
||||||
|
use Filament\Forms\Components\Toggle;
|
||||||
|
use Filament\Actions\BulkActionGroup;
|
||||||
|
use Filament\Actions\CreateAction;
|
||||||
|
use Filament\Actions\DeleteBulkAction;
|
||||||
|
use Filament\Actions\EditAction;
|
||||||
|
|
||||||
class ImageResource extends Resource
|
class ImageResource extends Resource
|
||||||
{
|
{
|
||||||
protected static ?string $model = Image::class;
|
protected static ?string $model = Image::class;
|
||||||
|
|
||||||
protected static ?string $navigationIcon = 'heroicon-o-rectangle-stack';
|
protected static BackedEnum|string|null $navigationIcon = 'heroicon-o-rectangle-stack';
|
||||||
|
|
||||||
public static function form(Form $form): Form
|
public static function form(Schema $schema): Schema
|
||||||
{
|
{
|
||||||
return $form
|
return $schema
|
||||||
->schema([
|
->components([
|
||||||
FileUpload::make('path')
|
FileUpload::make('path')
|
||||||
->label(__('filament.resource.image.form.path'))
|
|
||||||
->required()
|
->required()
|
||||||
->image()
|
->image()
|
||||||
->directory('uploads'),
|
->directory('uploads')
|
||||||
Forms\Components\Toggle::make('is_public')
|
->disk('public'),
|
||||||
->label(__('Publicly Visible'))
|
Toggle::make('is_public')
|
||||||
->default(false),
|
->default(false),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
@@ -41,22 +46,23 @@ class ImageResource extends Resource
|
|||||||
{
|
{
|
||||||
return $table
|
return $table
|
||||||
->columns([
|
->columns([
|
||||||
TextColumn::make('path')->label(__('filament.resource.image.table.path'))->searchable()->sortable(),
|
TextColumn::make('path')->searchable()->sortable(),
|
||||||
Tables\Columns\ImageColumn::make('path')->label(__('filament.resource.image.table.image')),
|
ImageColumn::make('path')
|
||||||
|
->disk('public'),
|
||||||
])
|
])
|
||||||
->filters([
|
->filters([
|
||||||
//
|
//
|
||||||
])
|
])
|
||||||
->actions([
|
->actions([
|
||||||
Tables\Actions\EditAction::make(),
|
EditAction::make(),
|
||||||
])
|
])
|
||||||
->bulkActions([
|
->bulkActions([
|
||||||
Tables\Actions\BulkActionGroup::make([
|
BulkActionGroup::make([
|
||||||
Tables\Actions\DeleteBulkAction::make(),
|
DeleteBulkAction::make(),
|
||||||
]),
|
]),
|
||||||
])
|
])
|
||||||
->emptyStateActions([
|
->emptyStateActions([
|
||||||
Tables\Actions\CreateAction::make(),
|
CreateAction::make(),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -71,8 +77,7 @@ class ImageResource extends Resource
|
|||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
'index' => Pages\ListImages::route('/'),
|
'index' => Pages\ListImages::route('/'),
|
||||||
'create' => Pages\CreateImage::route('/create'),
|
|
||||||
'edit' => Pages\EditImage::route('/{record}/edit'),
|
'edit' => Pages\EditImage::route('/{record}/edit'),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Filament\Resources\ImageResource\Pages;
|
namespace App\Filament\Resources\Images\Pages;
|
||||||
|
|
||||||
use App\Filament\Resources\ImageResource;
|
use App\Filament\Resources\Images\ImageResource;
|
||||||
use Filament\Actions;
|
use Filament\Actions;
|
||||||
use Filament\Resources\Pages\EditRecord;
|
use Filament\Resources\Pages\EditRecord;
|
||||||
|
|
||||||
@@ -1,12 +1,14 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Filament\Resources\ImageResource\Pages;
|
namespace App\Filament\Resources\Images\Pages;
|
||||||
|
|
||||||
use App\Filament\Resources\ImageResource;
|
use App\Filament\Resources\Images\ImageResource;
|
||||||
use Filament\Actions;
|
use Filament\Actions;
|
||||||
use Filament\Resources\Pages\ListRecords;
|
use Filament\Resources\Pages\ListRecords;
|
||||||
use Illuminate\Contracts\Pagination\Paginator;
|
use Illuminate\Contracts\Pagination\Paginator;
|
||||||
use Illuminate\Database\Eloquent\Builder;
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
|
use Illuminate\Support\Facades\Storage;
|
||||||
|
use Filament\Notifications\Notification;
|
||||||
|
|
||||||
class ListImages extends ListRecords
|
class ListImages extends ListRecords
|
||||||
{
|
{
|
||||||
@@ -15,7 +17,25 @@ class ListImages extends ListRecords
|
|||||||
protected function getHeaderActions(): array
|
protected function getHeaderActions(): array
|
||||||
{
|
{
|
||||||
return [
|
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());
|
||||||
|
}),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1,74 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Filament\Resources\PluginResource;
|
|
||||||
|
|
||||||
use Illuminate\Database\Eloquent\Builder;
|
|
||||||
use Illuminate\Database\Eloquent\Collection;
|
|
||||||
use Illuminate\Pagination\LengthAwarePaginator;
|
|
||||||
use Illuminate\Pagination\Paginator;
|
|
||||||
|
|
||||||
class CollectionEloquentBuilder extends Builder
|
|
||||||
{
|
|
||||||
protected $collection;
|
|
||||||
|
|
||||||
public function __construct($query)
|
|
||||||
{
|
|
||||||
parent::__construct($query);
|
|
||||||
$this->collection = new Collection(); // Initialize with an empty collection
|
|
||||||
}
|
|
||||||
|
|
||||||
public function setCollection(Collection $collection)
|
|
||||||
{
|
|
||||||
$this->collection = $collection;
|
|
||||||
return $this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function get($columns = ['*'])
|
|
||||||
{
|
|
||||||
return $this->collection;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function find($id, $columns = ['*'])
|
|
||||||
{
|
|
||||||
return $this->collection->firstWhere('id', $id);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function paginate($perPage = null, $columns = ['*'], $pageName = 'page', $page = null)
|
|
||||||
{
|
|
||||||
$page = $page ?: Paginator::resolveCurrentPage($pageName);
|
|
||||||
|
|
||||||
$results = $this->collection->slice(($page - 1) * $perPage, $perPage)->all();
|
|
||||||
|
|
||||||
return new LengthAwarePaginator($results, $this->collection->count(), $perPage, $page, [
|
|
||||||
'path' => Paginator::resolveCurrentPath(),
|
|
||||||
'pageName' => $pageName,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function count($columns = '*')
|
|
||||||
{
|
|
||||||
return $this->collection->count();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function where($column, $operator = null, $value = null, $boolean = 'and')
|
|
||||||
{
|
|
||||||
if (func_num_args() === 2) {
|
|
||||||
[$value, $operator] = [$operator, '='];
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($operator === '=') {
|
|
||||||
$this->collection = $this->collection->where($column, $value);
|
|
||||||
} else {
|
|
||||||
// For simplicity, only handling '=' operator for now. More complex operators would require more logic.
|
|
||||||
// For example, for 'like', you'd need to implement string matching.
|
|
||||||
}
|
|
||||||
|
|
||||||
return $this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function orderBy($column, $direction = 'asc')
|
|
||||||
{
|
|
||||||
$this->collection = $this->collection->sortBy($column, SORT_REGULAR, $direction === 'desc');
|
|
||||||
return $this;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Filament\Resources\PluginResource;
|
|
||||||
|
|
||||||
use Illuminate\Database\Eloquent\Model;
|
|
||||||
|
|
||||||
class Plugin extends Model
|
|
||||||
{
|
|
||||||
protected $table = null; // No actual table
|
|
||||||
|
|
||||||
protected $guarded = []; // Allow mass assignment for all attributes
|
|
||||||
|
|
||||||
public $incrementing = false;
|
|
||||||
|
|
||||||
protected $keyType = 'string';
|
|
||||||
|
|
||||||
protected $primaryKey = 'id';
|
|
||||||
|
|
||||||
public function __construct(array $attributes = [])
|
|
||||||
{
|
|
||||||
parent::__construct($attributes);
|
|
||||||
foreach ($attributes as $key => $value) {
|
|
||||||
$this->$key = $value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public function newCollection(array $models = [])
|
|
||||||
{
|
|
||||||
return new \Illuminate\Database\Eloquent\Collection($models);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function newEloquentBuilder($query)
|
|
||||||
{
|
|
||||||
return new CollectionEloquentBuilder($query);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Filament\Resources\RoleResource\Pages;
|
namespace App\Filament\Resources\Roles\Pages;
|
||||||
|
|
||||||
use App\Filament\Resources\RoleResource;
|
use App\Filament\Resources\Roles\RoleResource;
|
||||||
use Filament\Actions;
|
use Filament\Actions;
|
||||||
use Filament\Resources\Pages\CreateRecord;
|
use Filament\Resources\Pages\CreateRecord;
|
||||||
|
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Filament\Resources\RoleResource\Pages;
|
namespace App\Filament\Resources\Roles\Pages;
|
||||||
|
|
||||||
use App\Filament\Resources\RoleResource;
|
use App\Filament\Resources\Roles\RoleResource;
|
||||||
use Filament\Actions;
|
use Filament\Actions;
|
||||||
use Filament\Resources\Pages\EditRecord;
|
use Filament\Resources\Pages\EditRecord;
|
||||||
|
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Filament\Resources\RoleResource\Pages;
|
namespace App\Filament\Resources\Roles\Pages;
|
||||||
|
|
||||||
use App\Filament\Resources\RoleResource;
|
use App\Filament\Resources\Roles\RoleResource;
|
||||||
use Filament\Actions;
|
use Filament\Actions;
|
||||||
use Filament\Resources\Pages\ListRecords;
|
use Filament\Resources\Pages\ListRecords;
|
||||||
|
|
||||||
@@ -1,33 +1,36 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Filament\Resources;
|
namespace App\Filament\Resources\Roles;
|
||||||
|
|
||||||
use App\Filament\Resources\RoleResource\Pages;
|
use UnitEnum;
|
||||||
use App\Filament\Resources\RoleResource\RelationManagers;
|
use App\Filament\Resources\Roles\Pages;
|
||||||
|
use App\Filament\Resources\Roles\RelationManagers;
|
||||||
use App\Models\Role;
|
use App\Models\Role;
|
||||||
use Filament\Forms;
|
use Filament\Schemas;
|
||||||
use Filament\Forms\Form;
|
use Filament\Schemas\Schema;
|
||||||
use Filament\Resources\Resource;
|
use Filament\Resources\Resource;
|
||||||
use Filament\Tables;
|
use Filament\Tables;
|
||||||
use Filament\Tables\Table;
|
use Filament\Tables\Table;
|
||||||
use Illuminate\Database\Eloquent\Builder;
|
|
||||||
use Illuminate\Database\Eloquent\SoftDeletingScope;
|
use Illuminate\Database\Eloquent\SoftDeletingScope;
|
||||||
use Filament\Forms\Components\TextInput;
|
use Filament\Forms\Components\TextInput;
|
||||||
use Filament\Tables\Columns\TextColumn;
|
use Filament\Tables\Columns\TextColumn;
|
||||||
|
use Filament\Actions\BulkActionGroup;
|
||||||
|
use Filament\Actions\CreateAction;
|
||||||
|
use Filament\Actions\DeleteBulkAction;
|
||||||
|
use Filament\Actions\EditAction;
|
||||||
|
|
||||||
class RoleResource extends Resource
|
class RoleResource extends Resource
|
||||||
{
|
{
|
||||||
protected static ?string $model = Role::class;
|
protected static ?string $model = Role::class;
|
||||||
|
|
||||||
protected static ?string $navigationGroup = 'User Management';
|
protected static UnitEnum|string|null $navigationGroup = 'User Management';
|
||||||
protected static ?string $navigationLabel = 'User Roles';
|
protected static ?string $navigationLabel = 'User Roles';
|
||||||
|
|
||||||
public static function form(Form $form): Form
|
public static function form(Schema $schema): Schema
|
||||||
{
|
{
|
||||||
return $form
|
return $schema
|
||||||
->schema([
|
->components([
|
||||||
TextInput::make('name')
|
TextInput::make('name')
|
||||||
->label(__('filament.resource.role.form.name'))
|
|
||||||
->required()
|
->required()
|
||||||
->maxLength(255),
|
->maxLength(255),
|
||||||
]);
|
]);
|
||||||
@@ -37,21 +40,21 @@ class RoleResource extends Resource
|
|||||||
{
|
{
|
||||||
return $table
|
return $table
|
||||||
->columns([
|
->columns([
|
||||||
TextColumn::make('name')->label(__('filament.resource.role.table.name'))->searchable()->sortable(),
|
TextColumn::make('name')->searchable()->sortable(),
|
||||||
])
|
])
|
||||||
->filters([
|
->filters([
|
||||||
//
|
//
|
||||||
])
|
])
|
||||||
->actions([
|
->actions([
|
||||||
Tables\Actions\EditAction::make(),
|
EditAction::make(),
|
||||||
])
|
])
|
||||||
->bulkActions([
|
->bulkActions([
|
||||||
Tables\Actions\BulkActionGroup::make([
|
BulkActionGroup::make([
|
||||||
Tables\Actions\DeleteBulkAction::make(),
|
DeleteBulkAction::make(),
|
||||||
]),
|
]),
|
||||||
])
|
])
|
||||||
->emptyStateActions([
|
->emptyStateActions([
|
||||||
Tables\Actions\CreateAction::make(),
|
CreateAction::make(),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -70,4 +73,4 @@ class RoleResource extends Resource
|
|||||||
'edit' => Pages\EditRole::route('/{record}/edit'),
|
'edit' => Pages\EditRole::route('/{record}/edit'),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,82 +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';
|
|
||||||
|
|
||||||
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 ? $record->key : __('New Setting'))
|
|
||||||
->schema([
|
|
||||||
TextInput::make('value')
|
|
||||||
->label(__('Value'))
|
|
||||||
->disableLabel()
|
|
||||||
])
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
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'),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
@@ -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(),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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(),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,131 +0,0 @@
|
|||||||
<?php
|
|
||||||
namespace App\Filament\Resources;
|
|
||||||
|
|
||||||
use App\Filament\Resources\StyleResource\Pages;
|
|
||||||
use App\Models\Style;
|
|
||||||
use Filament\Forms;
|
|
||||||
use Filament\Forms\Form;
|
|
||||||
use Filament\Resources\Resource;
|
|
||||||
use Filament\Tables;
|
|
||||||
use Filament\Tables\Table;
|
|
||||||
use Filament\Forms\Components\TextInput;
|
|
||||||
use Filament\Forms\Components\Textarea;
|
|
||||||
use Filament\Forms\Components\FileUpload;
|
|
||||||
use Filament\Forms\Components\Select;
|
|
||||||
use Filament\Tables\Columns\TextColumn;
|
|
||||||
use Filament\Tables\Columns\ImageColumn;
|
|
||||||
use Filament\Forms\Components\Toggle;
|
|
||||||
use Filament\Tables\Columns\IconColumn;
|
|
||||||
use Filament\Tables\Filters\SelectFilter;
|
|
||||||
|
|
||||||
class StyleResource extends Resource
|
|
||||||
{
|
|
||||||
protected static ?string $model = Style::class;
|
|
||||||
|
|
||||||
protected static ?string $navigationIcon = 'heroicon-o-rectangle-stack';
|
|
||||||
|
|
||||||
public static function form(Form $form): Form
|
|
||||||
{
|
|
||||||
return $form
|
|
||||||
->schema([
|
|
||||||
TextInput::make('title')
|
|
||||||
->label(__('filament.resource.style.form.title'))
|
|
||||||
->required()
|
|
||||||
->maxLength(255),
|
|
||||||
Toggle::make('enabled')
|
|
||||||
->label(__('filament.resource.style.form.enabled'))
|
|
||||||
->default(true),
|
|
||||||
Textarea::make('prompt')
|
|
||||||
->label(__('filament.resource.style.form.prompt'))
|
|
||||||
->required()
|
|
||||||
->rows(5),
|
|
||||||
Textarea::make('description')
|
|
||||||
->label(__('filament.resource.style.form.description'))
|
|
||||||
->required()
|
|
||||||
->rows(5),
|
|
||||||
FileUpload::make('preview_image')
|
|
||||||
->label(__('filament.resource.style.form.preview_image'))
|
|
||||||
->required()
|
|
||||||
->image()
|
|
||||||
->disk('public')
|
|
||||||
->directory('style_previews'),
|
|
||||||
Textarea::make('parameters')
|
|
||||||
->label(__('filament.resource.style.form.parameters'))
|
|
||||||
->nullable()
|
|
||||||
->rows(15)
|
|
||||||
->json()
|
|
||||||
->helperText(__('filament.resource.style.form.parameters_help')),
|
|
||||||
Select::make('ai_model_id')
|
|
||||||
->relationship('aiModel', 'name')
|
|
||||||
->label(__('filament.resource.style.form.ai_model'))
|
|
||||||
->required(),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function table(Table $table): Table
|
|
||||||
{
|
|
||||||
return $table
|
|
||||||
->columns([
|
|
||||||
TextColumn::make('title')->label(__('filament.resource.style.table.title'))->searchable()->sortable(),
|
|
||||||
IconColumn::make('enabled')
|
|
||||||
->label(__('filament.resource.style.table.enabled'))
|
|
||||||
->boolean(),
|
|
||||||
TextColumn::make('aiModel.name')->label(__('filament.resource.style.table.ai_model'))->searchable()->sortable(),
|
|
||||||
ImageColumn::make('preview_image')->label(__('filament.resource.style.table.preview_image'))->disk('public'),
|
|
||||||
])
|
|
||||||
->filters([
|
|
||||||
SelectFilter::make('ai_model')
|
|
||||||
->relationship('aiModel', 'name')
|
|
||||||
->label(__('filament.resource.style.table.ai_model')),
|
|
||||||
])
|
|
||||||
->actions([
|
|
||||||
Tables\Actions\EditAction::make(),
|
|
||||||
Tables\Actions\Action::make('duplicate')
|
|
||||||
->label(__('filament.resource.style.action.duplicate'))
|
|
||||||
->icon('heroicon-o-document-duplicate')
|
|
||||||
->action(function (\App\Models\Style $record) {
|
|
||||||
$newStyle = $record->replicate();
|
|
||||||
$newStyle->title = $record->title . ' (Kopie)';
|
|
||||||
$newStyle->save();
|
|
||||||
|
|
||||||
return redirect()->to(StyleResource::getUrl('edit', ['record' => $newStyle->id]));
|
|
||||||
}),
|
|
||||||
])
|
|
||||||
->bulkActions([
|
|
||||||
Tables\Actions\BulkActionGroup::make([
|
|
||||||
Tables\Actions\DeleteBulkAction::make(),
|
|
||||||
Tables\Actions\BulkAction::make('enable')
|
|
||||||
->label(__('filament.resource.style.action.enable_selected'))
|
|
||||||
->icon('heroicon-o-check-circle')
|
|
||||||
->action(function (\Illuminate\Support\Collection $records) {
|
|
||||||
$records->each->update(['enabled' => true]);
|
|
||||||
}),
|
|
||||||
Tables\Actions\BulkAction::make('disable')
|
|
||||||
->label(__('filament.resource.style.action.disable_selected'))
|
|
||||||
->icon('heroicon-o-x-circle')
|
|
||||||
->action(function (\Illuminate\Support\Collection $records) {
|
|
||||||
$records->each->update(['enabled' => false]);
|
|
||||||
}),
|
|
||||||
]),
|
|
||||||
])
|
|
||||||
->emptyStateActions([
|
|
||||||
Tables\Actions\CreateAction::make(),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function getRelations(): array
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
//
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function getPages(): array
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
'index' => Pages\ListStyles::route('/'),
|
|
||||||
'create' => Pages\CreateStyle::route('/create'),
|
|
||||||
'edit' => Pages\EditStyle::route('/{record}/edit'),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Filament\Resources\StyleResource\Pages;
|
namespace App\Filament\Resources\Styles\Pages;
|
||||||
|
|
||||||
use App\Filament\Resources\StyleResource;
|
use App\Filament\Resources\Styles\StyleResource;
|
||||||
use Filament\Actions;
|
use Filament\Actions;
|
||||||
use Filament\Resources\Pages\CreateRecord;
|
use Filament\Resources\Pages\CreateRecord;
|
||||||
|
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Filament\Resources\StyleResource\Pages;
|
namespace App\Filament\Resources\Styles\Pages;
|
||||||
|
|
||||||
use App\Filament\Resources\StyleResource;
|
use App\Filament\Resources\Styles\StyleResource;
|
||||||
use Filament\Actions;
|
use Filament\Actions;
|
||||||
use Filament\Resources\Pages\EditRecord;
|
use Filament\Resources\Pages\EditRecord;
|
||||||
|
|
||||||
@@ -1,15 +1,17 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Filament\Resources\StyleResource\Pages;
|
namespace App\Filament\Resources\Styles\Pages;
|
||||||
|
|
||||||
use App\Filament\Resources\StyleResource;
|
use App\Filament\Resources\Styles\StyleResource;
|
||||||
use Filament\Actions;
|
use Filament\Actions;
|
||||||
use Filament\Resources\Pages\ListRecords;
|
use Filament\Resources\Pages\ListRecords;
|
||||||
|
use Filament\Tables\Concerns\CanReorderRecords;
|
||||||
use Illuminate\Contracts\Pagination\Paginator;
|
use Illuminate\Contracts\Pagination\Paginator;
|
||||||
use Illuminate\Database\Eloquent\Builder;
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
|
|
||||||
class ListStyles extends ListRecords
|
class ListStyles extends ListRecords
|
||||||
{
|
{
|
||||||
|
use CanReorderRecords;
|
||||||
protected static string $resource = StyleResource::class;
|
protected static string $resource = StyleResource::class;
|
||||||
|
|
||||||
protected function getHeaderActions(): array
|
protected function getHeaderActions(): array
|
||||||
154
app/Filament/Resources/Styles/StyleResource.php
Normal file
154
app/Filament/Resources/Styles/StyleResource.php
Normal file
@@ -0,0 +1,154 @@
|
|||||||
|
<?php
|
||||||
|
namespace App\Filament\Resources\Styles;
|
||||||
|
|
||||||
|
use BackedEnum;
|
||||||
|
use App\Filament\Resources\Styles\Pages;
|
||||||
|
use App\Models\Style;
|
||||||
|
use Filament\Schemas;
|
||||||
|
use Filament\Schemas\Schema;
|
||||||
|
use Filament\Resources\Resource;
|
||||||
|
use Filament\Tables;
|
||||||
|
use Filament\Tables\Table;
|
||||||
|
use Filament\Forms\Components\TextInput;
|
||||||
|
use Filament\Forms\Components\Textarea;
|
||||||
|
use Filament\Forms\Components\FileUpload;
|
||||||
|
use Filament\Forms\Components\Select;
|
||||||
|
use Filament\Tables\Columns\TextColumn;
|
||||||
|
use Filament\Tables\Columns\ImageColumn;
|
||||||
|
use Filament\Forms\Components\Toggle;
|
||||||
|
use Filament\Tables\Columns\IconColumn;
|
||||||
|
use Filament\Tables\Filters\SelectFilter;
|
||||||
|
use Filament\Schemas\Components\Grid;
|
||||||
|
use Filament\Schemas\Components\Tabs;
|
||||||
|
use Filament\Schemas\Components\Tabs\Tab;
|
||||||
|
use Filament\Actions\Action;
|
||||||
|
use Filament\Actions\BulkAction;
|
||||||
|
use Filament\Actions\BulkActionGroup;
|
||||||
|
use Filament\Actions\CreateAction;
|
||||||
|
use Filament\Actions\DeleteBulkAction;
|
||||||
|
use Filament\Actions\EditAction;
|
||||||
|
|
||||||
|
class StyleResource extends Resource
|
||||||
|
{
|
||||||
|
protected static ?string $model = Style::class;
|
||||||
|
|
||||||
|
protected static BackedEnum|string|null $navigationIcon = 'heroicon-o-rectangle-stack';
|
||||||
|
|
||||||
|
public static function form(Schema $schema): Schema
|
||||||
|
{
|
||||||
|
return $schema
|
||||||
|
->columns(1)
|
||||||
|
->components([
|
||||||
|
Tabs::make('Style Details')
|
||||||
|
->tabs([
|
||||||
|
Tab::make('General')
|
||||||
|
->components([
|
||||||
|
Grid::make(2)
|
||||||
|
->columnSpanFull()
|
||||||
|
->components([
|
||||||
|
TextInput::make('title')
|
||||||
|
->required()
|
||||||
|
->maxLength(255),
|
||||||
|
Toggle::make('enabled')
|
||||||
|
->default(true),
|
||||||
|
]),
|
||||||
|
Grid::make(2)
|
||||||
|
->columnSpanFull()
|
||||||
|
->components([
|
||||||
|
Textarea::make('prompt')
|
||||||
|
->required()
|
||||||
|
->rows(5),
|
||||||
|
Textarea::make('description')
|
||||||
|
->required()
|
||||||
|
->rows(5),
|
||||||
|
]),
|
||||||
|
Select::make('ai_model_id')
|
||||||
|
->relationship('aiModel', 'name')
|
||||||
|
->required(),
|
||||||
|
FileUpload::make('preview_image')
|
||||||
|
->disk('public')
|
||||||
|
->directory('style_previews')
|
||||||
|
->image()
|
||||||
|
->imageEditor()
|
||||||
|
->required(),
|
||||||
|
]),
|
||||||
|
Tab::make('Details')
|
||||||
|
->components([
|
||||||
|
TextInput::make('sort_order')
|
||||||
|
->numeric()
|
||||||
|
->default(0),
|
||||||
|
Textarea::make('parameters')
|
||||||
|
->nullable()
|
||||||
|
->rows(15),
|
||||||
|
]),
|
||||||
|
]),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function table(Table $table): Table
|
||||||
|
{
|
||||||
|
return $table
|
||||||
|
->defaultSort('sort_order')
|
||||||
|
->columns([
|
||||||
|
TextColumn::make('title')->searchable()->sortable(),
|
||||||
|
IconColumn::make('enabled')
|
||||||
|
->boolean(),
|
||||||
|
TextColumn::make('aiModel.name')->searchable()->sortable(),
|
||||||
|
ImageColumn::make('preview_image')->disk('public'),
|
||||||
|
TextColumn::make('sort_order')->sortable(),
|
||||||
|
])
|
||||||
|
->filters([
|
||||||
|
SelectFilter::make('ai_model')
|
||||||
|
->relationship('aiModel', 'name'),
|
||||||
|
])
|
||||||
|
->deferFilters(false)
|
||||||
|
->actions([
|
||||||
|
EditAction::make(),
|
||||||
|
Action::make('duplicate')
|
||||||
|
->label('Duplicate')
|
||||||
|
->icon('heroicon-o-document-duplicate')
|
||||||
|
->action(function (\App\Models\Style $record) {
|
||||||
|
$newStyle = $record->replicate();
|
||||||
|
$newStyle->title = $record->title . ' (Kopie)';
|
||||||
|
$newStyle->save();
|
||||||
|
return redirect()->to(\App\Filament\Resources\Styles\StyleResource::getUrl('edit', ['record' => $newStyle->id]));
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
->bulkActions([
|
||||||
|
BulkActionGroup::make([
|
||||||
|
DeleteBulkAction::make(),
|
||||||
|
BulkAction::make('enable')
|
||||||
|
->label('Enable Selected')
|
||||||
|
->icon('heroicon-o-check-circle')
|
||||||
|
->action(function (\Illuminate\Support\Collection $records) {
|
||||||
|
$records->each->update(['enabled' => true]);
|
||||||
|
}),
|
||||||
|
BulkAction::make('disable')
|
||||||
|
->label('Disable Selected')
|
||||||
|
->icon('heroicon-o-x-circle')
|
||||||
|
->action(function (\Illuminate\Support\Collection $records) {
|
||||||
|
$records->each->update(['enabled' => false]);
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
])
|
||||||
|
->emptyStateActions([
|
||||||
|
CreateAction::make(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getRelations(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
//
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getPages(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'index' => Pages\ListStyles::route('/'),
|
||||||
|
'create' => Pages\CreateStyle::route('/create'),
|
||||||
|
'edit' => Pages\EditStyle::route('/{record}/edit'),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Filament\Resources\UserResource\Pages;
|
namespace App\Filament\Resources\Users\Pages;
|
||||||
|
|
||||||
use App\Filament\Resources\UserResource;
|
use App\Filament\Resources\Users\UserResource;
|
||||||
use Filament\Actions;
|
use Filament\Actions;
|
||||||
use Filament\Resources\Pages\CreateRecord;
|
use Filament\Resources\Pages\CreateRecord;
|
||||||
|
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Filament\Resources\UserResource\Pages;
|
namespace App\Filament\Resources\Users\Pages;
|
||||||
|
|
||||||
use App\Filament\Resources\UserResource;
|
use App\Filament\Resources\Users\UserResource;
|
||||||
use Filament\Actions;
|
use Filament\Actions;
|
||||||
use Filament\Resources\Pages\EditRecord;
|
use Filament\Resources\Pages\EditRecord;
|
||||||
|
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Filament\Resources\UserResource\Pages;
|
namespace App\Filament\Resources\Users\Pages;
|
||||||
|
|
||||||
use App\Filament\Resources\UserResource;
|
use App\Filament\Resources\Users\UserResource;
|
||||||
use Filament\Actions;
|
use Filament\Actions;
|
||||||
use Filament\Resources\Pages\ListRecords;
|
use Filament\Resources\Pages\ListRecords;
|
||||||
use Illuminate\Contracts\Pagination\Paginator;
|
use Illuminate\Contracts\Pagination\Paginator;
|
||||||
@@ -1,45 +1,48 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Filament\Resources;
|
namespace App\Filament\Resources\Users;
|
||||||
|
|
||||||
use App\Filament\Resources\UserResource\Pages;
|
use UnitEnum;
|
||||||
use App\Filament\Resources\UserResource\RelationManagers;
|
use App\Filament\Resources\Users\Pages;
|
||||||
|
use App\Filament\Resources\Users\RelationManagers;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use Filament\Forms;
|
use Filament\Schemas;
|
||||||
use Filament\Forms\Form;
|
use Filament\Schemas\Schema;
|
||||||
use Filament\Resources\Resource;
|
use Filament\Resources\Resource;
|
||||||
use Filament\Tables;
|
use Filament\Tables;
|
||||||
use Filament\Tables\Table;
|
use Filament\Tables\Table;
|
||||||
use Illuminate\Database\Eloquent\Builder;
|
|
||||||
use Illuminate\Database\Eloquent\SoftDeletingScope;
|
use Illuminate\Database\Eloquent\SoftDeletingScope;
|
||||||
use Filament\Forms\Components\TextInput;
|
use Filament\Forms\Components\TextInput;
|
||||||
use Filament\Tables\Columns\TextColumn;
|
use Filament\Tables\Columns\TextColumn;
|
||||||
use Filament\Forms\Components\Select;
|
use Filament\Forms\Components\Select;
|
||||||
|
use Filament\Schemas\Components\Section;
|
||||||
|
use Filament\Forms\Components\Toggle;
|
||||||
|
use Filament\Actions\BulkActionGroup;
|
||||||
|
use Filament\Actions\CreateAction;
|
||||||
|
use Filament\Actions\DeleteBulkAction;
|
||||||
|
use Filament\Actions\EditAction;
|
||||||
|
|
||||||
class UserResource extends Resource
|
class UserResource extends Resource
|
||||||
{
|
{
|
||||||
protected static ?string $model = User::class;
|
protected static ?string $model = User::class;
|
||||||
|
|
||||||
protected static ?string $navigationIcon = 'heroicon-o-rectangle-stack';
|
protected static string|UnitEnum|null $navigationGroup = 'User Management';
|
||||||
protected static ?string $navigationGroup = 'User Management';
|
protected static ?string $navigationLabel = 'Users';
|
||||||
|
|
||||||
public static function form(Form $form): Form
|
public static function form(Schema $schema): Schema
|
||||||
{
|
{
|
||||||
return $form
|
return $schema
|
||||||
->schema([
|
->components([
|
||||||
Forms\Components\Section::make('User Details')
|
Section::make('User Details')
|
||||||
->schema([
|
->components([
|
||||||
TextInput::make('name')
|
TextInput::make('name')
|
||||||
->label(__('filament.resource.user.form.name'))
|
|
||||||
->required()
|
->required()
|
||||||
->maxLength(255),
|
->maxLength(255),
|
||||||
TextInput::make('email')
|
TextInput::make('email')
|
||||||
->label(__('filament.resource.user.form.email'))
|
|
||||||
->email()
|
->email()
|
||||||
->required()
|
->required()
|
||||||
->maxLength(255),
|
->maxLength(255),
|
||||||
TextInput::make('password')
|
TextInput::make('password')
|
||||||
->label(__('filament.resource.user.form.password'))
|
|
||||||
->password()
|
->password()
|
||||||
->dehydrateStateUsing(fn (string $state): string => bcrypt($state))
|
->dehydrateStateUsing(fn (string $state): string => bcrypt($state))
|
||||||
->dehydrated(fn (?string $state): bool => filled($state))
|
->dehydrated(fn (?string $state): bool => filled($state))
|
||||||
@@ -47,30 +50,28 @@ class UserResource extends Resource
|
|||||||
->maxLength(255),
|
->maxLength(255),
|
||||||
Select::make('role_id')
|
Select::make('role_id')
|
||||||
->relationship('role', 'name')
|
->relationship('role', 'name')
|
||||||
->label(__('filament.resource.user.form.role'))
|
|
||||||
->required(),
|
->required(),
|
||||||
])->columns(2),
|
])->columns(2)
|
||||||
|
->columnSpanFull(),
|
||||||
|
|
||||||
Forms\Components\Section::make('Preferences')
|
Section::make('Preferences')
|
||||||
->schema([
|
->components([
|
||||||
Forms\Components\Toggle::make('email_notifications_enabled')
|
Toggle::make('email_notifications_enabled')
|
||||||
->label(__('Email Notifications'))
|
|
||||||
->default(true),
|
->default(true),
|
||||||
Select::make('theme_preference')
|
Select::make('theme_preference')
|
||||||
->label(__('Theme'))
|
|
||||||
->options([
|
->options([
|
||||||
'light' => 'Light',
|
'light' => 'Light',
|
||||||
'dark' => 'Dark',
|
'dark' => 'Dark',
|
||||||
])
|
])
|
||||||
->default('light'),
|
->default('light'),
|
||||||
Select::make('locale')
|
Select::make('locale')
|
||||||
->label(__('Language'))
|
|
||||||
->options([
|
->options([
|
||||||
'en' => 'English',
|
'en' => 'English',
|
||||||
'de' => 'Deutsch',
|
'de' => 'Deutsch',
|
||||||
])
|
])
|
||||||
->default('en'),
|
->default('en'),
|
||||||
])->columns(2),
|
])->columns(2)
|
||||||
|
->columnSpanFull(),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -78,23 +79,23 @@ class UserResource extends Resource
|
|||||||
{
|
{
|
||||||
return $table
|
return $table
|
||||||
->columns([
|
->columns([
|
||||||
TextColumn::make('name')->label(__('filament.resource.user.table.name'))->searchable()->sortable(),
|
TextColumn::make('name')->searchable()->sortable(),
|
||||||
TextColumn::make('email')->label(__('filament.resource.user.table.email'))->searchable()->sortable(),
|
TextColumn::make('email')->searchable()->sortable(),
|
||||||
TextColumn::make('role.name')->label(__('filament.resource.user.table.role'))->searchable()->sortable(),
|
TextColumn::make('role.name')->searchable()->sortable(),
|
||||||
])
|
])
|
||||||
->filters([
|
->filters([
|
||||||
//
|
//
|
||||||
])
|
])
|
||||||
->actions([
|
->actions([
|
||||||
Tables\Actions\EditAction::make(),
|
EditAction::make(),
|
||||||
])
|
])
|
||||||
->bulkActions([
|
->bulkActions([
|
||||||
Tables\Actions\BulkActionGroup::make([
|
BulkActionGroup::make([
|
||||||
Tables\Actions\DeleteBulkAction::make(),
|
DeleteBulkAction::make(),
|
||||||
]),
|
]),
|
||||||
])
|
])
|
||||||
->emptyStateActions([
|
->emptyStateActions([
|
||||||
Tables\Actions\CreateAction::make(),
|
CreateAction::make(),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -112,5 +113,15 @@ class UserResource extends Resource
|
|||||||
'create' => Pages\CreateUser::route('/create'),
|
'create' => Pages\CreateUser::route('/create'),
|
||||||
'edit' => Pages\EditUser::route('/{record}/edit'),
|
'edit' => Pages\EditUser::route('/{record}/edit'),
|
||||||
];
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getNavigationGroup(): ?string
|
||||||
|
{
|
||||||
|
return __('filament.navigation.groups.user_management');
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getNavigationLabel(): string
|
||||||
|
{
|
||||||
|
return __('filament.navigation.user_roles');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
27
app/Filament/Widgets/AppStatsOverview.php
Normal file
27
app/Filament/Widgets/AppStatsOverview.php
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Filament\Widgets;
|
||||||
|
|
||||||
|
use Filament\Widgets\StatsOverviewWidget as BaseWidget;
|
||||||
|
use Filament\Widgets\StatsOverviewWidget\Stat;
|
||||||
|
use App\Models\AiModel;
|
||||||
|
use App\Models\ApiProvider;
|
||||||
|
use App\Models\Style;
|
||||||
|
|
||||||
|
class AppStatsOverview extends BaseWidget
|
||||||
|
{
|
||||||
|
protected function getCards(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
Stat::make('Anzahl AI Modelle', AiModel::count())
|
||||||
|
->icon('heroicon-o-server')
|
||||||
|
->url(route('filament.admin.resources.ai-models.index')),
|
||||||
|
Stat::make('Anzahl API-Provider', ApiProvider::count())
|
||||||
|
->icon('heroicon-o-cube')
|
||||||
|
->url(route('filament.admin.resources.api-providers.index')),
|
||||||
|
Stat::make('Anzahl Styles', Style::count())
|
||||||
|
->icon('heroicon-o-sparkles')
|
||||||
|
->url(route('filament.admin.resources.styles.index')),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
15
app/Http/Controllers/Admin/NavigationStateController.php
Normal file
15
app/Http/Controllers/Admin/NavigationStateController.php
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Admin;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
|
class NavigationStateController extends Controller
|
||||||
|
{
|
||||||
|
public function store(Request $request)
|
||||||
|
{
|
||||||
|
// Placeholder for storing navigation state
|
||||||
|
return response()->json(['message' => 'Navigation state stored.']);
|
||||||
|
}
|
||||||
|
}
|
||||||
16
app/Http/Controllers/Admin/PluginController.php
Normal file
16
app/Http/Controllers/Admin/PluginController.php
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Admin;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Models\Plugin;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
|
class PluginController extends Controller
|
||||||
|
{
|
||||||
|
public function index()
|
||||||
|
{
|
||||||
|
$plugins = Plugin::getAllPlugins();
|
||||||
|
return view('admin.plugins.index', compact('plugins'));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,24 +2,27 @@
|
|||||||
|
|
||||||
namespace App\Http\Controllers\Api;
|
namespace App\Http\Controllers\Api;
|
||||||
|
|
||||||
use App\Http\Controllers\Controller;
|
|
||||||
use Illuminate\Http\Request;
|
|
||||||
use App\Api\Plugins\PluginLoader;
|
use App\Api\Plugins\PluginLoader;
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
use App\Models\ApiProvider;
|
use App\Models\ApiProvider;
|
||||||
use App\Models\Style;
|
|
||||||
use App\Models\Image;
|
use App\Models\Image;
|
||||||
use Illuminate\Support\Facades\File;
|
use App\Models\Style;
|
||||||
|
use App\Settings\GeneralSettings;
|
||||||
use Carbon\Carbon;
|
use Carbon\Carbon;
|
||||||
use App\Models\Setting;
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\File;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
|
||||||
class ImageController extends Controller
|
class ImageController extends Controller
|
||||||
{
|
{
|
||||||
|
public function __construct(private GeneralSettings $settings) {}
|
||||||
|
|
||||||
public function index(Request $request)
|
public function index(Request $request)
|
||||||
{
|
{
|
||||||
$publicUploadsPath = public_path('storage/uploads');
|
$publicUploadsPath = public_path('storage/uploads');
|
||||||
|
|
||||||
// Ensure the directory exists
|
// Ensure the directory exists
|
||||||
if (!File::exists($publicUploadsPath)) {
|
if (! File::exists($publicUploadsPath)) {
|
||||||
File::makeDirectory($publicUploadsPath, 0755, true);
|
File::makeDirectory($publicUploadsPath, 0755, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -28,7 +31,7 @@ class ImageController extends Controller
|
|||||||
$diskImagePaths = [];
|
$diskImagePaths = [];
|
||||||
foreach ($diskFiles as $file) {
|
foreach ($diskFiles as $file) {
|
||||||
// Store path relative to public/storage/
|
// Store path relative to public/storage/
|
||||||
$diskImagePaths[] = 'uploads/' . $file->getFilename();
|
$diskImagePaths[] = 'uploads/'.$file->getFilename();
|
||||||
}
|
}
|
||||||
|
|
||||||
$dbImagePaths = Image::pluck('path')->toArray();
|
$dbImagePaths = Image::pluck('path')->toArray();
|
||||||
@@ -46,15 +49,20 @@ class ImageController extends Controller
|
|||||||
// Fetch images from the database after synchronization
|
// Fetch images from the database after synchronization
|
||||||
$query = Image::orderBy('updated_at', 'desc');
|
$query = Image::orderBy('updated_at', 'desc');
|
||||||
|
|
||||||
// If user is not authenticated, filter by is_public
|
// If user is not authenticated, filter by is_public, but also include their temporary images
|
||||||
if (!auth()->check()) {
|
if (! auth()->check()) {
|
||||||
$query->where('is_public', true);
|
$query->where(function ($q) {
|
||||||
|
$q->where('is_public', true)->orWhere('is_temp', true);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// If user is authenticated, show all their images
|
||||||
}
|
}
|
||||||
|
|
||||||
$newImageTimespanMinutes = Setting::where('key', 'new_image_timespan_minutes')->first()->value ?? 60; // Default to 60 minutes
|
$newImageTimespanMinutes = $this->settings->new_image_timespan_minutes;
|
||||||
|
|
||||||
$images = $query->get()->map(function ($image) use ($newImageTimespanMinutes) {
|
$images = $query->get()->map(function ($image) use ($newImageTimespanMinutes) {
|
||||||
$image->is_new = Carbon::parse($image->created_at)->diffInMinutes(Carbon::now()) <= $newImageTimespanMinutes;
|
$image->is_new = Carbon::parse($image->created_at)->diffInMinutes(Carbon::now()) <= $newImageTimespanMinutes;
|
||||||
|
|
||||||
return $image;
|
return $image;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -62,33 +70,34 @@ class ImageController extends Controller
|
|||||||
foreach ($images as $image) {
|
foreach ($images as $image) {
|
||||||
$formattedImages[] = [
|
$formattedImages[] = [
|
||||||
'image_id' => $image->id,
|
'image_id' => $image->id,
|
||||||
'path' => asset('storage/' . $image->path),
|
'path' => asset('storage/'.$image->path),
|
||||||
'name' => basename($image->path),
|
'name' => basename($image->path),
|
||||||
'is_temp' => (bool) $image->is_temp,
|
'is_temp' => (bool) $image->is_temp,
|
||||||
'is_public' => (bool) $image->is_public,
|
'is_public' => (bool) $image->is_public,
|
||||||
'is_new' => (bool) $image->is_new,
|
'is_new' => (bool) $image->is_new,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
return response()->json($formattedImages);
|
return response()->json($formattedImages);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function upload(Request $request)
|
public function upload(Request $request)
|
||||||
{
|
{
|
||||||
$request->validate([
|
$request->validate([
|
||||||
'image' => 'required|image|max:10240', // Max 10MB
|
'image' => 'required|image|mimes:jpeg,png,bmp,gif,webp|max:10240', // Max 10MB
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$file = $request->file('image');
|
$file = $request->file('image');
|
||||||
$fileName = uniqid() . '.' . $file->getClientOriginalExtension();
|
$fileName = uniqid().'.'.$file->getClientOriginalExtension();
|
||||||
$destinationPath = public_path('storage/uploads');
|
$destinationPath = public_path('storage/uploads');
|
||||||
|
|
||||||
// Ensure the directory exists
|
// Ensure the directory exists
|
||||||
if (!File::exists($destinationPath)) {
|
if (! File::exists($destinationPath)) {
|
||||||
File::makeDirectory($destinationPath, 0755, true);
|
File::makeDirectory($destinationPath, 0755, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
$file->move($destinationPath, $fileName);
|
$file->move($destinationPath, $fileName);
|
||||||
$relativePath = 'uploads/' . $fileName; // Path relative to public/storage/
|
$relativePath = 'uploads/'.$fileName; // Path relative to public/storage/
|
||||||
|
|
||||||
$image = Image::create([
|
$image = Image::create([
|
||||||
'path' => $relativePath,
|
'path' => $relativePath,
|
||||||
@@ -98,17 +107,29 @@ class ImageController extends Controller
|
|||||||
return response()->json([
|
return response()->json([
|
||||||
'message' => __('api.image_uploaded_successfully'),
|
'message' => __('api.image_uploaded_successfully'),
|
||||||
'image_id' => $image->id,
|
'image_id' => $image->id,
|
||||||
'path' => asset('storage/' . $relativePath),
|
'path' => asset('storage/'.$relativePath),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function styleChangeRequest(Request $request)
|
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
|
// Same-origin check
|
||||||
$appUrl = config('app.url');
|
$appUrl = config('app.url');
|
||||||
$referer = $request->headers->get('referer');
|
$referer = $request->headers->get('referer');
|
||||||
|
|
||||||
if ($referer && parse_url($referer, PHP_URL_HOST) !== parse_url($appUrl, PHP_URL_HOST)) {
|
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);
|
return response()->json(['error' => 'Unauthorized: Request must originate from the same domain.'], 403);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -122,70 +143,72 @@ class ImageController extends Controller
|
|||||||
|
|
||||||
if ($request->style_id) {
|
if ($request->style_id) {
|
||||||
$style = Style::with(['aiModel' => function ($query) {
|
$style = Style::with(['aiModel' => function ($query) {
|
||||||
$query->where('enabled', true)->with(['apiProviders' => function ($query) {
|
$query->where('enabled', true)->with('primaryApiProvider');
|
||||||
$query->where('enabled', true);
|
|
||||||
}]);
|
|
||||||
}])->find($request->style_id);
|
}])->find($request->style_id);
|
||||||
} else {
|
} else {
|
||||||
// Attempt to get default style from settings
|
// Attempt to get default style from settings
|
||||||
$defaultStyleSetting = \App\Models\Setting::where('key', 'default_style_id')->first();
|
$defaultStyleId = $this->settings->default_style_id;
|
||||||
if ($defaultStyleSetting && $defaultStyleSetting->value) {
|
if ($defaultStyleId) {
|
||||||
$style = Style::with(['aiModel' => function ($query) {
|
$style = Style::with(['aiModel' => function ($query) {
|
||||||
$query->where('enabled', true)->with(['apiProviders' => function ($query) {
|
$query->where('enabled', true)->with('primaryApiProvider');
|
||||||
$query->where('enabled', true);
|
}])->find($defaultStyleId);
|
||||||
}]);
|
|
||||||
}])->find($defaultStyleSetting->value);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!$style || !$style->aiModel || $style->aiModel->apiProviders->isEmpty()) {
|
if (! $style || ! $style->aiModel || ! $style->aiModel->primaryApiProvider) {
|
||||||
|
\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);
|
return response()->json(['error' => __('api.style_or_provider_not_found')], 404);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$apiProvider = $style->aiModel->apiProviders->first(); // Get the first enabled API provider
|
// Use the primary API provider for this AI model
|
||||||
if (!$apiProvider) {
|
$apiProvider = $style->aiModel->primaryApiProvider;
|
||||||
|
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);
|
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);
|
$plugin = PluginLoader::getPlugin($apiProvider->plugin, $apiProvider);
|
||||||
|
|
||||||
$result = $plugin->processImageStyleChange(
|
$result = $plugin->processImageStyleChange($image, $style);
|
||||||
public_path('storage/' . $image->path),
|
|
||||||
$style->prompt,
|
|
||||||
$style->aiModel->model_id,
|
|
||||||
$style->parameters
|
|
||||||
);
|
|
||||||
|
|
||||||
$base64Image = $result['base64Data'];
|
// Update the image model with the ComfyUI prompt_id and style_id
|
||||||
$decodedImage = base64_decode(preg_replace('#^data:image/\w+;base64, #i', '', $base64Image));
|
$image->comfyui_prompt_id = $result['prompt_id'];
|
||||||
|
$image->style_id = $style->id;
|
||||||
|
$image->save();
|
||||||
|
|
||||||
$newImageName = 'styled_' . uniqid() . '.png'; // Assuming PNG for now
|
// Return the prompt_id for WebSocket tracking
|
||||||
$newImagePathRelative = 'uploads/' . $newImageName; // Path relative to public/storage/
|
\Illuminate\Support\Facades\Log::info('Style change request completed', [
|
||||||
$newImageFullPath = public_path('storage/' . $newImagePathRelative); // Full path to save
|
'prompt_id' => $result['prompt_id'],
|
||||||
|
'image_uuid' => $image->uuid,
|
||||||
// Ensure the directory exists
|
|
||||||
if (!File::exists(public_path('storage/uploads'))) {
|
|
||||||
File::makeDirectory(public_path('storage/uploads'), 0755, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
File::put($newImageFullPath, $decodedImage); // Save using File facade
|
|
||||||
|
|
||||||
$newImage = Image::create([
|
|
||||||
'path' => $newImagePathRelative, // Store relative path
|
|
||||||
'original_image_id' => $image->id, // Link to original image
|
|
||||||
'style_id' => $style->id, // Link to applied style
|
|
||||||
'is_temp' => true, // Mark as temporary until user keeps it
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return response()->json([
|
return response()->json([
|
||||||
'message' => 'Style change successful',
|
'message' => 'Style change request sent.',
|
||||||
'styled_image' => [
|
'prompt_id' => $result['prompt_id'],
|
||||||
'id' => $newImage->id,
|
'image_uuid' => $image->uuid, // Pass image UUID for frontend tracking
|
||||||
'path' => asset('storage/' . $newImage->path),
|
'plugin' => $apiProvider->plugin, // Pass plugin name for frontend handling
|
||||||
'is_temp' => $newImage->is_temp,
|
|
||||||
],
|
|
||||||
]);
|
]);
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
|
\Illuminate\Support\Facades\Log::error('Error in styleChangeRequest', [
|
||||||
|
'error' => $e->getMessage(),
|
||||||
|
'trace' => $e->getTraceAsString(),
|
||||||
|
]);
|
||||||
|
|
||||||
return response()->json(['error' => $e->getMessage()], 500);
|
return response()->json(['error' => $e->getMessage()], 500);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -198,7 +221,7 @@ class ImageController extends Controller
|
|||||||
|
|
||||||
$image = Image::find($request->image_id);
|
$image = Image::find($request->image_id);
|
||||||
|
|
||||||
if (!$image) {
|
if (! $image) {
|
||||||
return response()->json(['error' => __('api.image_not_found')], 404);
|
return response()->json(['error' => __('api.image_not_found')], 404);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -212,8 +235,9 @@ class ImageController extends Controller
|
|||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
// Delete from the public/storage directory
|
// Delete from the public/storage directory
|
||||||
File::delete(public_path('storage/' . $image->path));
|
File::delete(public_path('storage/'.$image->path));
|
||||||
$image->delete();
|
$image->delete();
|
||||||
|
|
||||||
return response()->json(['message' => __('api.image_deleted_successfully')]);
|
return response()->json(['message' => __('api.image_deleted_successfully')]);
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
return response()->json(['error' => $e->getMessage()], 500);
|
return response()->json(['error' => $e->getMessage()], 500);
|
||||||
@@ -230,39 +254,166 @@ class ImageController extends Controller
|
|||||||
$image = Image::find($request->image_id);
|
$image = Image::find($request->image_id);
|
||||||
$apiProvider = ApiProvider::where('name', $request->api_provider_name)->first();
|
$apiProvider = ApiProvider::where('name', $request->api_provider_name)->first();
|
||||||
|
|
||||||
if (!$image || !$apiProvider) {
|
if (! $image || ! $apiProvider) {
|
||||||
return response()->json(['error' => __('api.image_or_provider_not_found')], 404);
|
return response()->json(['error' => __('api.image_or_provider_not_found')], 404);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$plugin = PluginLoader::getPlugin($apiProvider->name);
|
$plugin = PluginLoader::getPlugin($apiProvider->name);
|
||||||
$status = $plugin->getStatus($image->uuid); // Annahme: Image Model hat eine UUID
|
$status = $plugin->getStatus($image->uuid); // Annahme: Image Model hat eine UUID
|
||||||
|
|
||||||
return response()->json($status);
|
return response()->json($status);
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
return response()->json(['error' => $e->getMessage()], 500);
|
return response()->json(['error' => $e->getMessage()], 500);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getProgress(Request $request)
|
public function fetchStyledImage(string $promptId)
|
||||||
{
|
{
|
||||||
$request->validate([
|
Log::info('fetchStyledImage called.', ['prompt_id' => $promptId]);
|
||||||
'image_id' => 'required|exists:images,id',
|
|
||||||
'api_provider_name' => 'required|string',
|
|
||||||
]);
|
|
||||||
|
|
||||||
$image = Image::find($request->image_id);
|
|
||||||
$apiProvider = ApiProvider::where('name', $request->api_provider_name)->first();
|
|
||||||
|
|
||||||
if (!$image || !$apiProvider) {
|
|
||||||
return response()->json(['error' => __('api.image_or_provider_not_found')], 404);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$plugin = PluginLoader::getPlugin($apiProvider->name);
|
// Find the image associated with the prompt_id, eagerly loading relationships
|
||||||
$progress = $plugin->getProgress($image->uuid); // Annahme: Image Model hat eine UUID
|
$image = Image::with(['style.aiModel' => function ($query) {
|
||||||
return response()->json($progress);
|
$query->with('primaryApiProvider');
|
||||||
|
}])->where('comfyui_prompt_id', $promptId)->first();
|
||||||
|
|
||||||
|
if (! $image) {
|
||||||
|
Log::warning('fetchStyledImage: Image not found for prompt_id.', ['prompt_id' => $promptId]);
|
||||||
|
|
||||||
|
return response()->json(['error' => __('api.image_not_found')], 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
Log::info('fetchStyledImage: Image found.', ['image_id' => $image->id, 'image_uuid' => $image->uuid, 'comfyui_prompt_id' => $image->comfyui_prompt_id]);
|
||||||
|
|
||||||
|
// Get the style and API provider associated with the image
|
||||||
|
$style = $image->style;
|
||||||
|
if (! $style) {
|
||||||
|
Log::warning('fetchStyledImage: Style not found for image.', ['image_id' => $image->id]);
|
||||||
|
|
||||||
|
return response()->json(['error' => __('api.style_or_provider_not_found')], 404);
|
||||||
|
}
|
||||||
|
Log::info('fetchStyledImage: Style found.', ['style_id' => $style->id, 'style_name' => $style->title]);
|
||||||
|
|
||||||
|
if (! $style->aiModel) {
|
||||||
|
Log::warning('fetchStyledImage: AI Model not found for style.', ['style_id' => $style->id]);
|
||||||
|
|
||||||
|
return response()->json(['error' => __('api.style_or_provider_not_found')], 404);
|
||||||
|
}
|
||||||
|
Log::info('fetchStyledImage: AI Model found.', ['ai_model_id' => $style->aiModel->id, 'ai_model_name' => $style->aiModel->name]);
|
||||||
|
|
||||||
|
// Use the primary API provider for this AI model
|
||||||
|
$apiProvider = $style->aiModel->primaryApiProvider;
|
||||||
|
if (! $apiProvider) {
|
||||||
|
Log::warning('fetchStyledImage: No API Provider found for AI Model.', ['ai_model_id' => $style->aiModel->id]);
|
||||||
|
|
||||||
|
return response()->json(['error' => __('api.style_or_provider_not_found')], 404);
|
||||||
|
}
|
||||||
|
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]);
|
||||||
|
// Use the plugin to get the final image data (e.g., from ComfyUI's history/view)
|
||||||
|
$plugin = PluginLoader::getPlugin($apiProvider->plugin, $apiProvider);
|
||||||
|
$base64Image = $plugin->getStyledImage($promptId); // Use the new method
|
||||||
|
|
||||||
|
if (empty($base64Image)) {
|
||||||
|
Log::error('Received empty base64 image from plugin.', ['prompt_id' => $promptId]);
|
||||||
|
|
||||||
|
return response()->json(['error' => 'Received empty image data.'], 500);
|
||||||
|
}
|
||||||
|
|
||||||
|
Log::info('Base64 image received. Decoding and saving.');
|
||||||
|
$decodedImage = base64_decode(preg_replace('#^data:image/\w+;base64, #i', '', $base64Image));
|
||||||
|
|
||||||
|
$newImageName = 'styled_'.uniqid().'.png';
|
||||||
|
$newImagePathRelative = 'uploads/'.$newImageName;
|
||||||
|
$newImageFullPath = public_path('storage/'.$newImagePathRelative);
|
||||||
|
|
||||||
|
if (! File::exists(public_path('storage/uploads'))) {
|
||||||
|
File::makeDirectory(public_path('storage/uploads'), 0755, true);
|
||||||
|
Log::info('Created uploads directory.', ['path' => public_path('storage/uploads')]);
|
||||||
|
}
|
||||||
|
|
||||||
|
File::put($newImageFullPath, $decodedImage); // Save using File facade
|
||||||
|
Log::info('Image saved to disk.', ['path' => $newImageFullPath]);
|
||||||
|
|
||||||
|
$newImage = Image::create([
|
||||||
|
'path' => $newImagePathRelative, // Store relative path
|
||||||
|
'original_image_id' => $image->id,
|
||||||
|
'style_id' => $style->id,
|
||||||
|
'is_temp' => true,
|
||||||
|
]);
|
||||||
|
|
||||||
|
Log::info('New image record created in database.', ['image_id' => $newImage->id, 'path' => $newImage->path]);
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'message' => 'Styled image fetched successfully',
|
||||||
|
'styled_image' => [
|
||||||
|
'id' => $newImage->id,
|
||||||
|
'path' => asset('storage/'.$newImage->path),
|
||||||
|
'is_temp' => $newImage->is_temp,
|
||||||
|
],
|
||||||
|
]);
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
|
Log::error('Error in fetchStyledImage: '.$e->getMessage(), ['exception' => $e]);
|
||||||
|
|
||||||
return response()->json(['error' => $e->getMessage()], 500);
|
return response()->json(['error' => $e->getMessage()], 500);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
public function getComfyUiUrl(Request $request)
|
||||||
|
{
|
||||||
|
$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');
|
||||||
|
}])->find($styleId);
|
||||||
|
|
||||||
|
if ($style && $style->aiModel) {
|
||||||
|
// Use the primary API provider for this AI model
|
||||||
|
$apiProvider = $style->aiModel->primaryApiProvider;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 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');
|
||||||
|
}])->where('uuid', $imageUuid)->first();
|
||||||
|
|
||||||
|
if ($image && $image->style && $image->style->aiModel) {
|
||||||
|
// Use the primary API provider for this AI model
|
||||||
|
$apiProvider = $image->style->aiModel->primaryApiProvider;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 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
|
||||||
|
$defaultStyleId = $this->settings->default_style_id;
|
||||||
|
if ($defaultStyleId) {
|
||||||
|
$style = Style::with(['aiModel' => function ($query) {
|
||||||
|
$query->where('enabled', true)->with('primaryApiProvider');
|
||||||
|
}])->find($defaultStyleId);
|
||||||
|
|
||||||
|
if ($style && $style->aiModel) {
|
||||||
|
// Use the primary API provider for this AI model
|
||||||
|
$apiProvider = $style->aiModel->primaryApiProvider;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response()->json(['comfyui_url' => rtrim($apiProvider->api_url, '/')]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,21 +4,24 @@ namespace App\Http\Controllers\Api;
|
|||||||
|
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
use App\Models\Style;
|
use App\Models\Style;
|
||||||
use Illuminate\Http\Request;
|
use App\Settings\GeneralSettings;
|
||||||
|
|
||||||
class StyleController extends Controller
|
class StyleController extends Controller
|
||||||
{
|
{
|
||||||
|
public function __construct(private GeneralSettings $settings) {}
|
||||||
|
|
||||||
public function index()
|
public function index()
|
||||||
{
|
{
|
||||||
$styles = Style::with(['aiModel.apiProviders'])
|
$styles = Style::with(['aiModel.primaryApiProvider'])
|
||||||
->where('enabled', true)
|
->where('enabled', true)
|
||||||
->whereHas('aiModel', function ($query) {
|
->whereHas('aiModel', function ($query) {
|
||||||
$query->where('enabled', true);
|
$query->where('enabled', true);
|
||||||
$query->whereHas('apiProviders', function ($query) {
|
$query->whereHas('primaryApiProvider', function ($query) {
|
||||||
$query->where('enabled', true);
|
$query->where('enabled', true);
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
->get();
|
->get()
|
||||||
|
->sortBy('sort_order');
|
||||||
|
|
||||||
if ($styles->isEmpty()) {
|
if ($styles->isEmpty()) {
|
||||||
return response()->json(['message' => __('api.no_styles_available')], 404);
|
return response()->json(['message' => __('api.no_styles_available')], 404);
|
||||||
@@ -26,4 +29,18 @@ class StyleController extends Controller
|
|||||||
|
|
||||||
return response()->json($styles);
|
return response()->json($styles);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
public function getImageRefreshInterval()
|
||||||
|
{
|
||||||
|
return response()->json([
|
||||||
|
'interval' => $this->settings->image_refresh_interval / 1000,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getMaxNumberOfCopies()
|
||||||
|
{
|
||||||
|
return response()->json([
|
||||||
|
'max_copies' => $this->settings->max_number_of_copies,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
88
app/Http/Controllers/DownloadController.php
Normal file
88
app/Http/Controllers/DownloadController.php
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use Carbon\Carbon;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\File;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
|
||||||
|
class DownloadController extends Controller
|
||||||
|
{
|
||||||
|
public function downloadImage(Request $request)
|
||||||
|
{
|
||||||
|
$request->validate([
|
||||||
|
'image_path' => 'required|string',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$resolvedPath = $this->resolveImagePath($request->input('image_path'));
|
||||||
|
|
||||||
|
if (! $resolvedPath || ! File::exists($resolvedPath)) {
|
||||||
|
Log::error("DownloadController: Image file not found at {$resolvedPath}");
|
||||||
|
|
||||||
|
return response()->json(['error' => 'Image file not found.'], 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
$extension = strtolower(File::extension($resolvedPath) ?: 'jpg');
|
||||||
|
$downloadName = $this->buildDownloadName($extension);
|
||||||
|
$mimeType = File::mimeType($resolvedPath) ?: $this->getMimeType($extension);
|
||||||
|
|
||||||
|
try {
|
||||||
|
Log::info("DownloadController: Serving download for {$resolvedPath}");
|
||||||
|
|
||||||
|
return response()->download($resolvedPath, $downloadName, [
|
||||||
|
'Content-Type' => $mimeType,
|
||||||
|
'Content-Disposition' => 'attachment; filename="'.$downloadName.'"',
|
||||||
|
]);
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
Log::error('DownloadController: Error serving download: '.$e->getMessage());
|
||||||
|
|
||||||
|
return response()->json(['error' => 'Failed to serve download.'], 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function resolveImagePath(string $path): ?string
|
||||||
|
{
|
||||||
|
if (filter_var($path, FILTER_VALIDATE_URL)) {
|
||||||
|
$parsed = parse_url($path, PHP_URL_PATH);
|
||||||
|
|
||||||
|
return $parsed ? public_path(ltrim($parsed, '/')) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Str::startsWith($path, ['storage/', 'public/'])) {
|
||||||
|
return public_path(ltrim($path, '/'));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (File::exists($path)) {
|
||||||
|
return $path;
|
||||||
|
}
|
||||||
|
|
||||||
|
$candidate = public_path(ltrim($path, '/'));
|
||||||
|
|
||||||
|
return File::exists($candidate) ? $candidate : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function buildDownloadName(string $extension): string
|
||||||
|
{
|
||||||
|
$timestamp = Carbon::now()->format('Ymd_His');
|
||||||
|
|
||||||
|
return sprintf('stylegallery_%s.%s', $timestamp, $extension);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getMimeType(string $extension): string
|
||||||
|
{
|
||||||
|
$mimeTypes = [
|
||||||
|
'jpg' => 'image/jpeg',
|
||||||
|
'jpeg' => 'image/jpeg',
|
||||||
|
'png' => 'image/png',
|
||||||
|
'gif' => 'image/gif',
|
||||||
|
'webp' => 'image/webp',
|
||||||
|
'bmp' => 'image/bmp',
|
||||||
|
'svg' => 'image/svg+xml',
|
||||||
|
];
|
||||||
|
|
||||||
|
return $mimeTypes[strtolower($extension)] ?? 'application/octet-stream';
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,15 +2,16 @@
|
|||||||
|
|
||||||
namespace App\Http\Controllers;
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
use Illuminate\Http\Request;
|
|
||||||
use App\Models\Setting;
|
|
||||||
use App\Models\Image;
|
use App\Models\Image;
|
||||||
use Inertia\Inertia;
|
use App\Settings\GeneralSettings;
|
||||||
use Illuminate\Support\Facades\Lang;
|
|
||||||
use Carbon\Carbon;
|
use Carbon\Carbon;
|
||||||
|
use Illuminate\Support\Facades\Lang;
|
||||||
|
use Inertia\Inertia;
|
||||||
|
|
||||||
class HomeController extends Controller
|
class HomeController extends Controller
|
||||||
{
|
{
|
||||||
|
public function __construct(private GeneralSettings $settings) {}
|
||||||
|
|
||||||
public function index()
|
public function index()
|
||||||
{
|
{
|
||||||
$locale = app()->getLocale();
|
$locale = app()->getLocale();
|
||||||
@@ -18,12 +19,13 @@ class HomeController extends Controller
|
|||||||
Lang::get('api', [], $locale),
|
Lang::get('api', [], $locale),
|
||||||
Lang::get('settings', [], $locale)
|
Lang::get('settings', [], $locale)
|
||||||
);
|
);
|
||||||
$galleryHeading = Setting::where('key', 'gallery_heading')->first()->value ?? 'Style Gallery';
|
$galleryHeading = $this->settings->gallery_heading;
|
||||||
$newImageTimespanMinutes = Setting::where('key', 'new_image_timespan_minutes')->first()->value ?? 60; // Default to 60 minutes
|
$newImageTimespanMinutes = $this->settings->new_image_timespan_minutes;
|
||||||
|
|
||||||
$images = Image::all()->map(function ($image) use ($newImageTimespanMinutes) {
|
$images = Image::all()->map(function ($image) use ($newImageTimespanMinutes) {
|
||||||
$image->is_new = Carbon::parse($image->created_at)->diffInMinutes(Carbon::now()) <= $newImageTimespanMinutes;
|
$image->is_new = Carbon::parse($image->created_at)->diffInMinutes(Carbon::now()) <= $newImageTimespanMinutes;
|
||||||
$image->path = 'storage/' . $image->path;
|
$image->path = 'storage/'.$image->path;
|
||||||
|
|
||||||
return $image;
|
return $image;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -33,4 +35,4 @@ class HomeController extends Controller
|
|||||||
'images' => $images,
|
'images' => $images,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
52
app/Http/Controllers/PrintController.php
Normal file
52
app/Http/Controllers/PrintController.php
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Services\PrinterService;
|
||||||
|
use App\Settings\GeneralSettings;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
|
||||||
|
class PrintController extends Controller
|
||||||
|
{
|
||||||
|
public function __construct(private GeneralSettings $settings) {}
|
||||||
|
|
||||||
|
public function printImage(Request $request, PrinterService $printerService)
|
||||||
|
{
|
||||||
|
$request->validate([
|
||||||
|
'image_path' => 'required|string',
|
||||||
|
'quantity' => 'required|integer|min:1',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$imagePath = public_path(str_replace(url('/'), '', $request->input('image_path')));
|
||||||
|
$quantity = $request->input('quantity');
|
||||||
|
|
||||||
|
$printerName = $this->settings->selected_printer === '__custom__'
|
||||||
|
? $this->settings->custom_printer_address
|
||||||
|
: $this->settings->selected_printer;
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
$printSuccess = $printerService->printImage($imagePath, $printerName, $quantity);
|
||||||
|
|
||||||
|
if ($printSuccess) {
|
||||||
|
Log::info("PrintController: Successfully sent print command for {$imagePath} (x{$quantity}) to {$printerName}");
|
||||||
|
|
||||||
|
return response()->json(['message' => 'Print command sent successfully.']);
|
||||||
|
} 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,70 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Http;
|
|
||||||
|
|
||||||
use Illuminate\Foundation\Http\Kernel as HttpKernel;
|
|
||||||
|
|
||||||
class Kernel extends HttpKernel
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* The application's global HTTP middleware stack.
|
|
||||||
*
|
|
||||||
* These middleware are run during every request to your application.
|
|
||||||
*
|
|
||||||
* @var array<int, class-string|string>
|
|
||||||
*/
|
|
||||||
protected $middleware = [
|
|
||||||
// \App\Http\Middleware\TrustHosts::class,
|
|
||||||
\App\Http\Middleware\TrustProxies::class,
|
|
||||||
\Illuminate\Http\Middleware\HandleCors::class,
|
|
||||||
\App\Http\Middleware\PreventRequestsDuringMaintenance::class,
|
|
||||||
\Illuminate\Foundation\Http\Middleware\ValidatePostSize::class,
|
|
||||||
\App\Http\Middleware\TrimStrings::class,
|
|
||||||
\Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class,
|
|
||||||
];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The application's route middleware groups.
|
|
||||||
*
|
|
||||||
* @var array<string, array<int, class-string|string>>
|
|
||||||
*/
|
|
||||||
protected $middlewareGroups = [
|
|
||||||
'web' => [
|
|
||||||
\App\Http\Middleware\EncryptCookies::class,
|
|
||||||
\Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
|
|
||||||
\Illuminate\Session\Middleware\StartSession::class,
|
|
||||||
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
|
|
||||||
\App\Http\Middleware\SetContentSecurityPolicy::class,
|
|
||||||
\App\Http\Middleware\VerifyCsrfToken::class,
|
|
||||||
\Illuminate\Routing\Middleware\SubstituteBindings::class,
|
|
||||||
\App\Http\Middleware\HandleInertiaRequests::class,
|
|
||||||
\Illuminate\Http\Middleware\AddLinkHeadersForPreloadedAssets::class,
|
|
||||||
],
|
|
||||||
|
|
||||||
'api' => [
|
|
||||||
// \Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class,
|
|
||||||
\Illuminate\Routing\Middleware\ThrottleRequests::class.':api',
|
|
||||||
\Illuminate\Routing\Middleware\SubstituteBindings::class,
|
|
||||||
],
|
|
||||||
];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The application's middleware aliases.
|
|
||||||
*
|
|
||||||
* Aliases may be used to conveniently assign middleware to routes and groups.
|
|
||||||
*
|
|
||||||
* @var array<string, class-string|string>
|
|
||||||
*/
|
|
||||||
protected $middlewareAliases = [
|
|
||||||
'auth' => \App\Http\Middleware\Authenticate::class,
|
|
||||||
'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
|
|
||||||
'auth.session' => \Illuminate\Session\Middleware\AuthenticateSession::class,
|
|
||||||
'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class,
|
|
||||||
'can' => \Illuminate\Auth\Middleware\Authorize::class,
|
|
||||||
'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
|
|
||||||
'password.confirm' => \Illuminate\Auth\Middleware\RequirePassword::class,
|
|
||||||
'signed' => \App\Http\Middleware\ValidateSignature::class,
|
|
||||||
'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
|
|
||||||
'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
namespace App\Http\Middleware;
|
namespace App\Http\Middleware;
|
||||||
|
|
||||||
|
use App\Settings\GeneralSettings;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Inertia\Middleware;
|
use Inertia\Middleware;
|
||||||
|
|
||||||
@@ -17,7 +18,7 @@ class HandleInertiaRequests extends Middleware
|
|||||||
/**
|
/**
|
||||||
* Determine the current asset version.
|
* Determine the current asset version.
|
||||||
*/
|
*/
|
||||||
public function version(Request $request): string|null
|
public function version(Request $request): ?string
|
||||||
{
|
{
|
||||||
return parent::version($request);
|
return parent::version($request);
|
||||||
}
|
}
|
||||||
@@ -35,6 +36,7 @@ class HandleInertiaRequests extends Middleware
|
|||||||
'user' => $request->user(),
|
'user' => $request->user(),
|
||||||
],
|
],
|
||||||
'locale' => app()->getLocale(),
|
'locale' => app()->getLocale(),
|
||||||
|
'settings' => app(GeneralSettings::class)->toArray(),
|
||||||
'translations' => function () use ($request) {
|
'translations' => function () use ($request) {
|
||||||
$currentLocale = app()->getLocale(); // Store current locale
|
$currentLocale = app()->getLocale(); // Store current locale
|
||||||
$requestedLocale = $request->input('locale', $currentLocale);
|
$requestedLocale = $request->input('locale', $currentLocale);
|
||||||
@@ -47,9 +49,8 @@ class HandleInertiaRequests extends Middleware
|
|||||||
// Add other translation files as needed
|
// Add other translation files as needed
|
||||||
];
|
];
|
||||||
|
|
||||||
dd($lang); // <-- ADDED FOR DEBUGGING
|
|
||||||
|
|
||||||
app()->setLocale($currentLocale); // Revert to original locale
|
app()->setLocale($currentLocale); // Revert to original locale
|
||||||
|
|
||||||
return $lang;
|
return $lang;
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
121
app/Livewire/ListPlugins.php
Normal file
121
app/Livewire/ListPlugins.php
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Livewire;
|
||||||
|
|
||||||
|
use App\Api\Plugins\ApiPluginInterface;
|
||||||
|
use App\Models\Plugin;
|
||||||
|
use App\Models\ApiProvider;
|
||||||
|
use Filament\Forms\Concerns\InteractsWithForms;
|
||||||
|
use Filament\Forms\Contracts\HasForms;
|
||||||
|
use Filament\Notifications\Notification;
|
||||||
|
use Filament\Tables\Actions\Action;
|
||||||
|
use Filament\Tables\Columns\IconColumn;
|
||||||
|
use Filament\Tables\Columns\TextColumn;
|
||||||
|
use Filament\Tables\Concerns\InteractsWithTable;
|
||||||
|
use Filament\Tables\Contracts\HasTable;
|
||||||
|
use Filament\Tables\Table;
|
||||||
|
use Illuminate\Database\Eloquent\Collection;
|
||||||
|
use Illuminate\Support\Facades\File;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
use Livewire\Component;
|
||||||
|
use Exception;
|
||||||
|
|
||||||
|
class ListPlugins extends Component implements HasForms, HasTable
|
||||||
|
{
|
||||||
|
use InteractsWithForms;
|
||||||
|
use InteractsWithTable;
|
||||||
|
|
||||||
|
public function getPlugins(): Collection
|
||||||
|
{
|
||||||
|
$plugins = new Collection();
|
||||||
|
$path = app_path('Api/Plugins');
|
||||||
|
|
||||||
|
if (File::exists($path)) {
|
||||||
|
$files = File::files($path);
|
||||||
|
foreach ($files as $file) {
|
||||||
|
$filename = $file->getFilenameWithoutExtension();
|
||||||
|
if (in_array($filename, ['ApiPluginInterface', 'LoggablePlugin', 'PluginLoader'])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$class = 'App\Api\Plugins\\' . $filename;
|
||||||
|
|
||||||
|
if (class_exists($class) && in_array(ApiPluginInterface::class, class_implements($class))) {
|
||||||
|
try {
|
||||||
|
// Check if there's an ApiProvider for this plugin
|
||||||
|
$apiProvider = ApiProvider::where('plugin', $filename)->first();
|
||||||
|
$hasApiProvider = $apiProvider !== null;
|
||||||
|
|
||||||
|
// Get plugin information without instantiating the class
|
||||||
|
// This avoids issues with plugins requiring ApiProvider in constructor
|
||||||
|
$reflection = new \ReflectionClass($class);
|
||||||
|
$instance = $reflection->newInstanceWithoutConstructor();
|
||||||
|
|
||||||
|
$plugins->add(new Plugin([
|
||||||
|
'id' => $instance->getIdentifier(),
|
||||||
|
'name' => $instance->getName(),
|
||||||
|
'identifier' => $instance->getIdentifier(),
|
||||||
|
'enabled' => $hasApiProvider && $apiProvider->enabled,
|
||||||
|
'file_path' => $file->getPathname(),
|
||||||
|
'has_api_provider' => $hasApiProvider,
|
||||||
|
'configured' => $hasApiProvider
|
||||||
|
]));
|
||||||
|
} catch (Exception $e) {
|
||||||
|
Log::error('Error loading plugin for ListPlugins page.', ['plugin_file' => $file->getPathname(), 'error' => $e->getMessage()]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $plugins;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function table(Table $table): Table
|
||||||
|
{
|
||||||
|
return $table
|
||||||
|
->query(fn () => $this->getPlugins())
|
||||||
|
->columns([
|
||||||
|
TextColumn::make('name')
|
||||||
|
->label('Name')
|
||||||
|
->searchable()
|
||||||
|
->sortable(),
|
||||||
|
TextColumn::make('identifier')
|
||||||
|
->label('Identifier'),
|
||||||
|
IconColumn::make('enabled')
|
||||||
|
->label('Enabled')
|
||||||
|
->boolean(),
|
||||||
|
IconColumn::make('configured')
|
||||||
|
->label('Configured')
|
||||||
|
->boolean()
|
||||||
|
->tooltip(fn ($record) => $record->configured ? 'Has ApiProvider record' : 'No ApiProvider record'),
|
||||||
|
TextColumn::make('file_path')
|
||||||
|
->label('File Path')
|
||||||
|
->toggleable(isToggledHiddenByDefault: true),
|
||||||
|
])
|
||||||
|
->actions([
|
||||||
|
Action::make('toggle_enabled')
|
||||||
|
->label(fn($record) => $record->enabled ? 'Disable' : 'Enable')
|
||||||
|
->icon(fn($record) => $record->enabled ? 'heroicon-o-x-circle' :
|
||||||
|
'heroicon-o-check-circle')
|
||||||
|
->action(function ($record) {
|
||||||
|
// Action logic here
|
||||||
|
}),
|
||||||
|
Action::make('delete')
|
||||||
|
->label('Delete')
|
||||||
|
->icon('heroicon-o-trash')
|
||||||
|
->color('danger')
|
||||||
|
->requiresConfirmation()
|
||||||
|
->action(function ($record) {
|
||||||
|
// Action logic here
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function render()
|
||||||
|
{
|
||||||
|
return view('livewire.list-plugins');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function currentlyValidatingForm(\Filament\Forms\ComponentContainer|null $form): void
|
||||||
|
{
|
||||||
|
// No form validation needed for this component
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,10 +13,16 @@ class AiModel extends Model
|
|||||||
'name',
|
'name',
|
||||||
'model_id',
|
'model_id',
|
||||||
'model_type',
|
'model_type',
|
||||||
|
'parameters',
|
||||||
|
'api_provider_id',
|
||||||
];
|
];
|
||||||
|
|
||||||
public function apiProviders()
|
protected $casts = [
|
||||||
|
'parameters' => 'array',
|
||||||
|
];
|
||||||
|
|
||||||
|
public function primaryApiProvider()
|
||||||
{
|
{
|
||||||
return $this->belongsToMany(ApiProvider::class, 'ai_model_api_provider');
|
return $this->belongsTo(ApiProvider::class, 'api_provider_id');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -30,6 +30,6 @@ class ApiProvider extends Model
|
|||||||
|
|
||||||
public function aiModels()
|
public function aiModels()
|
||||||
{
|
{
|
||||||
return $this->belongsToMany(AiModel::class, 'ai_model_api_provider');
|
return $this->hasMany(AiModel::class, 'api_provider_id');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -17,5 +17,11 @@ class Image extends Model
|
|||||||
'style_id',
|
'style_id',
|
||||||
'is_temp',
|
'is_temp',
|
||||||
'is_public',
|
'is_public',
|
||||||
|
'comfyui_prompt_id',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
public function style()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Style::class);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
66
app/Models/Plugin.php
Normal file
66
app/Models/Plugin.php
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use App\Api\Plugins\ApiPluginInterface;
|
||||||
|
use App\Models\ApiProvider;
|
||||||
|
use Illuminate\Support\Facades\File;
|
||||||
|
use Exception;
|
||||||
|
use ReflectionClass;
|
||||||
|
|
||||||
|
class Plugin extends Model
|
||||||
|
{
|
||||||
|
protected $table = null; // No actual table
|
||||||
|
|
||||||
|
protected $guarded = []; // Allow mass assignment for all attributes
|
||||||
|
|
||||||
|
public $incrementing = false;
|
||||||
|
|
||||||
|
protected $keyType = 'string';
|
||||||
|
|
||||||
|
protected $primaryKey = 'id';
|
||||||
|
|
||||||
|
public static function getAllPlugins()
|
||||||
|
{
|
||||||
|
$plugins = [];
|
||||||
|
$path = app_path('Api/Plugins');
|
||||||
|
|
||||||
|
if (File::exists($path)) {
|
||||||
|
$files = File::files($path);
|
||||||
|
foreach ($files as $file) {
|
||||||
|
$filename = $file->getFilenameWithoutExtension();
|
||||||
|
if (in_array($filename, ['ApiPluginInterface', 'LoggablePlugin', 'PluginLoader'])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$class = 'App\Api\Plugins\\' . $filename;
|
||||||
|
|
||||||
|
if (class_exists($class) && in_array(ApiPluginInterface::class, class_implements($class))) {
|
||||||
|
try {
|
||||||
|
// Check if there's an ApiProvider for this plugin
|
||||||
|
$apiProvider = ApiProvider::where('plugin', $filename)->first();
|
||||||
|
$hasApiProvider = $apiProvider !== null;
|
||||||
|
|
||||||
|
// Get plugin information without instantiating the class
|
||||||
|
// This avoids issues with plugins requiring ApiProvider in constructor
|
||||||
|
$reflection = new ReflectionClass($class);
|
||||||
|
$instance = $reflection->newInstanceWithoutConstructor();
|
||||||
|
|
||||||
|
$plugins[] = new self([
|
||||||
|
'id' => $instance->getIdentifier(),
|
||||||
|
'name' => $instance->getName(),
|
||||||
|
'identifier' => $instance->getIdentifier(),
|
||||||
|
'enabled' => $hasApiProvider && $apiProvider && $apiProvider->enabled,
|
||||||
|
'file_path' => $file->getPathname(),
|
||||||
|
'has_api_provider' => $hasApiProvider,
|
||||||
|
'configured' => $hasApiProvider
|
||||||
|
]);
|
||||||
|
} catch (Exception $e) {
|
||||||
|
// Log error or handle as needed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return new \Illuminate\Database\Eloquent\Collection($plugins);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Models;
|
|
||||||
|
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
|
||||||
use Illuminate\Database\Eloquent\Model;
|
|
||||||
|
|
||||||
class Setting extends Model
|
|
||||||
{
|
|
||||||
use HasFactory;
|
|
||||||
|
|
||||||
protected $fillable = ['key', 'value'];
|
|
||||||
}
|
|
||||||
@@ -21,6 +21,7 @@ class Style extends Model
|
|||||||
|
|
||||||
protected $casts = [
|
protected $casts = [
|
||||||
'enabled' => 'boolean',
|
'enabled' => 'boolean',
|
||||||
|
'parameters' => 'array',
|
||||||
];
|
];
|
||||||
|
|
||||||
public function aiModel()
|
public function aiModel()
|
||||||
|
|||||||
@@ -3,12 +3,14 @@
|
|||||||
namespace App\Models;
|
namespace App\Models;
|
||||||
|
|
||||||
// use Illuminate\Contracts\Auth\MustVerifyEmail;
|
// use Illuminate\Contracts\Auth\MustVerifyEmail;
|
||||||
|
use Filament\Models\Contracts\FilamentUser;
|
||||||
|
use Filament\Panel;
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
use Illuminate\Foundation\Auth\User as Authenticatable;
|
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||||
use Illuminate\Notifications\Notifiable;
|
use Illuminate\Notifications\Notifiable;
|
||||||
use Laravel\Sanctum\HasApiTokens;
|
use Laravel\Sanctum\HasApiTokens;
|
||||||
|
|
||||||
class User extends Authenticatable
|
class User extends Authenticatable implements FilamentUser
|
||||||
{
|
{
|
||||||
use HasApiTokens, HasFactory, Notifiable;
|
use HasApiTokens, HasFactory, Notifiable;
|
||||||
|
|
||||||
@@ -50,4 +52,9 @@ class User extends Authenticatable
|
|||||||
protected $casts = [
|
protected $casts = [
|
||||||
'email_verified_at' => 'datetime',
|
'email_verified_at' => 'datetime',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
public function canAccessPanel(Panel $panel): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
namespace App\Providers\Filament;
|
namespace App\Providers\Filament;
|
||||||
|
|
||||||
|
use App\Filament\Resources\PluginResource;
|
||||||
use Filament\Http\Middleware\Authenticate;
|
use Filament\Http\Middleware\Authenticate;
|
||||||
use Filament\Http\Middleware\DisableBladeIconComponents;
|
use Filament\Http\Middleware\DisableBladeIconComponents;
|
||||||
use Filament\Http\Middleware\DispatchServingFilamentEvent;
|
use Filament\Http\Middleware\DispatchServingFilamentEvent;
|
||||||
@@ -16,10 +17,9 @@ use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken;
|
|||||||
use Illuminate\Routing\Middleware\SubstituteBindings;
|
use Illuminate\Routing\Middleware\SubstituteBindings;
|
||||||
use Illuminate\Session\Middleware\AuthenticateSession;
|
use Illuminate\Session\Middleware\AuthenticateSession;
|
||||||
use Illuminate\Session\Middleware\StartSession;
|
use Illuminate\Session\Middleware\StartSession;
|
||||||
use Illuminate\View\Middleware\ShareErrorsFromSession;
|
|
||||||
use App\Filament\Resources\StyleResource;
|
|
||||||
use App\Filament\Resources\SettingResource\Pages\Settings;
|
|
||||||
use Illuminate\Support\Facades\Auth;
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
use Illuminate\Support\HtmlString;
|
||||||
|
use Illuminate\View\Middleware\ShareErrorsFromSession;
|
||||||
|
|
||||||
class AdminPanelProvider extends PanelProvider
|
class AdminPanelProvider extends PanelProvider
|
||||||
{
|
{
|
||||||
@@ -30,20 +30,28 @@ class AdminPanelProvider extends PanelProvider
|
|||||||
->id('admin')
|
->id('admin')
|
||||||
->path('admin')
|
->path('admin')
|
||||||
->login()
|
->login()
|
||||||
|
->brandLogo(fn () => new HtmlString(
|
||||||
|
'<img src="'.asset('icon.png').'" alt="App Icon" style="height: 2.5rem; display: inline-block; vertical-align: middle; margin-right: 0.5rem;" />'.
|
||||||
|
'<span style="vertical-align: middle; font-weight: bold; font-size: 1.25rem;">'.config('app.name').'</span>'
|
||||||
|
))
|
||||||
->colors([
|
->colors([
|
||||||
'primary' => Color::Amber,
|
'primary' => Color::Amber,
|
||||||
])
|
])
|
||||||
->discoverResources(in: app_path('Filament/Resources'), for: 'App\\Filament\\Resources')
|
->discoverResources(in: app_path('Filament/Resources'), for: 'App\\Filament\\Resources')
|
||||||
|
->resources([
|
||||||
|
// PluginResource::class, // Removed as it's a custom page now
|
||||||
|
])
|
||||||
->discoverPages(in: app_path('Filament/Pages'), for: 'App\\Filament\\Pages')
|
->discoverPages(in: app_path('Filament/Pages'), for: 'App\\Filament\\Pages')
|
||||||
->pages([
|
->pages([
|
||||||
Pages\Dashboard::class,
|
Pages\Dashboard::class,
|
||||||
\App\Filament\Pages\InstallPluginPage::class,
|
\App\Filament\Pages\InstallPluginPage::class,
|
||||||
|
// \App\Filament\Pages\ListPlugins::class, // Removed duplicate entry
|
||||||
])
|
])
|
||||||
->discoverWidgets(in: app_path('Filament/Widgets'), for: 'App\\Filament\\Widgets')
|
->discoverWidgets(in: app_path('Filament/Widgets'), for: 'App\\Filament\\Widgets')
|
||||||
->widgets([
|
->widgets([
|
||||||
Widgets\AccountWidget::class,
|
Widgets\AccountWidget::class,
|
||||||
Widgets\FilamentInfoWidget::class,
|
Widgets\FilamentInfoWidget::class,
|
||||||
|
\App\Filament\Widgets\AppStatsOverview::class,
|
||||||
])
|
])
|
||||||
->middleware([
|
->middleware([
|
||||||
EncryptCookies::class,
|
EncryptCookies::class,
|
||||||
@@ -60,7 +68,7 @@ class AdminPanelProvider extends PanelProvider
|
|||||||
Authenticate::class,
|
Authenticate::class,
|
||||||
])
|
])
|
||||||
->plugins([
|
->plugins([
|
||||||
|
|
||||||
])
|
])
|
||||||
->profile();
|
->profile();
|
||||||
|
|
||||||
@@ -76,4 +84,3 @@ class AdminPanelProvider extends PanelProvider
|
|||||||
return $panel;
|
return $panel;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
119
app/Services/PrinterService.php
Normal file
119
app/Services/PrinterService.php
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
29
app/Settings/GeneralSettings.php
Normal file
29
app/Settings/GeneralSettings.php
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Settings;
|
||||||
|
|
||||||
|
use Spatie\LaravelSettings\Settings;
|
||||||
|
|
||||||
|
class GeneralSettings extends Settings
|
||||||
|
{
|
||||||
|
public string $gallery_heading = 'Style Gallery';
|
||||||
|
|
||||||
|
public int $new_image_timespan_minutes = 60;
|
||||||
|
|
||||||
|
public int $image_refresh_interval = 30_000;
|
||||||
|
|
||||||
|
public int $max_number_of_copies = 3;
|
||||||
|
|
||||||
|
public bool $show_print_button = true;
|
||||||
|
|
||||||
|
public ?string $selected_printer = null;
|
||||||
|
|
||||||
|
public ?string $custom_printer_address = null;
|
||||||
|
|
||||||
|
public ?int $default_style_id = null;
|
||||||
|
|
||||||
|
public static function group(): string
|
||||||
|
{
|
||||||
|
return 'general';
|
||||||
|
}
|
||||||
|
}
|
||||||
5328
bin/imagick/win64/ChangeLog.md
Normal file
5328
bin/imagick/win64/ChangeLog.md
Normal file
File diff suppressed because it is too large
Load Diff
106
bin/imagick/win64/LICENSE.txt
Normal file
106
bin/imagick/win64/LICENSE.txt
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
ImageMagick License
|
||||||
|
https://imagemagick.org/script/license.php
|
||||||
|
|
||||||
|
Before we get to the text of the license, lets just review what the license says in simple terms:
|
||||||
|
|
||||||
|
It allows you to:
|
||||||
|
|
||||||
|
* freely download and use ImageMagick software, in whole or in part, for personal, company internal, or commercial purposes;
|
||||||
|
* use ImageMagick software in packages or distributions that you create;
|
||||||
|
* link against a library under a different license;
|
||||||
|
* link code under a different license against a library under this license;
|
||||||
|
* merge code into a work under a different license;
|
||||||
|
* extend patent grants to any code using code under this license;
|
||||||
|
* and extend patent protection.
|
||||||
|
|
||||||
|
It forbids you to:
|
||||||
|
|
||||||
|
* redistribute any piece of ImageMagick-originated software without proper attribution;
|
||||||
|
* use any marks owned by ImageMagick Studio LLC in any way that might state or imply that ImageMagick Studio LLC endorses your distribution;
|
||||||
|
* use any marks owned by ImageMagick Studio LLC in any way that might state or imply that you created the ImageMagick software in question.
|
||||||
|
|
||||||
|
It requires you to:
|
||||||
|
|
||||||
|
* include a copy of the license in any redistribution you may make that includes ImageMagick software;
|
||||||
|
* provide clear attribution to ImageMagick Studio LLC for any distributions that include ImageMagick software.
|
||||||
|
|
||||||
|
It does not require you to:
|
||||||
|
|
||||||
|
* include the source of the ImageMagick software itself, or of any modifications you may have made to it, in any redistribution you may assemble that includes it;
|
||||||
|
* submit changes that you make to the software back to the ImageMagick Studio LLC (though such feedback is encouraged).
|
||||||
|
|
||||||
|
A few other clarifications include:
|
||||||
|
|
||||||
|
* ImageMagick is freely available without charge;
|
||||||
|
* you may include ImageMagick on a DVD as long as you comply with the terms of the license;
|
||||||
|
* you can give modified code away for free or sell it under the terms of the ImageMagick license or distribute the result under a different license, but you need to acknowledge the use of the ImageMagick software;
|
||||||
|
* the license is compatible with the GPL V3.
|
||||||
|
* when exporting the ImageMagick software, review its export classification.
|
||||||
|
|
||||||
|
Terms and Conditions for Use, Reproduction, and Distribution
|
||||||
|
|
||||||
|
The legally binding and authoritative terms and conditions for use, reproduction, and distribution of ImageMagick follow:
|
||||||
|
|
||||||
|
Copyright @ 1999 ImageMagick Studio LLC, a non-profit organization dedicated to making software imaging solutions freely available.
|
||||||
|
|
||||||
|
1. Definitions.
|
||||||
|
|
||||||
|
License shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document.
|
||||||
|
|
||||||
|
Licensor shall mean the copyright owner or entity authorized by the copyright owner that is granting the License.
|
||||||
|
|
||||||
|
Legal Entity shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, control means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity.
|
||||||
|
|
||||||
|
You (or Your) shall mean an individual or Legal Entity exercising permissions granted by this License.
|
||||||
|
|
||||||
|
Source form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files.
|
||||||
|
|
||||||
|
Object form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types.
|
||||||
|
|
||||||
|
Work shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below).
|
||||||
|
|
||||||
|
Derivative Works shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof.
|
||||||
|
|
||||||
|
Contribution shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as Not a Contribution.
|
||||||
|
|
||||||
|
Contributor shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work.
|
||||||
|
|
||||||
|
2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form.
|
||||||
|
|
||||||
|
3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed.
|
||||||
|
|
||||||
|
4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions:
|
||||||
|
|
||||||
|
* You must give any other recipients of the Work or Derivative Works a copy of this License; and
|
||||||
|
* You must cause any modified files to carry prominent notices stating that You changed the files; and
|
||||||
|
* You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and
|
||||||
|
* If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License.
|
||||||
|
You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License.
|
||||||
|
|
||||||
|
5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions.
|
||||||
|
|
||||||
|
6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file.
|
||||||
|
|
||||||
|
7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an AS IS BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License.
|
||||||
|
|
||||||
|
8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages.
|
||||||
|
|
||||||
|
9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability.
|
||||||
|
|
||||||
|
How to Apply the License to your Work
|
||||||
|
|
||||||
|
To apply the ImageMagick License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information (don't include the brackets). The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives.
|
||||||
|
|
||||||
|
Copyright [yyyy] [name of copyright owner]
|
||||||
|
|
||||||
|
Licensed under the ImageMagick License (the "License"); you may not use
|
||||||
|
this file except in compliance with the License. You may obtain a copy
|
||||||
|
of the License at
|
||||||
|
|
||||||
|
https://imagemagick.org/script/license.php
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
License for the specific language governing permissions and limitations
|
||||||
|
under the License.
|
||||||
7245
bin/imagick/win64/NOTICE.txt
Normal file
7245
bin/imagick/win64/NOTICE.txt
Normal file
File diff suppressed because it is too large
Load Diff
28
bin/imagick/win64/colors.xml
Normal file
28
bin/imagick/win64/colors.xml
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE colormap [
|
||||||
|
<!ELEMENT colormap (color)*>
|
||||||
|
<!ELEMENT color (#PCDATA)>
|
||||||
|
<!ATTLIST color name CDATA "0">
|
||||||
|
<!ATTLIST color color CDATA "rgb(0,0,0)">
|
||||||
|
<!ATTLIST color compliance CDATA "SVG">
|
||||||
|
]>
|
||||||
|
<!--
|
||||||
|
Associate a color name with its red, green, blue, and alpha intensities.
|
||||||
|
|
||||||
|
A number of methods and options require a color parameter. It is often
|
||||||
|
convenient to refer to a color by name (e.g. white) rather than by hex
|
||||||
|
value (e.g. #fff). This file maps a color name to its equivalent red,
|
||||||
|
green, blue, and alpha intensities (e.g. for white, red = 255, green =
|
||||||
|
255, blue = 255, and alpha = 0).
|
||||||
|
-->
|
||||||
|
<colormap>
|
||||||
|
<!-- <color name="none" color="rgba(0,0,0,0)" compliance="SVG, XPM"/> -->
|
||||||
|
<!-- <color name="black" color="rgb(0,0,0)" compliance="SVG, X11, XPM"/> -->
|
||||||
|
<!-- <color name="red" color="rgb(255,0,0)" compliance="SVG, X11, XPM"/> -->
|
||||||
|
<!-- <color name="magenta" color="rgb(255,0,255)" compliance="SVG, X11, XPM"/> -->
|
||||||
|
<!-- <color name="green" color="rgb(0,128,0)" compliance="SVG"/> -->
|
||||||
|
<!-- <color name="cyan" color="rgb(0,255,255)" compliance="SVG, X11, XPM"/> -->
|
||||||
|
<!-- <color name="blue" color="rgb(0,0,255)" compliance="SVG, X11, XPM"/> -->
|
||||||
|
<!-- <color name="yellow" color="rgb(255,255,0)" compliance="SVG, X11, XPM"/> -->
|
||||||
|
<!-- <color name="white" color="rgb(255,255,255)" compliance="SVG, X11"/> -->
|
||||||
|
</colormap>
|
||||||
28
bin/imagick/win64/configure.xml
Normal file
28
bin/imagick/win64/configure.xml
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE configuremap [
|
||||||
|
<!ELEMENT configuremap (configure)+>
|
||||||
|
<!ATTLIST configuremap xmlns CDATA #FIXED ''>
|
||||||
|
<!ELEMENT configure EMPTY>
|
||||||
|
<!ATTLIST configure xmlns CDATA #FIXED '' name NMTOKEN #REQUIRED
|
||||||
|
value CDATA #REQUIRED>
|
||||||
|
]>
|
||||||
|
<!--
|
||||||
|
ImageMagick build configuration.
|
||||||
|
-->
|
||||||
|
<configuremap>
|
||||||
|
<configure name="CC" value="VS2022"/>
|
||||||
|
<configure name="CHANNEL_MASK_DEPTH" value="64"/>
|
||||||
|
<configure name="COPYRIGHT" value="Copyright (C) 1999 ImageMagick Studio LLC"/>
|
||||||
|
<configure name="CXX" value="VS2022"/>
|
||||||
|
<configure name="DOCUMENTATION_PATH" value="unavailable"/>
|
||||||
|
<configure name="GIT_REVISION" value="83b6fc3:20250811" />
|
||||||
|
<configure name="LIB_VERSION_NUMBER" value="7,1,2,1"/>
|
||||||
|
<configure name="LIB_VERSION" value="0x712"/>
|
||||||
|
<configure name="NAME" value="ImageMagick"/>
|
||||||
|
<configure name="QuantumDepth" value="16"/>
|
||||||
|
<configure name="RELEASE_DATE" value="2025-08-12"/>
|
||||||
|
<configure name="TARGET_CPU" value="x64"/>
|
||||||
|
<configure name="TARGET_OS" value="Windows"/>
|
||||||
|
<configure name="VERSION" value="7.1.2"/>
|
||||||
|
<configure name="WEBSITE" value="https://imagemagick.org"/>
|
||||||
|
</configuremap>
|
||||||
101
bin/imagick/win64/delegates.xml
Normal file
101
bin/imagick/win64/delegates.xml
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE delegatemap [
|
||||||
|
<!ELEMENT delegatemap (delegate)+>
|
||||||
|
<!ATTLIST delegatemap xmlns CDATA #FIXED ''>
|
||||||
|
<!ELEMENT delegate EMPTY>
|
||||||
|
<!ATTLIST delegate xmlns CDATA #FIXED '' command CDATA #REQUIRED
|
||||||
|
decode NMTOKEN #IMPLIED encode NMTOKEN #IMPLIED mode NMTOKEN #IMPLIED
|
||||||
|
spawn NMTOKEN #IMPLIED stealth NMTOKEN #IMPLIED>
|
||||||
|
]>
|
||||||
|
<!--
|
||||||
|
Delegate command file.
|
||||||
|
|
||||||
|
Commands which specify
|
||||||
|
|
||||||
|
decode="in_format" encode="out_format"
|
||||||
|
|
||||||
|
specify the rules for converting from in_format to out_format. Use these
|
||||||
|
rules to translate directly between formats.
|
||||||
|
|
||||||
|
Commands which specify only
|
||||||
|
|
||||||
|
decode="in_format"
|
||||||
|
|
||||||
|
specify the rules for converting from in_format to some format that
|
||||||
|
ImageMagick automatically recognizes. Use these rules to decode formats.
|
||||||
|
|
||||||
|
Commands which specify only
|
||||||
|
|
||||||
|
encode="out_format"
|
||||||
|
|
||||||
|
specify the rules for an "encoder" which may accept any input format.
|
||||||
|
|
||||||
|
The substitution rules are as follows:
|
||||||
|
|
||||||
|
%a authentication passphrase
|
||||||
|
%b image file size in bytes
|
||||||
|
%g image geometry
|
||||||
|
%h image rows (height)
|
||||||
|
%i input image filename
|
||||||
|
%# input image signature
|
||||||
|
%m input image format
|
||||||
|
%o output image filename
|
||||||
|
%p page number
|
||||||
|
%q input image depth
|
||||||
|
%s scene number
|
||||||
|
%u unique temporary filename
|
||||||
|
%w image columns (width)
|
||||||
|
%x input image x resolution
|
||||||
|
%y input image y resolution
|
||||||
|
%Q input image compression quality
|
||||||
|
|
||||||
|
Set option delegate:bimodal=true to process bimodal delegates otherwise they
|
||||||
|
are ignored.
|
||||||
|
|
||||||
|
If stealth="True" the delegate is not listed in user requested
|
||||||
|
"-list delegate" listings. These are typically special internal delegates.
|
||||||
|
|
||||||
|
If spawn="True", ImageMagick does not wait for the delegate to finish, nor
|
||||||
|
will it read any output image.
|
||||||
|
-->
|
||||||
|
<delegatemap>
|
||||||
|
<delegate decode="bpg" command=""bpgdec" -b 16 -o "%o" "%i""/>
|
||||||
|
<delegate decode="png" encode="bpg" command=""bpgenc" -b 12 -q "%~" -o "%o" "%i""/>
|
||||||
|
<delegate decode="dng:decode" stealth="True" command="dcraw.exe -6 -W -O "%u.ppm" "%i""/>
|
||||||
|
<delegate decode="dot" command=""dot" -Tsvg "%i" -o "%o"" />
|
||||||
|
<delegate decode="dvi" command=""dvips" -q -o "%o" "%i""/>
|
||||||
|
<delegate decode="eps" encode="pdf" mode="bi" command=""@PSDelegate@" -q -dQUIET -dSAFER -dBATCH -dNOPAUSE -dNOPROMPT -dMaxBitmap=500000000 "-sDEVICE=pdfwrite" "-sOutputFile=%o" "-f%i""/>
|
||||||
|
<delegate decode="eps" encode="ps" mode="bi" command=""@PSDelegate@" -q -dQUIET -dSAFER -dBATCH -dNOPAUSE -dNOPROMPT -dMaxBitmap=500000000 -dAlignToPixels=0 -dGridFitTT=2 "-sDEVICE=ps2write" "-sOutputFile=%o" "-f%i""/>
|
||||||
|
<delegate decode="hpg" command=""hp2xx" -q -m eps -f "%o" "%i""/>
|
||||||
|
<delegate decode="hpgl" command=""hp2xx" -q -m eps -f "%o" "%i""/>
|
||||||
|
<delegate decode="htm" command=""html2ps" -U -o "%o" "%i""/>
|
||||||
|
<delegate decode="html" command=""html2ps" -U -o "%o" "%i""/>
|
||||||
|
<delegate decode="ilbm" command=""ilbmtoppm" "%i" > "%o""/>
|
||||||
|
<delegate decode="jpg" encode="lep" mode="encode" command=""lepton" "%i" "%o""/>
|
||||||
|
<delegate decode="jxr" command="cmd.exe /c (move "%i" "%i.jxr" >nul) & ("JXRDecApp.exe" -i "%i.jxr" -o "%o.tiff") & (move "%i.jxr" "%i" >nul) & (move "%o.tiff" "%o" >nul)"/>
|
||||||
|
<delegate decode="lep" mode="decode" command=""lepton" "%i" "%o""/>
|
||||||
|
<delegate decode="pcl:cmyk" stealth="True" command=""pcl6" -dQUIET -dSAFER -dBATCH -dNOPAUSE -dNOPROMPT -dMaxBitmap=500000000 -dAlignToPixels=0 -dGridFitTT=2 "-sDEVICE=pamcmyk32" -dTextAlphaBits=%u -dGraphicsAlphaBits=%u "-r%s" %s "-sOutputFile=%s" "%s""/>
|
||||||
|
<delegate decode="pcl:color" stealth="True" command=""pcl6" -dQUIET -dSAFER -dBATCH -dNOPAUSE -dNOPROMPT -dMaxBitmap=500000000 -dAlignToPixels=0 -dGridFitTT=2 "-sDEVICE=ppmraw" -dTextAlphaBits=%u -dGraphicsAlphaBits=%u "-r%s" %s "-sOutputFile=%s" "%s""/>
|
||||||
|
<delegate decode="pcl:mono" stealth="True" command=""pcl6" -dQUIET -dSAFER -dBATCH -dNOPAUSE -dNOPROMPT -dMaxBitmap=500000000 -dAlignToPixels=0 -dGridFitTT=2 "-sDEVICE=pbmraw" -dTextAlphaBits=%u -dGraphicsAlphaBits=%u "-r%s" %s "-sOutputFile=%s" "%s""/>
|
||||||
|
<delegate decode="pdf" encode="eps" mode="bi" command=""@PSDelegate@" -q -dQUIET -dSAFER -dBATCH -dNOPAUSE -dNOPROMPT -dMaxBitmap=500000000 -dAlignToPixels=0 -dGridFitTT=2 "-sDEVICE=eps2write" "-sPDFPassword=%a" "-sOutputFile=%o" "-f%i""/>
|
||||||
|
<delegate decode="pdf" encode="ps" mode="bi" command=""@PSDelegate@" -q -dQUIET -dSAFER -dBATCH -dNOPAUSE -dNOPROMPT -dMaxBitmap=500000000 -dAlignToPixels=0 -dGridFitTT=2 "-sDEVICE=ps2write" "-sPDFPassword=%a" "-sOutputFile=%o" "-f%i""/>
|
||||||
|
<delegate decode="pnm" encode="ilbm" mode="encode" command=""ppmtoilbm" -24if "%i" > "%o""/>
|
||||||
|
<delegate decode="tiff" encode="jxr" command="cmd.exe /c (move "%i" "%i.tiff" >nul) & ("JXREncApp.exe" -i "%i.tiff" -o "%o.jxr" -q %Q) & (move "%i.tiff" "%i" >nul) & (move "%o.jxr" "%o" >nul)"/>
|
||||||
|
<delegate decode="tiff" encode="wdp" command="cmd.exe /c (move "%i" "%i.tiff" >nul) & ("JXREncApp.exe" -i "%i.tiff" -o "%o.jxr" -q %Q) & (move "%i.tiff" "%i" >nul) & (move "%o.jxr" "%o" >nul)"/>
|
||||||
|
<delegate decode="ps:alpha" stealth="True" command=""@PSDelegate@" -q -dQUIET -dSAFER -dBATCH -dNOPAUSE -dNOPROMPT -dMaxBitmap=500000000 -dAlignToPixels=0 -dGridFitTT=2 "-sDEVICE=pngalpha" -dTextAlphaBits=%u -dGraphicsAlphaBits=%u "-r%s" %s "-sOutputFile=%s" "-f%s" "-f%s""/>
|
||||||
|
<delegate decode="ps:cmyk" stealth="True" command=""@PSDelegate@" -q -dQUIET -dSAFER -dBATCH -dNOPAUSE -dNOPROMPT -dMaxBitmap=500000000 -dAlignToPixels=0 -dGridFitTT=2 "-sDEVICE=pamcmyk32" -dTextAlphaBits=%u -dGraphicsAlphaBits=%u "-r%s" %s "-sOutputFile=%s" "-f%s" "-f%s""/>
|
||||||
|
<delegate decode="ps:color" stealth="True" command=""@PSDelegate@" -q -dQUIET -dSAFER -dBATCH -dNOPAUSE -dNOPROMPT -dMaxBitmap=500000000 -dAlignToPixels=0 -dGridFitTT=2 "-sDEVICE=png16m" -dTextAlphaBits=%u -dGraphicsAlphaBits=%u "-r%s" %s "-sOutputFile=%s" "-f%s" "-f%s""/>
|
||||||
|
<delegate decode="ps" encode="eps" mode="bi" command=""@PSDelegate@" -q -dQUIET -dSAFER -dBATCH -dNOPAUSE -dNOPROMPT -dMaxBitmap=500000000 -dAlignToPixels=0 -dGridFitTT=2 "-sDEVICE=eps2write" "-sOutputFile=%o" "-f%i""/>
|
||||||
|
<delegate decode="ps" encode="pdf" mode="bi" command=""@PSDelegate@" -q -dQUIET -dSAFER -dBATCH -dNOPAUSE -dNOPROMPT -dMaxBitmap=500000000 -dAlignToPixels=0 -dGridFitTT=2 "-sDEVICE=pdfwrite" "-sOutputFile=%o" "-f%i""/>
|
||||||
|
<delegate decode="ps:mono" stealth="True" command=""@PSDelegate@" -q -dQUIET -dSAFER -dBATCH -dNOPAUSE -dNOPROMPT -dMaxBitmap=500000000 -dAlignToPixels=0 -dGridFitTT=2 "-sDEVICE=pbmraw" -dTextAlphaBits=%u -dGraphicsAlphaBits=%u "-r%s" %s "-sOutputFile=%s" "-f%s" "-f%s""/>
|
||||||
|
<delegate decode="shtml" command=""html2ps" -U -o "%o" "%i""/>
|
||||||
|
<delegate decode="svg" command=""rsvg-convert" --dpi-x %x --dpi-y %y -o "%o" "%i""/>
|
||||||
|
<!-- Change export-filename to export-png for inkscape < 1.0 -->
|
||||||
|
<delegate decode="svg:decode" stealth="True" command=""inkscape" "%s" "--export-filename=%s" "--export-dpi=%s" "--export-background=%s" "--export-background-opacity=%s""/>
|
||||||
|
<delegate decode="wdp" command="cmd.exe /c (move "%i" "%i.jxr" >nul) & ("JXRDecApp.exe" -i "%i.jxr" -o "%o.pnm") & (move "%i.jxr" "%i" >nul) & (move "%o.pnm" "%o" >nul)"/>
|
||||||
|
<delegate decode="xps:cmyk" stealth="True" command=""gxps" -dQUIET -dSAFER -dBATCH -dNOPAUSE -dNOPROMPT -dMaxBitmap=500000000 -dAlignToPixels=0 -dGridFitTT=2 "-sDEVICE=bmpsep8" -dTextAlphaBits=%u -dGraphicsAlphaBits=%u "-r%s" %s "-sOutputFile=%s" "%s""/>
|
||||||
|
<delegate decode="xps:color" stealth="True" command=""gxps" -dQUIET -dSAFER -dBATCH -dNOPAUSE -dNOPROMPT -dMaxBitmap=500000000 -dAlignToPixels=0 -dGridFitTT=2 "-sDEVICE=ppmraw" -dTextAlphaBits=%u -dGraphicsAlphaBits=%u "-r%s" %s "-sOutputFile=%s" "%s""/>
|
||||||
|
<delegate decode="xps:mono" stealth="True" command=""gxps" -dQUIET -dSAFER -dBATCH -dNOPAUSE -dNOPROMPT -dMaxBitmap=500000000 -dAlignToPixels=0 -dGridFitTT=2 "-sDEVICE=pbmraw" -dTextAlphaBits=%u -dGraphicsAlphaBits=%u "-r%s" %s "-sOutputFile=%s" "%s""/>
|
||||||
|
<delegate decode="video:decode" command=""ffmpeg" -nostdin -loglevel error -i "%s" -an -f rawvideo -y %s "%s""/>
|
||||||
|
<delegate encode="video:encode" stealth="True" command=""ffmpeg" -nostdin -loglevel error -i "%s%%d.%s" %s "%s.%s""/>
|
||||||
|
</delegatemap>
|
||||||
1728
bin/imagick/win64/english.xml
Normal file
1728
bin/imagick/win64/english.xml
Normal file
File diff suppressed because it is too large
Load Diff
48
bin/imagick/win64/locale.xml
Normal file
48
bin/imagick/win64/locale.xml
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE localemap [
|
||||||
|
<!ELEMENT localemap (include)+>
|
||||||
|
<!ATTLIST localemap xmlns CDATA #FIXED ''>
|
||||||
|
<!ELEMENT include EMPTY>
|
||||||
|
<!ATTLIST include xmlns CDATA #FIXED '' file NMTOKEN #REQUIRED
|
||||||
|
locale NMTOKEN #REQUIRED>
|
||||||
|
]>
|
||||||
|
<localemap>
|
||||||
|
<include locale="no_NO.ISO-8859-1" file="bokmal.xml"/>
|
||||||
|
<include locale="ca_ES.ISO-8859-1" file="catalan.xml"/>
|
||||||
|
<include locale="hr_HR.ISO-8859-2" file="croatian.xml"/>
|
||||||
|
<include locale="cs_CZ.ISO-8859-2" file="czech.xml"/>
|
||||||
|
<include locale="da_DK.ISO-8859-1" file="danish.xml"/>
|
||||||
|
<include locale="de_DE.ISO-8859-1" file="deutsch.xml"/>
|
||||||
|
<include locale="nl_NL.ISO-8859-1" file="dutch.xml"/>
|
||||||
|
<include locale="C" file="english.xml"/>
|
||||||
|
<include locale="et_EE.ISO-8859-1" file="estonian.xml"/>
|
||||||
|
<include locale="fi_FI.ISO-8859-1" file="finnish.xml"/>
|
||||||
|
<include locale="fr_FR.ISO-8859-1" file="francais.xml"/>
|
||||||
|
<include locale="fr_FR.ISO-8859-1" file="francais.xml"/>
|
||||||
|
<include locale="fr_FR.UTF-8" file="francais.xml"/>
|
||||||
|
<include locale="gl_ES.ISO-8859-1" file="galego.xml"/>
|
||||||
|
<include locale="gl_ES.ISO-8859-1" file="galician.xml"/>
|
||||||
|
<include locale="de_DE.ISO-8859-1" file="german.xml"/>
|
||||||
|
<include locale="el_GR.ISO-8859-7" file="greek.xml"/>
|
||||||
|
<include locale="en_US.UTF-8" file="english.xml"/>
|
||||||
|
<include locale="iw_IL.ISO-8859-8" file="hebrew.xml"/>
|
||||||
|
<include locale="hr_HR.ISO-8859-2" file="hrvatski.xml"/>
|
||||||
|
<include locale="hu_HU.ISO-8859-2" file="hungarian.xml"/>
|
||||||
|
<include locale="is_IS.ISO-8859-1" file="icelandic.xml"/>
|
||||||
|
<include locale="it_IT.ISO-8859-1" file="italian.xml"/>
|
||||||
|
<include locale="ja_JP.eucJP" file="japanese.xml"/>
|
||||||
|
<include locale="ko_KR.eucKR" file="korean.xml"/>
|
||||||
|
<include locale="lt_LT.ISO-8859-13" file="lithuanian.xml"/>
|
||||||
|
<include locale="no_NO.ISO-8859-1" file="norwegian.xml"/>
|
||||||
|
<include locale="nn_NO.ISO-8859-1" file="nynorsk.xml"/>
|
||||||
|
<include locale="pl_PL.ISO-8859-2" file="polish.xml"/>
|
||||||
|
<include locale="pt_PT.ISO-8859-1" file="portuguese.xml"/>
|
||||||
|
<include locale="ro_RO.ISO-8859-2" file="romanian.xml"/>
|
||||||
|
<include locale="ru_RU.ISO-8859-5" file="russian.xml"/>
|
||||||
|
<include locale="sk_SK.ISO-8859-2" file="slovak.xml"/>
|
||||||
|
<include locale="sl_SI.ISO-8859-2" file="slovene.xml"/>
|
||||||
|
<include locale="es_ES.ISO-8859-1" file="spanish.xml"/>
|
||||||
|
<include locale="sv_SE.ISO-8859-1" file="swedish.xml"/>
|
||||||
|
<include locale="th_TH.TIS-620" file="thai.xml"/>
|
||||||
|
<include locale="tr_TR.ISO-8859-9" file="turkish.xml"/>
|
||||||
|
</localemap>
|
||||||
79
bin/imagick/win64/log.xml
Normal file
79
bin/imagick/win64/log.xml
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE logmap [
|
||||||
|
<!ELEMENT logmap (log)+>
|
||||||
|
<!ELEMENT log (#PCDATA)>
|
||||||
|
<!ATTLIST log events CDATA #IMPLIED>
|
||||||
|
<!ATTLIST log output CDATA #IMPLIED>
|
||||||
|
<!ATTLIST log filename CDATA #IMPLIED>
|
||||||
|
<!ATTLIST log generations CDATA #IMPLIED>
|
||||||
|
<!ATTLIST log limit CDATA #IMPLIED>
|
||||||
|
<!ATTLIST log format CDATA #IMPLIED>
|
||||||
|
]>
|
||||||
|
<!--
|
||||||
|
Configure ImageMagick logger.
|
||||||
|
|
||||||
|
Choose from one or more these events separated by a comma:
|
||||||
|
all
|
||||||
|
accelerate
|
||||||
|
annotate
|
||||||
|
blob
|
||||||
|
cache
|
||||||
|
coder
|
||||||
|
command
|
||||||
|
configure
|
||||||
|
deprecate
|
||||||
|
draw
|
||||||
|
exception
|
||||||
|
locale
|
||||||
|
module
|
||||||
|
none
|
||||||
|
pixel
|
||||||
|
policy
|
||||||
|
resource
|
||||||
|
trace
|
||||||
|
transform
|
||||||
|
user
|
||||||
|
wand
|
||||||
|
x11
|
||||||
|
|
||||||
|
Choose one output handler:
|
||||||
|
console
|
||||||
|
debug
|
||||||
|
event
|
||||||
|
file
|
||||||
|
none
|
||||||
|
stderr
|
||||||
|
stdout
|
||||||
|
|
||||||
|
When output is to a file, specify the filename. Embed %g in the filename to
|
||||||
|
support log generations. Generations is the number of log files to retain.
|
||||||
|
Limit is the number of logging events before generating a new log generation.
|
||||||
|
|
||||||
|
The format of the log is defined by embedding special format characters:
|
||||||
|
|
||||||
|
%c client
|
||||||
|
%d domain
|
||||||
|
%e event
|
||||||
|
%f function
|
||||||
|
%i thread id
|
||||||
|
%l line
|
||||||
|
%m module
|
||||||
|
%n log name
|
||||||
|
%p process id
|
||||||
|
%r real CPU time
|
||||||
|
%t wall clock time
|
||||||
|
%u user CPU time
|
||||||
|
%v version
|
||||||
|
%% percent sign
|
||||||
|
\n newline
|
||||||
|
\r carriage return
|
||||||
|
xml
|
||||||
|
-->
|
||||||
|
<logmap>
|
||||||
|
<log events="None"/>
|
||||||
|
<log output="console"/>
|
||||||
|
<log filename="Magick-%g.log"/>
|
||||||
|
<log generations="3"/>
|
||||||
|
<log limit="2GiB"/>
|
||||||
|
<log format="%t %r %u %v %d %c[%p]: %m/%f/%l/%d\n %e"/>
|
||||||
|
</logmap>
|
||||||
BIN
bin/imagick/win64/magick.exe
Normal file
BIN
bin/imagick/win64/magick.exe
Normal file
Binary file not shown.
1157
bin/imagick/win64/mime.xml
Normal file
1157
bin/imagick/win64/mime.xml
Normal file
File diff suppressed because it is too large
Load Diff
153
bin/imagick/win64/policy.xml
Normal file
153
bin/imagick/win64/policy.xml
Normal file
@@ -0,0 +1,153 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE policymap [
|
||||||
|
<!ELEMENT policymap (policy)*>
|
||||||
|
<!ATTLIST policymap xmlns CDATA #FIXED "">
|
||||||
|
<!ELEMENT policy EMPTY>
|
||||||
|
<!ATTLIST policy xmlns CDATA #FIXED "">
|
||||||
|
<!ATTLIST policy domain NMTOKEN #REQUIRED>
|
||||||
|
<!ATTLIST policy name NMTOKEN #IMPLIED>
|
||||||
|
<!ATTLIST policy pattern CDATA #IMPLIED>
|
||||||
|
<!ATTLIST policy rights NMTOKEN #IMPLIED>
|
||||||
|
<!ATTLIST policy stealth NMTOKEN #IMPLIED>
|
||||||
|
<!ATTLIST policy value CDATA #IMPLIED>
|
||||||
|
]>
|
||||||
|
<!--
|
||||||
|
Creating a security policy that fits your specific local environment
|
||||||
|
before making use of ImageMagick is highly advised. You can find guidance on
|
||||||
|
setting up this policy at https://imagemagick.org/script/security-policy.php,
|
||||||
|
and it's important to verify your policy using the validation tool located
|
||||||
|
at https://imagemagick-secevaluator.doyensec.com/.
|
||||||
|
|
||||||
|
|
||||||
|
Open ImageMagick security policy:
|
||||||
|
|
||||||
|
The default policy for ImageMagick installations is the open security
|
||||||
|
policy. This policy is designed for usage in secure settings like those
|
||||||
|
protected by firewalls or within Docker containers. Within this framework,
|
||||||
|
ImageMagick enjoys broad access to resources and functionalities. This policy
|
||||||
|
provides convenient and adaptable options for image manipulation. However,
|
||||||
|
it's important to note that it might present security vulnerabilities in
|
||||||
|
less regulated conditions. Thus, organizations should thoroughly assess
|
||||||
|
the appropriateness of the open policy according to their particular use
|
||||||
|
case and security prerequisites.
|
||||||
|
|
||||||
|
ImageMagick security policies in a nutshell:
|
||||||
|
|
||||||
|
Domains include system, delegate, coder, filter, module, path, or resource.
|
||||||
|
|
||||||
|
Rights include none, read, write, execute and all. Use | to combine them,
|
||||||
|
for example: "read | write" to permit read from, or write to, a path.
|
||||||
|
|
||||||
|
Use a glob expression as a pattern.
|
||||||
|
|
||||||
|
Suppose we do not want users to process MPEG video images, use this policy:
|
||||||
|
|
||||||
|
<policy domain="delegate" rights="none" pattern="mpeg:decode" />
|
||||||
|
|
||||||
|
Here we do not want users reading images from HTTP:
|
||||||
|
|
||||||
|
<policy domain="coder" rights="none" pattern="HTTP" />
|
||||||
|
|
||||||
|
The /repository file system is restricted to read only. We use a glob
|
||||||
|
expression to match all paths that start with /repository:
|
||||||
|
|
||||||
|
<policy domain="path" rights="read" pattern="/repository/*" />
|
||||||
|
|
||||||
|
Prevent users from executing any image filters:
|
||||||
|
|
||||||
|
<policy domain="filter" rights="none" pattern="*" />
|
||||||
|
|
||||||
|
Cache large images to disk rather than memory:
|
||||||
|
|
||||||
|
<policy domain="resource" name="area" value="1GP"/>
|
||||||
|
|
||||||
|
Use the default system font unless overridden by the application:
|
||||||
|
|
||||||
|
<policy domain="system" name="font" value="/usr/share/fonts/favorite.ttf"/>
|
||||||
|
|
||||||
|
Define arguments for the memory, map, area, width, height and disk resources
|
||||||
|
with SI prefixes (.e.g 100MB). In addition, resource policies are maximums
|
||||||
|
for each instance of ImageMagick (e.g. policy memory limit 1GB, -limit 2GB
|
||||||
|
exceeds policy maximum so memory limit is 1GB).
|
||||||
|
|
||||||
|
Rules are processed in order. Here we want to restrict ImageMagick to only
|
||||||
|
read or write a small subset of proven web-safe image types:
|
||||||
|
|
||||||
|
<policy domain="delegate" rights="none" pattern="*" />
|
||||||
|
<policy domain="filter" rights="none" pattern="*" />
|
||||||
|
<policy domain="coder" rights="none" pattern="*" />
|
||||||
|
<policy domain="coder" rights="read|write" pattern="{GIF,JPEG,PNG,WEBP}" />
|
||||||
|
|
||||||
|
See https://imagemagick.org/script/security-policy.php for a deeper
|
||||||
|
understanding of ImageMagick security policies.
|
||||||
|
-->
|
||||||
|
<policymap>
|
||||||
|
<policy domain="Undefined" rights="none"/>
|
||||||
|
<!-- Set maximum parallel threads. -->
|
||||||
|
<!-- <policy domain="resource" name="thread" value="2"/> -->
|
||||||
|
<!-- Set maximum time to live in seconds or mnemonics, e.g. "2 minutes". When
|
||||||
|
this limit is exceeded, an exception is thrown and processing stops. -->
|
||||||
|
<!-- <policy domain="resource" name="time" value="120"/> -->
|
||||||
|
<!-- Set maximum number of open pixel cache files. When this limit is
|
||||||
|
exceeded, any subsequent pixels cached to disk are closed and reopened
|
||||||
|
on demand. -->
|
||||||
|
<!-- <policy domain="resource" name="file" value="768"/> -->
|
||||||
|
<!-- Set maximum amount of memory in bytes to allocate for the pixel cache
|
||||||
|
from the heap. When this limit is exceeded, the image pixels are cached
|
||||||
|
to memory-mapped disk. -->
|
||||||
|
<!-- <policy domain="resource" name="memory" value="256MiB"/> -->
|
||||||
|
<!-- Set maximum amount of memory map in bytes to allocate for the pixel
|
||||||
|
cache. When this limit is exceeded, the image pixels are cached to
|
||||||
|
disk. -->
|
||||||
|
<!-- <policy domain="resource" name="map" value="512MiB"/> -->
|
||||||
|
<!-- Set the maximum width * height of an image that can reside in the pixel
|
||||||
|
cache memory. Images that exceed the area limit are cached to disk. -->
|
||||||
|
<!-- <policy domain="resource" name="area" value="16KP"/> -->
|
||||||
|
<!-- Set maximum amount of disk space in bytes permitted for use by the pixel
|
||||||
|
cache. When this limit is exceeded, the pixel cache is not be created
|
||||||
|
and an exception is thrown. -->
|
||||||
|
<!-- <policy domain="resource" name="disk" value="1GiB"/> -->
|
||||||
|
<!-- Set the maximum length of an image sequence. When this limit is
|
||||||
|
exceeded, an exception is thrown. -->
|
||||||
|
<!-- <policy domain="resource" name="list-length" value="32"/> -->
|
||||||
|
<!-- Set the maximum width of an image. When this limit is exceeded, an
|
||||||
|
exception is thrown. -->
|
||||||
|
<!-- <policy domain="resource" name="width" value="8KP"/> -->
|
||||||
|
<!-- Set the maximum height of an image. When this limit is exceeded, an
|
||||||
|
exception is thrown. -->
|
||||||
|
<!-- <policy domain="resource" name="height" value="8KP"/> -->
|
||||||
|
<!-- Periodically yield the CPU for at least the time specified in
|
||||||
|
milliseconds. -->
|
||||||
|
<!-- <policy domain="resource" name="throttle" value="2"/> -->
|
||||||
|
<!-- Do not create temporary files in the default shared directories, instead
|
||||||
|
specify a private area to store only ImageMagick temporary files. -->
|
||||||
|
<!-- <policy domain="resource" name="temporary-path" value="/magick/tmp/"/> -->
|
||||||
|
<!-- Force memory initialization by memory mapping select memory
|
||||||
|
allocations. -->
|
||||||
|
<!-- <policy domain="cache" name="memory-map" value="anonymous"/> -->
|
||||||
|
<!-- Ensure all image data is fully flushed and synchronized to disk. -->
|
||||||
|
<!-- <policy domain="cache" name="synchronize" value="true"/> -->
|
||||||
|
<!-- Replace passphrase for secure distributed processing -->
|
||||||
|
<!-- <policy domain="cache" name="shared-secret" value="secret-passphrase" stealth="true"/> -->
|
||||||
|
<!-- Do not permit any delegates to execute. -->
|
||||||
|
<!-- <policy domain="delegate" rights="none" pattern="*"/> -->
|
||||||
|
<!-- Do not permit any image filters to load. -->
|
||||||
|
<!-- <policy domain="filter" rights="none" pattern="*"/> -->
|
||||||
|
<!-- Don't read/write from/to stdin/stdout. -->
|
||||||
|
<!-- <policy domain="path" rights="none" pattern="-"/> -->
|
||||||
|
<!-- don't read sensitive paths. -->
|
||||||
|
<!-- <policy domain="path" rights="none" pattern="/etc/*"/> -->
|
||||||
|
<!-- Indirect reads are not permitted. -->
|
||||||
|
<!-- <policy domain="path" rights="none" pattern="@*"/> -->
|
||||||
|
<!-- These image types are security risks on read, but write is fine -->
|
||||||
|
<!-- <policy domain="module" rights="write" pattern="{MSL,MVG,PS,SVG,URL,XPS}"/> -->
|
||||||
|
<!-- This policy sets the number of times to replace content of certain
|
||||||
|
memory buffers and temporary files before they are freed or deleted. -->
|
||||||
|
<!-- <policy domain="system" name="shred" value="1"/> -->
|
||||||
|
<!-- Enable the initialization of buffers with zeros, resulting in a minor
|
||||||
|
performance penalty but with improved security. -->
|
||||||
|
<!-- <policy domain="system" name="memory-map" value="anonymous"/> -->
|
||||||
|
<!-- Set the maximum amount of memory in bytes that are permitted for
|
||||||
|
allocation requests. -->
|
||||||
|
<!-- <policy domain="system" name="max-memory-request" value="256MiB"/> -->
|
||||||
|
</policymap>
|
||||||
BIN
bin/imagick/win64/sRGB.icc
Normal file
BIN
bin/imagick/win64/sRGB.icc
Normal file
Binary file not shown.
336
bin/imagick/win64/thresholds.xml
Normal file
336
bin/imagick/win64/thresholds.xml
Normal file
@@ -0,0 +1,336 @@
|
|||||||
|
<?xml version="1.0" encoding="ISO-8859-1"?>
|
||||||
|
<!DOCTYPE thresholds [
|
||||||
|
<!ELEMENT thresholds (threshold)+>
|
||||||
|
<!ATTLIST thresholds xmlns CDATA #FIXED ''>
|
||||||
|
<!ELEMENT threshold (description,levels)>
|
||||||
|
<!ATTLIST threshold xmlns CDATA #FIXED '' alias NMTOKEN #IMPLIED
|
||||||
|
map NMTOKEN #REQUIRED>
|
||||||
|
<!ELEMENT description (#PCDATA)>
|
||||||
|
<!ATTLIST description xmlns CDATA #FIXED ''>
|
||||||
|
<!ELEMENT levels (#PCDATA)>
|
||||||
|
<!ATTLIST levels xmlns CDATA #FIXED '' divisor CDATA #REQUIRED
|
||||||
|
height CDATA #REQUIRED width CDATA #REQUIRED>
|
||||||
|
]>
|
||||||
|
<!--
|
||||||
|
Threshold Maps for Ordered Posterized Dither
|
||||||
|
|
||||||
|
Each "<threshold>" element defines the map name, description, and an array
|
||||||
|
of "levels" used to provide the threshold map for ordered dithering and
|
||||||
|
digital halftoning.
|
||||||
|
|
||||||
|
The "alias" attribute provides a backward compatible name for this threshold
|
||||||
|
map (pre-dating IM v6.2.9-6), and are deprecated.
|
||||||
|
|
||||||
|
The description is a english description of what the threshold map achieves
|
||||||
|
and is only used for 'listing' the maps.
|
||||||
|
|
||||||
|
The map itself is a rectangular array of integers or threshold "levels"
|
||||||
|
of the given "width" and "height" declared within the enclosing <levels>
|
||||||
|
element. That is "width*height" integers or "levels" *must* be provided
|
||||||
|
within each map.
|
||||||
|
|
||||||
|
Each of the "levels" integer values (each value representing the threshold
|
||||||
|
intensity "level/divisor" at which that pixel is turned on. The "levels"
|
||||||
|
integers given can be any positive integers between "0" and the "divisor",
|
||||||
|
excluding those limits.
|
||||||
|
|
||||||
|
The "divisor" not only defines the upper limit and threshold divisor for each
|
||||||
|
"level" but also the total number of pseudo-levels the threshold mapping
|
||||||
|
creates and fills with a dither pattern. That is a ordered bitmap dither
|
||||||
|
of a pure greyscale gradient will use a maximum of "divisor" ordered bitmap
|
||||||
|
patterns, including the patterns with all the pixels 'on' and all the pixel
|
||||||
|
'off'. It may define less patterns than that, but the color channels will
|
||||||
|
be thresholded in units based on "divisor".
|
||||||
|
|
||||||
|
Alternatively for a multi-level posterization, ImageMagick inserts
|
||||||
|
"divisor-2" dither patterns (as defined by the threshold map) between each of
|
||||||
|
channel color level produced.
|
||||||
|
|
||||||
|
For example the map "o2x2" has a divisor of 5, which will define 3 bitmap
|
||||||
|
patterns plus the patterns with all pixels 'on' and 'off'. A greyscale
|
||||||
|
gradient will thus have 5 distinct areas.
|
||||||
|
-->
|
||||||
|
<thresholds>
|
||||||
|
|
||||||
|
<!-- - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||||
|
Minimal Dither and Non-Dither Threshold Maps
|
||||||
|
-->
|
||||||
|
<threshold map="threshold" alias="1x1">
|
||||||
|
<description>Threshold 1x1 (non-dither)</description>
|
||||||
|
<levels width="1" height="1" divisor="2">
|
||||||
|
1
|
||||||
|
</levels>
|
||||||
|
</threshold>
|
||||||
|
|
||||||
|
<threshold map="checks" alias="2x1">
|
||||||
|
<description>Checkerboard 2x1 (dither)</description>
|
||||||
|
<levels width="2" height="2" divisor="3">
|
||||||
|
1 2
|
||||||
|
2 1
|
||||||
|
</levels>
|
||||||
|
</threshold>
|
||||||
|
|
||||||
|
<!-- - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||||
|
(dispersed) Ordered Dither Patterns
|
||||||
|
-->
|
||||||
|
<threshold map="o2x2" alias="2x2">
|
||||||
|
<description>Ordered 2x2 (dispersed)</description>
|
||||||
|
<levels width="2" height="2" divisor="5">
|
||||||
|
1 3
|
||||||
|
4 2
|
||||||
|
</levels>
|
||||||
|
</threshold>
|
||||||
|
|
||||||
|
<threshold map="o3x3" alias="3x3">
|
||||||
|
<description>Ordered 3x3 (dispersed)</description>
|
||||||
|
<levels width="3" height="3" divisor="10">
|
||||||
|
3 7 4
|
||||||
|
6 1 9
|
||||||
|
2 8 5
|
||||||
|
</levels>
|
||||||
|
</threshold>
|
||||||
|
|
||||||
|
<threshold map="o4x4" alias="4x4">
|
||||||
|
<!--
|
||||||
|
From "Dithering Algorithms"
|
||||||
|
http://www.efg2.com/Lab/Library/ImageProcessing/DHALF.TXT
|
||||||
|
-->
|
||||||
|
<description>Ordered 4x4 (dispersed)</description>
|
||||||
|
<levels width="4" height="4" divisor="17">
|
||||||
|
1 9 3 11
|
||||||
|
13 5 15 7
|
||||||
|
4 12 2 10
|
||||||
|
16 8 14 6
|
||||||
|
</levels>
|
||||||
|
</threshold>
|
||||||
|
|
||||||
|
<threshold map="o8x8" alias="8x8">
|
||||||
|
<!-- Extracted from original 'OrderedDither()' Function -->
|
||||||
|
<description>Ordered 8x8 (dispersed)</description>
|
||||||
|
<levels width="8" height="8" divisor="65">
|
||||||
|
1 49 13 61 4 52 16 64
|
||||||
|
33 17 45 29 36 20 48 32
|
||||||
|
9 57 5 53 12 60 8 56
|
||||||
|
41 25 37 21 44 28 40 24
|
||||||
|
3 51 15 63 2 50 14 62
|
||||||
|
35 19 47 31 34 18 46 30
|
||||||
|
11 59 7 55 10 58 6 54
|
||||||
|
43 27 39 23 42 26 38 22
|
||||||
|
</levels>
|
||||||
|
</threshold>
|
||||||
|
|
||||||
|
<!-- - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||||
|
Halftones - Angled 45 degrees
|
||||||
|
|
||||||
|
Initially added to ImageMagick by Glenn Randers-Pehrson, IM v6.2.8-6,
|
||||||
|
modified to be more symmetrical with intensity by Anthony, IM v6.2.9-7
|
||||||
|
|
||||||
|
These patterns initially start as circles, but then form diamonds
|
||||||
|
pattern at the 50% threshold level, before forming negated circles,
|
||||||
|
as it approached the other threshold extreme.
|
||||||
|
-->
|
||||||
|
<threshold map="h4x4a" alias="4x1">
|
||||||
|
<description>Halftone 4x4 (angled)</description>
|
||||||
|
<levels width="4" height="4" divisor="9">
|
||||||
|
4 2 7 5
|
||||||
|
3 1 8 6
|
||||||
|
7 5 4 2
|
||||||
|
8 6 3 1
|
||||||
|
</levels>
|
||||||
|
</threshold>
|
||||||
|
|
||||||
|
<threshold map="h6x6a" alias="6x1">
|
||||||
|
<description>Halftone 6x6 (angled)</description>
|
||||||
|
<levels width="6" height="6" divisor="19">
|
||||||
|
14 13 10 8 2 3
|
||||||
|
16 18 12 7 1 4
|
||||||
|
15 17 11 9 6 5
|
||||||
|
8 2 3 14 13 10
|
||||||
|
7 1 4 16 18 12
|
||||||
|
9 6 5 15 17 11
|
||||||
|
</levels>
|
||||||
|
</threshold>
|
||||||
|
|
||||||
|
<threshold map="h8x8a" alias="8x1">
|
||||||
|
<description>Halftone 8x8 (angled)</description>
|
||||||
|
<levels width="8" height="8" divisor="33">
|
||||||
|
13 7 8 14 17 21 22 18
|
||||||
|
6 1 3 9 28 31 29 23
|
||||||
|
5 2 4 10 27 32 30 24
|
||||||
|
16 12 11 15 20 26 25 19
|
||||||
|
17 21 22 18 13 7 8 14
|
||||||
|
28 31 29 23 6 1 3 9
|
||||||
|
27 32 30 24 5 2 4 10
|
||||||
|
20 26 25 19 16 12 11 15
|
||||||
|
</levels>
|
||||||
|
</threshold>
|
||||||
|
|
||||||
|
<!-- - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||||
|
Halftones - Orthogonally Aligned, or Un-angled
|
||||||
|
|
||||||
|
Initially added by Anthony Thyssen, IM v6.2.9-5 using techniques from
|
||||||
|
"Dithering & Halftoning" by Gernot Hoffmann
|
||||||
|
http://www.fho-emden.de/~hoffmann/hilb010101.pdf
|
||||||
|
|
||||||
|
These patterns initially start as circles, but then form square
|
||||||
|
pattern at the 50% threshold level, before forming negated circles,
|
||||||
|
as it approached the other threshold extreme.
|
||||||
|
-->
|
||||||
|
<threshold map="h4x4o">
|
||||||
|
<description>Halftone 4x4 (orthogonal)</description>
|
||||||
|
<levels width="4" height="4" divisor="17">
|
||||||
|
7 13 11 4
|
||||||
|
12 16 14 8
|
||||||
|
10 15 6 2
|
||||||
|
5 9 3 1
|
||||||
|
</levels>
|
||||||
|
</threshold>
|
||||||
|
|
||||||
|
<threshold map="h6x6o">
|
||||||
|
<description>Halftone 6x6 (orthogonal)</description>
|
||||||
|
<levels width="6" height="6" divisor="37">
|
||||||
|
7 17 27 14 9 4
|
||||||
|
21 29 33 31 18 11
|
||||||
|
24 32 36 34 25 22
|
||||||
|
19 30 35 28 20 10
|
||||||
|
8 15 26 16 6 2
|
||||||
|
5 13 23 12 3 1
|
||||||
|
</levels>
|
||||||
|
</threshold>
|
||||||
|
|
||||||
|
<threshold map="h8x8o">
|
||||||
|
<description>Halftone 8x8 (orthogonal)</description>
|
||||||
|
<levels width="8" height="8" divisor="65">
|
||||||
|
7 21 33 43 36 19 9 4
|
||||||
|
16 27 51 55 49 29 14 11
|
||||||
|
31 47 57 61 59 45 35 23
|
||||||
|
41 53 60 64 62 52 40 38
|
||||||
|
37 44 58 63 56 46 30 22
|
||||||
|
15 28 48 54 50 26 17 10
|
||||||
|
8 18 34 42 32 20 6 2
|
||||||
|
5 13 25 39 24 12 3 1
|
||||||
|
</levels>
|
||||||
|
</threshold>
|
||||||
|
|
||||||
|
<threshold map="h16x16o">
|
||||||
|
<!--
|
||||||
|
Direct extract from "Dithering & Halftoning" by Gernot Hoffmann.
|
||||||
|
This may need some fine tuning for symmetry of the halftone dots,
|
||||||
|
as it was a mathematically formulated pattern.
|
||||||
|
-->
|
||||||
|
<description>Halftone 16x16 (orthogonal)</description>
|
||||||
|
<levels width="16" height="16" divisor="257">
|
||||||
|
4 12 24 44 72 100 136 152 150 134 98 70 42 23 11 3
|
||||||
|
7 16 32 52 76 104 144 160 158 142 102 74 50 31 15 6
|
||||||
|
19 27 40 60 92 132 168 180 178 166 130 90 58 39 26 18
|
||||||
|
36 48 56 80 124 176 188 204 203 187 175 122 79 55 47 35
|
||||||
|
64 68 84 116 164 200 212 224 223 211 199 162 114 83 67 63
|
||||||
|
88 96 112 156 192 216 232 240 239 231 214 190 154 111 95 87
|
||||||
|
108 120 148 184 208 228 244 252 251 243 226 206 182 147 119 107
|
||||||
|
128 140 172 196 219 235 247 256 255 246 234 218 194 171 139 127
|
||||||
|
126 138 170 195 220 236 248 253 254 245 233 217 193 169 137 125
|
||||||
|
106 118 146 183 207 227 242 249 250 241 225 205 181 145 117 105
|
||||||
|
86 94 110 155 191 215 229 238 237 230 213 189 153 109 93 85
|
||||||
|
62 66 82 115 163 198 210 221 222 209 197 161 113 81 65 61
|
||||||
|
34 46 54 78 123 174 186 202 201 185 173 121 77 53 45 33
|
||||||
|
20 28 37 59 91 131 167 179 177 165 129 89 57 38 25 17
|
||||||
|
8 13 29 51 75 103 143 159 157 141 101 73 49 30 14 5
|
||||||
|
1 9 21 43 71 99 135 151 149 133 97 69 41 22 10 2
|
||||||
|
</levels>
|
||||||
|
</threshold>
|
||||||
|
|
||||||
|
<!-- - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||||
|
Halftones - Orthogonally Expanding Circle Patterns
|
||||||
|
|
||||||
|
Added by Glenn Randers-Pehrson, 4 Nov 2010, ImageMagick 6.6.5-6
|
||||||
|
|
||||||
|
Rather than producing a diamond 50% threshold pattern, these
|
||||||
|
continue to generate larger (overlapping) circles. They are
|
||||||
|
more like a true halftone pattern formed by covering a surface
|
||||||
|
with either pure white or pure black circular dots.
|
||||||
|
|
||||||
|
WARNING: true halftone patterns only use true circles even in
|
||||||
|
areas of highly varying intensity. Threshold dither patterns
|
||||||
|
can generate distorted circles in such areas.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<threshold map="c5x5b" alias="c5x5">
|
||||||
|
<description>Circles 5x5 (black)</description>
|
||||||
|
<levels width="5" height="5" divisor="26">
|
||||||
|
1 21 16 15 4
|
||||||
|
5 17 20 19 14
|
||||||
|
6 21 25 24 12
|
||||||
|
7 18 22 23 11
|
||||||
|
2 8 9 10 3
|
||||||
|
</levels>
|
||||||
|
</threshold>
|
||||||
|
|
||||||
|
|
||||||
|
<threshold map="c5x5w">
|
||||||
|
<description>Circles 5x5 (white)</description>
|
||||||
|
<levels width="5" height="5" divisor="26">
|
||||||
|
25 21 10 11 22
|
||||||
|
20 9 6 7 12
|
||||||
|
19 5 1 2 13
|
||||||
|
18 8 4 3 14
|
||||||
|
24 17 16 15 23
|
||||||
|
</levels>
|
||||||
|
</threshold>
|
||||||
|
|
||||||
|
<threshold map="c6x6b" alias="c6x6">
|
||||||
|
<description>Circles 6x6 (black)</description>
|
||||||
|
<levels width="6" height="6" divisor="37">
|
||||||
|
1 5 14 13 12 4
|
||||||
|
6 22 28 27 21 11
|
||||||
|
15 29 35 34 26 20
|
||||||
|
16 30 36 33 25 19
|
||||||
|
7 23 31 32 24 10
|
||||||
|
2 8 17 18 9 3
|
||||||
|
</levels>
|
||||||
|
</threshold>
|
||||||
|
|
||||||
|
<threshold map="c6x6w">
|
||||||
|
<description>Circles 6x6 (white)</description>
|
||||||
|
<levels width="6" height="6" divisor="37">
|
||||||
|
36 32 23 24 25 33
|
||||||
|
31 15 9 10 16 26
|
||||||
|
22 8 2 3 11 17
|
||||||
|
21 7 1 4 12 18
|
||||||
|
30 14 6 5 13 27
|
||||||
|
35 29 20 19 28 34
|
||||||
|
</levels>
|
||||||
|
</threshold>
|
||||||
|
|
||||||
|
<threshold map="c7x7b" alias="c7x7">
|
||||||
|
<description>Circles 7x7 (black)</description>
|
||||||
|
<levels width="7" height="7" divisor="50">
|
||||||
|
3 9 18 28 17 8 2
|
||||||
|
10 24 33 39 32 23 7
|
||||||
|
19 34 44 48 43 31 16
|
||||||
|
25 40 45 49 47 38 27
|
||||||
|
20 35 41 46 42 29 15
|
||||||
|
11 21 36 37 28 22 6
|
||||||
|
4 12 13 26 14 5 1
|
||||||
|
</levels>
|
||||||
|
</threshold>
|
||||||
|
|
||||||
|
|
||||||
|
<threshold map="c7x7w">
|
||||||
|
<description>Circles 7x7 (white)</description>
|
||||||
|
<levels width="7" height="7" divisor="50">
|
||||||
|
47 41 32 22 33 42 48
|
||||||
|
40 26 17 11 18 27 43
|
||||||
|
31 16 6 2 7 19 34
|
||||||
|
25 10 5 1 3 12 23
|
||||||
|
30 15 9 4 8 20 35
|
||||||
|
39 29 14 13 21 28 44
|
||||||
|
46 38 37 24 36 45 49
|
||||||
|
</levels>
|
||||||
|
</threshold>
|
||||||
|
|
||||||
|
|
||||||
|
<!-- - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||||
|
Special Purpose Dithers
|
||||||
|
-->
|
||||||
|
|
||||||
|
</thresholds>
|
||||||
55
bin/imagick/win64/type-ghostscript.xml
Normal file
55
bin/imagick/win64/type-ghostscript.xml
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE typemap [
|
||||||
|
<!ELEMENT typemap (type)+>
|
||||||
|
<!ELEMENT type (#PCDATA)>
|
||||||
|
<!ELEMENT include (#PCDATA)>
|
||||||
|
<!ATTLIST type name CDATA #REQUIRED>
|
||||||
|
<!ATTLIST type fullname CDATA #IMPLIED>
|
||||||
|
<!ATTLIST type family CDATA #IMPLIED>
|
||||||
|
<!ATTLIST type foundry CDATA #IMPLIED>
|
||||||
|
<!ATTLIST type weight CDATA #IMPLIED>
|
||||||
|
<!ATTLIST type style CDATA #IMPLIED>
|
||||||
|
<!ATTLIST type stretch CDATA #IMPLIED>
|
||||||
|
<!ATTLIST type format CDATA #IMPLIED>
|
||||||
|
<!ATTLIST type metrics CDATA #IMPLIED>
|
||||||
|
<!ATTLIST type glyphs CDATA #REQUIRED>
|
||||||
|
<!ATTLIST type version CDATA #IMPLIED>
|
||||||
|
<!ATTLIST include file CDATA #REQUIRED>
|
||||||
|
]>
|
||||||
|
<typemap>
|
||||||
|
<type name="AvantGarde-Book" fullname="AvantGarde Book" family="AvantGarde" foundry="URW" weight="400" style="normal" stretch="normal" format="type1" metrics="@ghostscript_font_path@a010013l.afm" glyphs="@ghostscript_font_path@a010013l.pfb"/>
|
||||||
|
<type name="AvantGarde-BookOblique" fullname="AvantGarde Book Oblique" family="AvantGarde" foundry="URW" weight="400" style="oblique" stretch="normal" format="type1" metrics="@ghostscript_font_path@a010033l.afm" glyphs="@ghostscript_font_path@a010033l.pfb"/>
|
||||||
|
<type name="AvantGarde-Demi" fullname="AvantGarde DemiBold" family="AvantGarde" foundry="URW" weight="600" style="normal" stretch="normal" format="type1" metrics="@ghostscript_font_path@a010015l.afm" glyphs="@ghostscript_font_path@a010015l.pfb"/>
|
||||||
|
<type name="AvantGarde-DemiOblique" fullname="AvantGarde DemiOblique" family="AvantGarde" foundry="URW" weight="600" style="oblique" stretch="normal" format="type1" metrics="@ghostscript_font_path@a010035l.afm" glyphs="@ghostscript_font_path@a010035l.pfb"/>
|
||||||
|
<type name="Bookman-Demi" fullname="Bookman DemiBold" family="Bookman" foundry="URW" weight="600" style="normal" stretch="normal" format="type1" metrics="@ghostscript_font_path@b018015l.afm" glyphs="@ghostscript_font_path@b018015l.pfb"/>
|
||||||
|
<type name="Bookman-DemiItalic" fullname="Bookman DemiBold Italic" family="Bookman" foundry="URW" weight="600" style="italic" stretch="normal" format="type1" metrics="@ghostscript_font_path@b018035l.afm" glyphs="@ghostscript_font_path@b018035l.pfb"/>
|
||||||
|
<type name="Bookman-Light" fullname="Bookman Light" family="Bookman" foundry="URW" weight="300" style="normal" stretch="normal" format="type1" metrics="@ghostscript_font_path@b018012l.afm" glyphs="@ghostscript_font_path@b018012l.pfb"/>
|
||||||
|
<type name="Bookman-LightItalic" fullname="Bookman Light Italic" family="Bookman" foundry="URW" weight="300" style="italic" stretch="normal" format="type1" metrics="@ghostscript_font_path@b018032l.afm" glyphs="@ghostscript_font_path@b018032l.pfb"/>
|
||||||
|
<type name="Fixed" fullname="Courier Regular" family="Courier" foundry="URW" weight="400" style="normal" stretch="normal" format="type1" metrics="@ghostscript_font_path@n022003l.afm" glyphs="@ghostscript_font_path@n022003l.pfb"/>
|
||||||
|
<type name="Courier" fullname="Courier Regular" family="Courier" foundry="URW" weight="400" style="normal" stretch="normal" format="type1" metrics="@ghostscript_font_path@n022003l.afm" glyphs="@ghostscript_font_path@n022003l.pfb"/>
|
||||||
|
<type name="Courier-Bold" fullname="Courier Bold" family="Courier" foundry="URW" weight="700" style="normal" stretch="normal" format="type1" metrics="@ghostscript_font_path@n022004l.afm" glyphs="@ghostscript_font_path@n022004l.pfb"/>
|
||||||
|
<type name="Courier-Oblique" fullname="Courier Regular Oblique" family="Courier" foundry="URW" weight="400" style="oblique" stretch="normal" format="type1" metrics="@ghostscript_font_path@n022023l.afm" glyphs="@ghostscript_font_path@n022023l.pfb"/>
|
||||||
|
<type name="Courier-BoldOblique" fullname="Courier Bold Oblique" family="Courier" foundry="URW" weight="700" style="oblique" stretch="normal" format="type1" metrics="@ghostscript_font_path@n022024l.afm" glyphs="@ghostscript_font_path@n022024l.pfb"/>
|
||||||
|
<type name="fixed" fullname="Helvetica Regular" family="Helvetica" foundry="URW" weight="400" style="normal" stretch="normal" format="type1" metrics="@ghostscript_font_path@n019003l.afm" glyphs="@ghostscript_font_path@n019003l.pfb"/>
|
||||||
|
<type name="Helvetica" fullname="Helvetica Regular" family="Helvetica" foundry="URW" weight="400" style="normal" stretch="normal" format="type1" metrics="@ghostscript_font_path@n019003l.afm" glyphs="@ghostscript_font_path@n019003l.pfb"/>
|
||||||
|
<type name="Helvetica-Bold" fullname="Helvetica Bold" family="Helvetica" foundry="URW" weight="700" style="normal" stretch="normal" format="type1" metrics="@ghostscript_font_path@n019004l.afm" glyphs="@ghostscript_font_path@n019004l.pfb"/>
|
||||||
|
<type name="Helvetica-Oblique" fullname="Helvetica Regular Italic" family="Helvetica" foundry="URW" weight="400" style="italic" stretch="normal" format="type1" metrics="@ghostscript_font_path@n019023l.afm" glyphs="@ghostscript_font_path@n019023l.pfb"/>
|
||||||
|
<type name="Helvetica-BoldOblique" fullname="Helvetica Bold Italic" family="Helvetica" foundry="URW" weight="700" style="italic" stretch="normal" format="type1" metrics="@ghostscript_font_path@n019024l.afm" glyphs="@ghostscript_font_path@n019024l.pfb"/>
|
||||||
|
<type name="Helvetica-Narrow" fullname="Helvetica Narrow" family="Helvetica Narrow" foundry="URW" weight="400" style="normal" stretch="condensed" format="type1" metrics="@ghostscript_font_path@n019043l.afm" glyphs="@ghostscript_font_path@n019043l.pfb"/>
|
||||||
|
<type name="Helvetica-Narrow-Oblique" fullname="Helvetica Narrow Oblique" family="Helvetica Narrow" foundry="URW" weight="400" style="oblique" stretch="condensed" format="type1" metrics="@ghostscript_font_path@n019063l.afm" glyphs="@ghostscript_font_path@n019063l.pfb"/>
|
||||||
|
<type name="Helvetica-Narrow-Bold" fullname="Helvetica Narrow Bold" family="Helvetica Narrow" foundry="URW" weight="700" style="normal" stretch="condensed" format="type1" metrics="@ghostscript_font_path@n019044l.afm" glyphs="@ghostscript_font_path@n019044l.pfb"/>
|
||||||
|
<type name="Helvetica-Narrow-BoldOblique" fullname="Helvetica Narrow Bold Oblique" family="Helvetica Narrow" foundry="URW" weight="700" style="oblique" stretch="condensed" format="type1" metrics="@ghostscript_font_path@n019064l.afm" glyphs="@ghostscript_font_path@n019064l.pfb"/>
|
||||||
|
<type name="NewCenturySchlbk-Roman" fullname="New Century Schoolbook" family="NewCenturySchlbk" foundry="URW" weight="400" style="normal" stretch="normal" format="type1" metrics="@ghostscript_font_path@c059013l.afm" glyphs="@ghostscript_font_path@c059013l.pfb"/>
|
||||||
|
<type name="NewCenturySchlbk-Italic" fullname="New Century Schoolbook Italic" family="NewCenturySchlbk" foundry="URW" weight="400" style="italic" stretch="normal" format="type1" metrics="@ghostscript_font_path@c059033l.afm" glyphs="@ghostscript_font_path@c059033l.pfb"/>
|
||||||
|
<type name="NewCenturySchlbk-Bold" fullname="New Century Schoolbook Bold" family="NewCenturySchlbk" foundry="URW" weight="700" style="normal" stretch="normal" format="type1" metrics="@ghostscript_font_path@c059016l.afm" glyphs="@ghostscript_font_path@c059016l.pfb"/>
|
||||||
|
<type name="NewCenturySchlbk-BoldItalic" fullname="New Century Schoolbook Bold Italic" family="NewCenturySchlbk" foundry="URW" weight="700" style="italic" stretch="normal" format="type1" metrics="@ghostscript_font_path@c059036l.afm" glyphs="@ghostscript_font_path@c059036l.pfb"/>
|
||||||
|
<type name="Palatino-Roman" fullname="Palatino Regular" family="Palatino" foundry="URW" weight="400" style="normal" stretch="normal" format="type1" metrics="@ghostscript_font_path@p052003l.afm" glyphs="@ghostscript_font_path@p052003l.pfb"/>
|
||||||
|
<type name="Palatino-Italic" fullname="Palatino Italic" family="Palatino" foundry="URW" weight="400" style="italic" stretch="normal" format="type1" metrics="@ghostscript_font_path@p052023l.afm" glyphs="@ghostscript_font_path@p052023l.pfb"/>
|
||||||
|
<type name="Palatino-Bold" fullname="Palatino Bold" family="Palatino" foundry="URW" weight="700" style="normal" stretch="normal" format="type1" metrics="@ghostscript_font_path@p052004l.afm" glyphs="@ghostscript_font_path@p052004l.pfb"/>
|
||||||
|
<type name="Palatino-BoldItalic" fullname="Palatino Bold Italic" family="Palatino" foundry="URW" weight="700" style="italic" stretch="normal" format="type1" metrics="@ghostscript_font_path@p052024l.afm" glyphs="@ghostscript_font_path@p052024l.pfb"/>
|
||||||
|
<type name="Times-Roman" fullname="Times Regular" family="Times" foundry="URW" weight="400" style="normal" stretch="normal" format="type1" metrics="@ghostscript_font_path@n021003l.afm" glyphs="@ghostscript_font_path@n021003l.pfb"/>
|
||||||
|
<type name="Times-Bold" fullname="Times Medium" family="Times" foundry="URW" weight="700" style="normal" stretch="normal" format="type1" metrics="@ghostscript_font_path@n021004l.afm" glyphs="@ghostscript_font_path@n021004l.pfb"/>
|
||||||
|
<type name="Times-Italic" fullname="Times Regular Italic" family="Times" foundry="URW" weight="400" style="italic" stretch="normal" format="type1" metrics="@ghostscript_font_path@n021023l.afm" glyphs="@ghostscript_font_path@n021023l.pfb"/>
|
||||||
|
<type name="Times-BoldItalic" fullname="Times Medium Italic" family="Times" foundry="URW" weight="700" style="italic" stretch="normal" format="type1" metrics="@ghostscript_font_path@n021024l.afm" glyphs="@ghostscript_font_path@n021024l.pfb"/>
|
||||||
|
<type name="Symbol" fullname="Symbol" family="Symbol" foundry="URW" weight="400" style="normal" stretch="normal" format="type1" metrics="@ghostscript_font_path@s050000l.afm" glyphs="@ghostscript_font_path@s050000l.pfb" version="0.1" encoding="AdobeCustom"/>
|
||||||
|
</typemap>
|
||||||
21
bin/imagick/win64/type.xml
Normal file
21
bin/imagick/win64/type.xml
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE typemap [
|
||||||
|
<!ELEMENT typemap (type)+>
|
||||||
|
<!ELEMENT type (#PCDATA)>
|
||||||
|
<!ELEMENT include (#PCDATA)>
|
||||||
|
<!ATTLIST type name CDATA #REQUIRED>
|
||||||
|
<!ATTLIST type fullname CDATA #IMPLIED>
|
||||||
|
<!ATTLIST type family CDATA #IMPLIED>
|
||||||
|
<!ATTLIST type foundry CDATA #IMPLIED>
|
||||||
|
<!ATTLIST type weight CDATA #IMPLIED>
|
||||||
|
<!ATTLIST type style CDATA #IMPLIED>
|
||||||
|
<!ATTLIST type stretch CDATA #IMPLIED>
|
||||||
|
<!ATTLIST type format CDATA #IMPLIED>
|
||||||
|
<!ATTLIST type metrics CDATA #IMPLIED>
|
||||||
|
<!ATTLIST type glyphs CDATA #REQUIRED>
|
||||||
|
<!ATTLIST type version CDATA #IMPLIED>
|
||||||
|
<!ATTLIST include file CDATA #REQUIRED>
|
||||||
|
]>
|
||||||
|
<typemap>
|
||||||
|
<include file="type-ghostscript.xml"/>
|
||||||
|
</typemap>
|
||||||
7
boost.json
Normal file
7
boost.json
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"agents": [
|
||||||
|
"codex",
|
||||||
|
"opencode"
|
||||||
|
],
|
||||||
|
"guidelines": []
|
||||||
|
}
|
||||||
@@ -1,55 +1,35 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
/*
|
use Illuminate\Foundation\Application;
|
||||||
|--------------------------------------------------------------------------
|
use Illuminate\Foundation\Configuration\Exceptions;
|
||||||
| Create The Application
|
use Illuminate\Foundation\Configuration\Middleware;
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
|
|
|
||||||
| The first thing we will do is create a new Laravel application instance
|
|
||||||
| which serves as the "glue" for all the components of Laravel, and is
|
|
||||||
| the IoC container for the system binding all of the various parts.
|
|
||||||
|
|
|
||||||
*/
|
|
||||||
|
|
||||||
$app = new Illuminate\Foundation\Application(
|
return Application::configure(basePath: dirname(__DIR__))
|
||||||
$_ENV['APP_BASE_PATH'] ?? dirname(__DIR__)
|
->withRouting(
|
||||||
);
|
web: __DIR__.'/../routes/web.php',
|
||||||
|
api: __DIR__.'/../routes/api.php',
|
||||||
|
commands: __DIR__.'/../routes/console.php',
|
||||||
|
health: '/up',
|
||||||
|
)
|
||||||
|
->withMiddleware(function (Middleware $middleware) {
|
||||||
|
$middleware->web(append: [
|
||||||
|
\App\Http\Middleware\HandleInertiaRequests::class,
|
||||||
|
\Illuminate\Http\Middleware\AddLinkHeadersForPreloadedAssets::class,
|
||||||
|
]);
|
||||||
|
|
||||||
/*
|
$middleware->alias([
|
||||||
|--------------------------------------------------------------------------
|
'auth' => \App\Http\Middleware\Authenticate::class,
|
||||||
| Bind Important Interfaces
|
'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
|
||||||
|--------------------------------------------------------------------------
|
'auth.session' => \Illuminate\Session\Middleware\AuthenticateSession::class,
|
||||||
|
|
'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class,
|
||||||
| Next, we need to bind some important interfaces into the container so
|
'can' => \Illuminate\Auth\Middleware\Authorize::class,
|
||||||
| we will be able to resolve them when needed. The kernels serve the
|
'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
|
||||||
| incoming requests to this application from both the web and CLI.
|
'password.confirm' => \Illuminate\Auth\Middleware\RequirePassword::class,
|
||||||
|
|
'signed' => \App\Http\Middleware\ValidateSignature::class,
|
||||||
*/
|
'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
|
||||||
|
'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class,
|
||||||
$app->singleton(
|
]);
|
||||||
Illuminate\Contracts\Http\Kernel::class,
|
})
|
||||||
App\Http\Kernel::class
|
->withExceptions(function (Exceptions $exceptions) {
|
||||||
);
|
//
|
||||||
|
})->create();
|
||||||
$app->singleton(
|
|
||||||
Illuminate\Contracts\Console\Kernel::class,
|
|
||||||
App\Console\Kernel::class
|
|
||||||
);
|
|
||||||
|
|
||||||
$app->singleton(
|
|
||||||
Illuminate\Contracts\Debug\ExceptionHandler::class,
|
|
||||||
App\Exceptions\Handler::class
|
|
||||||
);
|
|
||||||
|
|
||||||
/*
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
| Return The Application
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
|
|
|
||||||
| This script returns the application instance. The instance is given to
|
|
||||||
| the calling script so we can separate the building of the instances
|
|
||||||
| from the actual running of the application and sending responses.
|
|
||||||
|
|
|
||||||
*/
|
|
||||||
|
|
||||||
return $app;
|
|
||||||
@@ -5,24 +5,29 @@
|
|||||||
"keywords": ["framework", "laravel"],
|
"keywords": ["framework", "laravel"],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"require": {
|
"require": {
|
||||||
"php": "^8.1",
|
"php": "^8.3",
|
||||||
"filament/filament": "3.0",
|
"amphp/websocket-client": "^2.0",
|
||||||
|
"blade-ui-kit/blade-icons": "^1.8",
|
||||||
|
"filament/filament": "^4.0",
|
||||||
"guzzlehttp/guzzle": "^7.2",
|
"guzzlehttp/guzzle": "^7.2",
|
||||||
"inertiajs/inertia-laravel": "^0.6.8",
|
"inertiajs/inertia-laravel": "^1.0",
|
||||||
"laravel/breeze": "1.29",
|
"laravel/breeze": "^2.0",
|
||||||
"laravel/framework": "^10.0",
|
"laravel/framework": "^12.0",
|
||||||
"laravel/sanctum": "^3.2",
|
"laravel/sanctum": "^4.0",
|
||||||
"laravel/tinker": "^2.8",
|
"laravel/tinker": "^2.8",
|
||||||
|
"predis/predis": "^3.1",
|
||||||
|
"spatie/laravel-settings": "*",
|
||||||
"tightenco/ziggy": "^2.0"
|
"tightenco/ziggy": "^2.0"
|
||||||
},
|
},
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
"fakerphp/faker": "^1.9.1",
|
"fakerphp/faker": "^1.9.1",
|
||||||
|
"laravel/boost": "^1.1",
|
||||||
"laravel/pint": "^1.0",
|
"laravel/pint": "^1.0",
|
||||||
"laravel/sail": "^1.18",
|
"laravel/sail": "^1.18",
|
||||||
"mockery/mockery": "^1.4.4",
|
"mockery/mockery": "^1.4.4",
|
||||||
"nunomaduro/collision": "^7.0",
|
"nunomaduro/collision": "^8.1",
|
||||||
"phpunit/phpunit": "^10.0",
|
"phpunit/phpunit": "^12.0",
|
||||||
"spatie/laravel-ignition": "^2.0"
|
"spatie/laravel-ignition": "^2.7"
|
||||||
},
|
},
|
||||||
"autoload": {
|
"autoload": {
|
||||||
"psr-4": {
|
"psr-4": {
|
||||||
|
|||||||
5709
composer.lock
generated
5709
composer.lock
generated
File diff suppressed because it is too large
Load Diff
BIN
composer.phar
Normal file
BIN
composer.phar
Normal file
Binary file not shown.
@@ -15,7 +15,7 @@ return [
|
|||||||
|
|
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
'name' => env('APP_NAME', 'Laravel'),
|
'name' => env('APP_NAME', 'AI Stylegallery'),
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
@@ -185,6 +185,7 @@ return [
|
|||||||
/*
|
/*
|
||||||
* Package Service Providers...
|
* Package Service Providers...
|
||||||
*/
|
*/
|
||||||
|
BladeUI\Icons\BladeIconsServiceProvider::class,
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Application Service Providers...
|
* Application Service Providers...
|
||||||
|
|||||||
183
config/blade-icons.php
Normal file
183
config/blade-icons.php
Normal file
@@ -0,0 +1,183 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
return [
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Icons Sets
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| With this config option you can define a couple of
|
||||||
|
| default icon sets. Provide a key name for your icon
|
||||||
|
| set and a combination from the options below.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'sets' => [
|
||||||
|
|
||||||
|
// 'default' => [
|
||||||
|
//
|
||||||
|
// /*
|
||||||
|
// |-----------------------------------------------------------------
|
||||||
|
// | Icons Path
|
||||||
|
// |-----------------------------------------------------------------
|
||||||
|
// |
|
||||||
|
// | Provide the relative path from your app root to your SVG icons
|
||||||
|
// | directory. Icons are loaded recursively so there's no need to
|
||||||
|
// | list every sub-directory.
|
||||||
|
// |
|
||||||
|
// | Relative to the disk root when the disk option is set.
|
||||||
|
// |
|
||||||
|
// */
|
||||||
|
//
|
||||||
|
// 'path' => 'resources/svg',
|
||||||
|
//
|
||||||
|
// /*
|
||||||
|
// |-----------------------------------------------------------------
|
||||||
|
// | Filesystem Disk
|
||||||
|
// |-----------------------------------------------------------------
|
||||||
|
// |
|
||||||
|
// | Optionally, provide a specific filesystem disk to read
|
||||||
|
// | icons from. When defining a disk, the "path" option
|
||||||
|
// | starts relatively from the disk root.
|
||||||
|
// |
|
||||||
|
// */
|
||||||
|
//
|
||||||
|
// 'disk' => '',
|
||||||
|
//
|
||||||
|
// /*
|
||||||
|
// |-----------------------------------------------------------------
|
||||||
|
// | Default Prefix
|
||||||
|
// |-----------------------------------------------------------------
|
||||||
|
// |
|
||||||
|
// | This config option allows you to define a default prefix for
|
||||||
|
// | your icons. The dash separator will be applied automatically
|
||||||
|
// | to every icon name. It's required and needs to be unique.
|
||||||
|
// |
|
||||||
|
// */
|
||||||
|
//
|
||||||
|
// 'prefix' => 'icon',
|
||||||
|
//
|
||||||
|
// /*
|
||||||
|
// |-----------------------------------------------------------------
|
||||||
|
// | Fallback Icon
|
||||||
|
// |-----------------------------------------------------------------
|
||||||
|
// |
|
||||||
|
// | This config option allows you to define a fallback
|
||||||
|
// | icon when an icon in this set cannot be found.
|
||||||
|
// |
|
||||||
|
// */
|
||||||
|
//
|
||||||
|
// 'fallback' => '',
|
||||||
|
//
|
||||||
|
// /*
|
||||||
|
// |-----------------------------------------------------------------
|
||||||
|
// | Default Set Classes
|
||||||
|
// |-----------------------------------------------------------------
|
||||||
|
// |
|
||||||
|
// | This config option allows you to define some classes which
|
||||||
|
// | will be applied by default to all icons within this set.
|
||||||
|
// |
|
||||||
|
// */
|
||||||
|
//
|
||||||
|
// 'class' => '',
|
||||||
|
//
|
||||||
|
// /*
|
||||||
|
// |-----------------------------------------------------------------
|
||||||
|
// | Default Set Attributes
|
||||||
|
// |-----------------------------------------------------------------
|
||||||
|
// |
|
||||||
|
// | This config option allows you to define some attributes which
|
||||||
|
// | will be applied by default to all icons within this set.
|
||||||
|
// |
|
||||||
|
// */
|
||||||
|
//
|
||||||
|
// 'attributes' => [
|
||||||
|
// // 'width' => 50,
|
||||||
|
// // 'height' => 50,
|
||||||
|
// ],
|
||||||
|
//
|
||||||
|
// ],
|
||||||
|
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Global Default Classes
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This config option allows you to define some classes which
|
||||||
|
| will be applied by default to all icons.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'class' => '',
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Global Default Attributes
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This config option allows you to define some attributes which
|
||||||
|
| will be applied by default to all icons.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'attributes' => [
|
||||||
|
// 'width' => 50,
|
||||||
|
// 'height' => 50,
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Global Fallback Icon
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This config option allows you to define a global fallback
|
||||||
|
| icon when an icon in any set cannot be found. It can
|
||||||
|
| reference any icon from any configured set.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'fallback' => '',
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Components
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| These config options allow you to define some
|
||||||
|
| settings related to Blade Components.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'components' => [
|
||||||
|
|
||||||
|
/*
|
||||||
|
|----------------------------------------------------------------------
|
||||||
|
| Disable Components
|
||||||
|
|----------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This config option allows you to disable Blade components
|
||||||
|
| completely. It's useful to avoid performance problems
|
||||||
|
| when working with large icon libraries.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'disabled' => false,
|
||||||
|
|
||||||
|
/*
|
||||||
|
|----------------------------------------------------------------------
|
||||||
|
| Default Icon Component Name
|
||||||
|
|----------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This config option allows you to define the name
|
||||||
|
| for the default Icon class component.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'default' => 'icon',
|
||||||
|
|
||||||
|
],
|
||||||
|
|
||||||
|
];
|
||||||
@@ -15,7 +15,7 @@ return [
|
|||||||
|
|
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
'default' => env('BROADCAST_DRIVER', 'null'),
|
'default' => env('BROADCAST_DRIVER', 'redis'),
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -4,35 +4,8 @@ use Illuminate\Support\Str;
|
|||||||
|
|
||||||
return [
|
return [
|
||||||
|
|
||||||
/*
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
| Default Database Connection Name
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
|
|
|
||||||
| Here you may specify which of the database connections below you wish
|
|
||||||
| to use as your default connection for all database work. Of course
|
|
||||||
| you may use many connections at once using the Database library.
|
|
||||||
|
|
|
||||||
*/
|
|
||||||
|
|
||||||
'default' => env('DB_CONNECTION', 'mysql'),
|
'default' => env('DB_CONNECTION', 'mysql'),
|
||||||
|
|
||||||
/*
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
| Database Connections
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
|
|
|
||||||
| Here are each of the database connections setup for your application.
|
|
||||||
| Of course, examples of configuring each database platform that is
|
|
||||||
| supported by Laravel is shown below to make development simple.
|
|
||||||
|
|
|
||||||
|
|
|
||||||
| All database work in Laravel is done through the PHP PDO facilities
|
|
||||||
| so make sure you have the driver for your particular database of
|
|
||||||
| choice installed on your machine before you begin development.
|
|
||||||
|
|
|
||||||
*/
|
|
||||||
|
|
||||||
'connections' => [
|
'connections' => [
|
||||||
|
|
||||||
'sqlite' => [
|
'sqlite' => [
|
||||||
@@ -95,30 +68,8 @@ return [
|
|||||||
|
|
||||||
],
|
],
|
||||||
|
|
||||||
/*
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
| Migration Repository Table
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
|
|
|
||||||
| This table keeps track of all the migrations that have already run for
|
|
||||||
| your application. Using this information, we can determine which of
|
|
||||||
| the migrations on disk haven't actually been run in the database.
|
|
||||||
|
|
|
||||||
*/
|
|
||||||
|
|
||||||
'migrations' => 'migrations',
|
'migrations' => 'migrations',
|
||||||
|
|
||||||
/*
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
| Redis Databases
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
|
|
|
||||||
| Redis is an open source, fast, and advanced key-value store that also
|
|
||||||
| provides a richer body of commands than a typical key-value system
|
|
||||||
| such as APC or Memcached. Laravel makes it easy to dig right in.
|
|
||||||
|
|
|
||||||
*/
|
|
||||||
|
|
||||||
'redis' => [
|
'redis' => [
|
||||||
|
|
||||||
'client' => env('REDIS_CLIENT', 'phpredis'),
|
'client' => env('REDIS_CLIENT', 'phpredis'),
|
||||||
|
|||||||
101
config/filament.php
Normal file
101
config/filament.php
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
return [
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Broadcasting
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| By uncommenting the Laravel Echo configuration, you may connect Filament
|
||||||
|
| to any Pusher-compatible websockets server.
|
||||||
|
|
|
||||||
|
| This will allow your users to receive real-time notifications.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'broadcasting' => [
|
||||||
|
|
||||||
|
// 'echo' => [
|
||||||
|
// 'broadcaster' => 'pusher',
|
||||||
|
// 'key' => env('VITE_PUSHER_APP_KEY'),
|
||||||
|
// 'cluster' => env('VITE_PUSHER_APP_CLUSTER'),
|
||||||
|
// 'wsHost' => env('VITE_PUSHER_HOST'),
|
||||||
|
// 'wsPort' => env('VITE_PUSHER_PORT'),
|
||||||
|
// 'wssPort' => env('VITE_PUSHER_PORT'),
|
||||||
|
// 'authEndpoint' => '/broadcasting/auth',
|
||||||
|
// 'disableStats' => true,
|
||||||
|
// 'encrypted' => true,
|
||||||
|
// 'forceTLS' => true,
|
||||||
|
// ],
|
||||||
|
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Default Filesystem Disk
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This is the storage disk Filament will use to store files. You may use
|
||||||
|
| any of the disks defined in the `config/filesystems.php`.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'default_filesystem_disk' => env('FILAMENT_FILESYSTEM_DISK', 'public'),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Assets Path
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This is the directory where Filament's assets will be published to. It
|
||||||
|
| is relative to the `public` directory of your Laravel application.
|
||||||
|
|
|
||||||
|
| After changing the path, you should run `php artisan filament:assets`.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'assets_path' => null,
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Cache Path
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This is the directory that Filament will use to store cache files that
|
||||||
|
| are used to optimize the registration of components.
|
||||||
|
|
|
||||||
|
| After changing the path, you should run `php artisan filament:cache-components`.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'cache_path' => base_path('bootstrap/cache/filament'),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Livewire Loading Delay
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This sets the delay before loading indicators appear.
|
||||||
|
|
|
||||||
|
| Setting this to 'none' makes indicators appear immediately, which can be
|
||||||
|
| desirable for high-latency connections. Setting it to 'default' applies
|
||||||
|
| Livewire's standard 200ms delay.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'livewire_loading_delay' => 'default',
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| System Route Prefix
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This is the prefix used for the system routes that Filament registers,
|
||||||
|
| such as the routes for downloading exports and failed import rows.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'system_route_prefix' => 'filament',
|
||||||
|
|
||||||
|
];
|
||||||
@@ -38,7 +38,7 @@ return [
|
|||||||
|
|
||||||
'public' => [
|
'public' => [
|
||||||
'driver' => 'local',
|
'driver' => 'local',
|
||||||
'root' => storage_path('app/public'),
|
'root' => public_path('storage'),
|
||||||
'url' => env('APP_URL').'/storage',
|
'url' => env('APP_URL').'/storage',
|
||||||
'visibility' => 'public',
|
'visibility' => 'public',
|
||||||
'throw' => false,
|
'throw' => false,
|
||||||
@@ -70,7 +70,7 @@ return [
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
'links' => [
|
'links' => [
|
||||||
public_path('storage') => storage_path('app/public'),
|
public_path('storage') => public_path('storage'),
|
||||||
],
|
],
|
||||||
|
|
||||||
];
|
];
|
||||||
|
|||||||
94
config/settings.php
Normal file
94
config/settings.php
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
return [
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Each settings class used in your application must be registered, you can
|
||||||
|
* put them (manually) here.
|
||||||
|
*/
|
||||||
|
'settings' => [
|
||||||
|
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
* The path where the settings classes will be created.
|
||||||
|
*/
|
||||||
|
'setting_class_path' => app_path('Settings'),
|
||||||
|
|
||||||
|
/*
|
||||||
|
* In these directories settings migrations will be stored and ran when migrating. A settings
|
||||||
|
* migration created via the make:settings-migration command will be stored in the first path or
|
||||||
|
* a custom defined path when running the command.
|
||||||
|
*/
|
||||||
|
'migrations_paths' => [
|
||||||
|
database_path('settings'),
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
* When no repository was set for a settings class the following repository
|
||||||
|
* will be used for loading and saving settings.
|
||||||
|
*/
|
||||||
|
'default_repository' => 'database',
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Settings will be stored and loaded from these repositories.
|
||||||
|
*/
|
||||||
|
'repositories' => [
|
||||||
|
'database' => [
|
||||||
|
'type' => Spatie\LaravelSettings\SettingsRepositories\DatabaseSettingsRepository::class,
|
||||||
|
'model' => null,
|
||||||
|
'table' => null,
|
||||||
|
'connection' => null,
|
||||||
|
],
|
||||||
|
'redis' => [
|
||||||
|
'type' => Spatie\LaravelSettings\SettingsRepositories\RedisSettingsRepository::class,
|
||||||
|
'connection' => null,
|
||||||
|
'prefix' => null,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
* The encoder and decoder will determine how settings are stored and
|
||||||
|
* retrieved in the database. By default, `json_encode` and `json_decode`
|
||||||
|
* are used.
|
||||||
|
*/
|
||||||
|
'encoder' => null,
|
||||||
|
'decoder' => null,
|
||||||
|
|
||||||
|
/*
|
||||||
|
* The contents of settings classes can be cached through your application,
|
||||||
|
* settings will be stored within a provided Laravel store and can have an
|
||||||
|
* additional prefix.
|
||||||
|
*/
|
||||||
|
'cache' => [
|
||||||
|
'enabled' => env('SETTINGS_CACHE_ENABLED', false),
|
||||||
|
'store' => null,
|
||||||
|
'prefix' => null,
|
||||||
|
'ttl' => null,
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
* These global casts will be automatically used whenever a property within
|
||||||
|
* your settings class isn't a default PHP type.
|
||||||
|
*/
|
||||||
|
'global_casts' => [
|
||||||
|
DateTimeInterface::class => Spatie\LaravelSettings\SettingsCasts\DateTimeInterfaceCast::class,
|
||||||
|
DateTimeZone::class => Spatie\LaravelSettings\SettingsCasts\DateTimeZoneCast::class,
|
||||||
|
// Spatie\DataTransferObject\DataTransferObject::class => Spatie\LaravelSettings\SettingsCasts\DtoCast::class,
|
||||||
|
Spatie\LaravelData\Data::class => Spatie\LaravelSettings\SettingsCasts\DataCast::class,
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
* The package will look for settings in these paths and automatically
|
||||||
|
* register them.
|
||||||
|
*/
|
||||||
|
'auto_discover_settings' => [
|
||||||
|
app_path('Settings'),
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Automatically discovered settings classes can be cached, so they don't
|
||||||
|
* need to be searched each time the application boots up.
|
||||||
|
*/
|
||||||
|
'discovered_settings_cache_path' => base_path('bootstrap/cache'),
|
||||||
|
];
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
use Illuminate\Database\Migrations\Migration;
|
|
||||||
use Illuminate\Database\Schema\Blueprint;
|
|
||||||
use Illuminate\Support\Facades\Schema;
|
|
||||||
|
|
||||||
return new class extends Migration
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Run the migrations.
|
|
||||||
*/
|
|
||||||
public function up(): void
|
|
||||||
{
|
|
||||||
Schema::create('users', function (Blueprint $table) {
|
|
||||||
$table->id();
|
|
||||||
$table->string('name');
|
|
||||||
$table->string('email')->unique();
|
|
||||||
$table->timestamp('email_verified_at')->nullable();
|
|
||||||
$table->string('password');
|
|
||||||
$table->rememberToken();
|
|
||||||
$table->timestamps();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reverse the migrations.
|
|
||||||
*/
|
|
||||||
public function down(): void
|
|
||||||
{
|
|
||||||
Schema::dropIfExists('users');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
use Illuminate\Database\Migrations\Migration;
|
|
||||||
use Illuminate\Database\Schema\Blueprint;
|
|
||||||
use Illuminate\Support\Facades\Schema;
|
|
||||||
|
|
||||||
return new class extends Migration
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Run the migrations.
|
|
||||||
*/
|
|
||||||
public function up(): void
|
|
||||||
{
|
|
||||||
Schema::create('password_reset_tokens', function (Blueprint $table) {
|
|
||||||
$table->string('email')->primary();
|
|
||||||
$table->string('token');
|
|
||||||
$table->timestamp('created_at')->nullable();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reverse the migrations.
|
|
||||||
*/
|
|
||||||
public function down(): void
|
|
||||||
{
|
|
||||||
Schema::dropIfExists('password_reset_tokens');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user