feat: filament (#679)

This commit is contained in:
Kyrch
2024-05-23 17:22:01 -03:00
committed by GitHub
parent f33db28908
commit 97b49dbb5e
470 changed files with 32491 additions and 683 deletions
+6 -2
View File
@@ -82,7 +82,7 @@ REDIS_CACHE_DB=1
DUMP_DISK=dumps DUMP_DISK=dumps
DUMP_DISK_ROOT= DUMP_DISK_ROOT=
DUMP_URL=http://localhost DUMP_URL=http://localhost
DUMP_PATH= DUMP_PATH=/dump
# elastic client # elastic client
ELASTIC_CONNECTION=default ELASTIC_CONNECTION=default
@@ -98,6 +98,10 @@ ELASTIC_MIGRATIONS_TABLE=elastic_migrations
FFMPEG_BINARIES= FFMPEG_BINARIES=
FFPROBE_BINARIES= FFPROBE_BINARIES=
# filament
FILAMENT_DOMAIN_NAME=
FILAMENT_PATH=/admin
# filesystems # filesystems
FILESYSTEM_DISK=local FILESYSTEM_DISK=local
@@ -211,7 +215,7 @@ NOVA_DOMAIN_NAME=null
NOVA_GUARD=null NOVA_GUARD=null
NOVA_LICENSE_KEY= NOVA_LICENSE_KEY=
NOVA_PASSWORDS=null NOVA_PASSWORDS=null
NOVA_PATH=/admin NOVA_PATH=/nova
# pennant # pennant
PENNANT_STORE=database PENNANT_STORE=database
+5 -1
View File
@@ -83,7 +83,7 @@ REDIS_CACHE_DB=1
DUMP_DISK=dumps DUMP_DISK=dumps
DUMP_DISK_ROOT= DUMP_DISK_ROOT=
DUMP_URL=http://localhost DUMP_URL=http://localhost
DUMP_PATH= DUMP_PATH=/dump
# elastic client # elastic client
ELASTIC_CONNECTION=default ELASTIC_CONNECTION=default
@@ -99,6 +99,10 @@ ELASTIC_MIGRATIONS_TABLE=elastic_migrations
FFMPEG_BINARIES= FFMPEG_BINARIES=
FFPROBE_BINARIES= FFPROBE_BINARIES=
# filament
FILAMENT_DOMAIN_NAME=
FILAMENT_PATH=/admin
# filesystems # filesystems
FILESYSTEM_DISK=local FILESYSTEM_DISK=local
@@ -0,0 +1,86 @@
<?php
declare(strict_types=1);
namespace App\Actions\Discord;
use App\Constants\Config\ServiceConstants;
use App\Enums\Discord\EmbedColor;
use App\Enums\Models\Wiki\AnimeSeason;
use App\Enums\Models\Wiki\ImageFacet;
use App\Models\Wiki\Anime;
use App\Models\Wiki\Image;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Storage;
class DiscordThreadAction
{
/**
* Handle the action.
*
* @param Anime $anime
* @param array $fields
*
* @return void
*/
public function handle(Anime $anime, array $fields): void
{
$name = Arr::get($fields, 'name');
/** @var \Illuminate\Filesystem\FilesystemAdapter */
$imageDisk = Storage::disk(Config::get('image.disk'));
$imagePath = $anime->images()->where(Image::ATTRIBUTE_FACET, ImageFacet::COVER_LARGE)->first()->path;
$animepage = json_decode(file_get_contents(base_path('composer.json')), true)['homepage'].'anime/';
$description = '**Synopsis:** '.strip_tags($anime->synopsis)."\n\n".'**Link:** '.$animepage.$anime->slug;
Http::withToken(Config::get('services.discord.token'), 'Bot')
->asMultipart()
->attach('file', file_get_contents($imageDisk->url($imagePath)), 'image.jpg')
->post("https://discord.com/api/v10/channels/{$this->getDiscordChannel()}/threads", [
'payload_json' => json_encode([
'name' => $name,
'applied_tags' => $this->getAppliedTags($anime->season->value),
'message' => [
'embeds' => [
[
'color' => EmbedColor::PURPLE->value,
'title' => $anime->name,
'description' => $description,
]
],
]
])
])->throw();
}
/**
* Get Discord forum channel the thread will be created to.
*
* @return string
*/
protected function getDiscordChannel(): string
{
return Config::get(ServiceConstants::SUBMISSIONS_DISCORD_CHANNEL_QUALIFIED);
}
/**
* Get the IDs of the tags applied to the thread.
*
* @param int $season
* @return array
*/
protected function getAppliedTags(int $season): array
{
return match ($season) {
AnimeSeason::WINTER->value => [Config::get('services.discord.submissions_forum_tags.winter')],
AnimeSeason::SPRING->value => [Config::get('services.discord.submissions_forum_tags.spring')],
AnimeSeason::SUMMER->value => [Config::get('services.discord.submissions_forum_tags.summer')],
AnimeSeason::FALL->value => [Config::get('services.discord.submissions_forum_tags.fall')],
default => [],
};
}
}
@@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
namespace App\Actions\Models\Wiki\Anime;
use App\Models\Wiki\Image;
use App\Actions\Models\Wiki\AttachImageAction;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
/**
* Class AttachAnimeImageAction.
*/
class AttachAnimeImageAction extends AttachImageAction
{
/**
* Get the relation to the action models.
*
* @param Image $image
* @return BelongsToMany
*/
protected function relation(Image $image): BelongsToMany
{
return $image->anime();
}
}
@@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
namespace App\Actions\Models\Wiki\Anime;
use App\Models\Wiki\ExternalResource;
use App\Actions\Models\Wiki\AttachResourceAction;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
/**
* Class AttachAnimeResourceAction.
*/
class AttachAnimeResourceAction extends AttachResourceAction
{
/**
* Get the relation to the action models.
*
* @param ExternalResource $resource
* @return BelongsToMany
*/
protected function relation(ExternalResource $resource): BelongsToMany
{
return $resource->anime();
}
}
@@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
namespace App\Actions\Models\Wiki\Artist;
use App\Models\Wiki\Image;
use App\Actions\Models\Wiki\AttachImageAction;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
/**
* Class AttachArtistImageAction.
*/
class AttachArtistImageAction extends AttachImageAction
{
/**
* Get the relation to the action models.
*
* @param Image $image
* @return BelongsToMany
*/
protected function relation(Image $image): BelongsToMany
{
return $image->artists();
}
}
@@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
namespace App\Actions\Models\Wiki\Artist;
use App\Models\Wiki\ExternalResource;
use App\Actions\Models\Wiki\AttachResourceAction;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
/**
* Class AttachArtistResourceAction.
*/
class AttachArtistResourceAction extends AttachResourceAction
{
/**
* Get the relation to the action models.
*
* @param ExternalResource $resource
* @return BelongsToMany
*/
protected function relation(ExternalResource $resource): BelongsToMany
{
return $resource->artists();
}
}
@@ -0,0 +1,102 @@
<?php
declare(strict_types=1);
namespace App\Actions\Models\Wiki;
use App\Enums\Models\Wiki\ImageFacet;
use App\Models\BaseModel;
use App\Models\Wiki\Image;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
/**
* Class AttachImageAction.
*/
abstract class AttachImageAction
{
/**
* Create a new action instance.
*
* @param ImageFacet[] $facets
*/
public function __construct(protected array $facets)
{
}
/**
* Perform the action on the given models.
*
* @param BaseModel $model
* @param array $fields
* @return void
*/
public function handle(BaseModel $model, array $fields): void
{
$images = $this->createImages($fields, $model);
foreach ($images as $image) {
$relation = $this->relation($image);
$relation->attach($model);
}
}
/**
* Create the images.
*
* @param array $fields
* @param BaseModel $model
* @return Image[]
*/
protected function createImages(array $fields, BaseModel $model): array
{
$images = [];
foreach ($this->facets as $facet) {
$image = Arr::get($fields, $facet->name);
if (empty($image)) continue;
/** @var \Illuminate\Filesystem\FilesystemAdapter */
$fs = Storage::disk(Config::get('image.disk'));
$fsFile = $fs->putFile($this->path($facet, $model), $image);
$image = Image::query()->create([
Image::ATTRIBUTE_FACET => $facet->value,
Image::ATTRIBUTE_PATH => $fsFile,
]);
$images[] = $image;
}
return $images;
}
/**
* Path to storage image in filesystem.
*
* @param ImageFacet $facet
* @param BaseModel $model
* @return string
*/
protected function path(ImageFacet $facet, BaseModel $model): string
{
return Str::of(Str::kebab(class_basename($model)))
->append(DIRECTORY_SEPARATOR)
->append(Str::kebab($facet->localize()))
->__toString();
}
/**
* Get the relation to the action models.
*
* @param Image $image
* @return BelongsToMany
*/
abstract protected function relation(Image $image): BelongsToMany;
}
@@ -0,0 +1,82 @@
<?php
declare(strict_types=1);
namespace App\Actions\Models\Wiki;
use App\Enums\Models\Wiki\ResourceSite;
use App\Models\BaseModel;
use App\Models\Wiki\ExternalResource;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Support\Arr;
abstract class AttachResourceAction
{
/**
* Create a new action instance.
*
* @param ResourceSite[] $sites
*/
public function __construct(protected array $sites)
{
}
/**
* Handle the action.
*
* @param BaseModel $model
* @param array $data
* @return void
*/
public function handle(BaseModel $model, array $data): void
{
$resources = $this->getOrCreateResource($data);
foreach ($resources as $resource) {
$relation = $this->relation($resource);
$relation->attach($model);
}
}
/**
* Get or Create Resource from link field.
*
* @param array $data
* @return ExternalResource[]
*/
protected function getOrCreateResource(array $data): array
{
$resources = [];
foreach ($this->sites as $resourceSite) {
$link = Arr::get($data, $resourceSite->name);
if (empty($link)) continue;
$resource = ExternalResource::query()
->where(ExternalResource::ATTRIBUTE_LINK, $link)
->first();
if ($resource === null) {
$resource = ExternalResource::query()->create([
ExternalResource::ATTRIBUTE_EXTERNAL_ID => ResourceSite::parseIdFromLink($link),
ExternalResource::ATTRIBUTE_LINK => $link,
ExternalResource::ATTRIBUTE_SITE => $resourceSite->value,
]);
}
$resources[] = $resource;
}
return $resources;
}
/**
* Get the relation to the action models.
*
* @param ExternalResource $resource
* @return BelongsToMany
*/
abstract protected function relation(ExternalResource $resource): BelongsToMany;
}
@@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
namespace App\Actions\Models\Wiki\Song;
use App\Models\Wiki\ExternalResource;
use App\Actions\Models\Wiki\AttachResourceAction;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
/**
* Class AttachSongResourceAction.
*/
class AttachSongResourceAction extends AttachResourceAction
{
/**
* Get the relation to the action models.
*
* @param ExternalResource $resource
* @return BelongsToMany
*/
protected function relation(ExternalResource $resource): BelongsToMany
{
return $resource->songs();
}
}
@@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
namespace App\Actions\Models\Wiki\Studio;
use App\Models\Wiki\Image;
use App\Actions\Models\Wiki\AttachImageAction;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
/**
* Class AttachStudioImageAction.
*/
class AttachStudioImageAction extends AttachImageAction
{
/**
* Get the relation to the action models.
*
* @param Image $image
* @return BelongsToMany
*/
protected function relation(Image $image): BelongsToMany
{
return $image->studios();
}
}
@@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
namespace App\Actions\Models\Wiki\Studio;
use App\Models\Wiki\ExternalResource;
use App\Actions\Models\Wiki\AttachResourceAction;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
/**
* Class AttachStudioResourceAction.
*/
class AttachStudioResourceAction extends AttachResourceAction
{
/**
* Get the relation to the action models.
*
* @param ExternalResource $resource
* @return BelongsToMany
*/
protected function relation(ExternalResource $resource): BelongsToMany
{
return $resource->studios();
}
}
@@ -0,0 +1,52 @@
<?php
declare(strict_types=1);
namespace App\Actions\Models\Wiki;
use App\Enums\Models\Wiki\ImageFacet;
use App\Models\Wiki\Image;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
/**
* Class UploadImageAction.
*/
class UploadImageAction
{
/**
* Create the images.
*
* @param array $fields
* @return void
*/
public function handle(array $fields): void
{
$facet = ImageFacet::from(intval(Arr::get($fields, Image::ATTRIBUTE_FACET)));
$image = Arr::get($fields, Image::ATTRIBUTE_PATH);
/** @var \Illuminate\Filesystem\FilesystemAdapter */
$fs = Storage::disk(Config::get('image.disk'));
$fsFile = $fs->putFile($this->path($facet), $image);
Image::query()->create([
Image::ATTRIBUTE_FACET => $facet->value,
Image::ATTRIBUTE_PATH => $fsFile,
]);
}
/**
* Path to storage image in filesystem.
*
* @param ImageFacet $facet
* @return string
*/
protected function path(ImageFacet $facet): string
{
return Str::of(Str::kebab($facet->localize()))
->__toString();
}
}
@@ -0,0 +1,66 @@
<?php
declare(strict_types=1);
namespace App\Concerns\Filament\Enums;
use Illuminate\Support\Str;
/**
* Trait HasColorOrEmoji.
*/
trait HasColorOrEmoji
{
/**
* Get the enum as an array formatted for a select, but styled.
*
* @param bool|null $hasColor
* @param bool|null $hasEmoji
* @param string|null $locale
* @return array
*/
public static function asSelectArrayStyled(?bool $hasColor = true, ?bool $hasEmoji = true, ?string $locale = null): array
{
$selectArray = [];
/** @var static $case */
foreach (static::cases() as $case) {
$selectArray[$case->value] = $case->localizeStyled($hasColor, $hasEmoji, $locale);
}
return $selectArray;
}
/**
* Localize the enum, but styled.
*
* @param bool|null $hasColor
* @param bool|null $hasEmoji
* @param string|null $locale
* @return string
*/
public function localizeStyled(?bool $hasColor = true, ?bool $hasEmoji = true, ?string $locale = null): string
{
$localizedName = $this->getLocalizedName($locale) ?? $this->getPrettyName();
$name = Str::of('');
if ($hasColor) {
$color = $this->getColor();
$name = $name->append("<p style='color: rgb($color);'>");
}
if ($hasEmoji) {
$emoji = $this->getEmoji();
$name = $name->append("$emoji ");
}
$name = $name->append($localizedName);
if ($hasColor) {
$name = $name->append('</p>');
}
return $name->__toString();
}
}
+31
View File
@@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
namespace App\Concerns\Filament;
use App\Filament\Tabs\BaseTab;
/**
* Trait HasTabs.
*/
trait HasTabs
{
/**
* Get the tabs for an array key-mapped.
*
* @param class-string<BaseTab>[] $tabClasses
* @return array
*/
public function toArray(array $tabClasses): array
{
$tabs = [];
foreach ($tabClasses as $class) {
if ((new $class)->shouldBeHidden()) continue;
$tabs[$class::getKey()] = $class::make();
}
return $tabs;
}
}
@@ -43,7 +43,7 @@ trait ReconcilesDumpRepositories
* *
* @return ReconcileRepositoriesAction * @return ReconcileRepositoriesAction
*/ */
protected function action(): ReconcileRepositoriesAction protected function reconcileAction(): ReconcileRepositoriesAction
{ {
return new ReconcileDumpRepositoriesAction(); return new ReconcileDumpRepositoriesAction();
} }
@@ -43,7 +43,7 @@ trait ReconcilesRepositories
$this->handleFilters($sourceRepository, $destinationRepository, $data); $this->handleFilters($sourceRepository, $destinationRepository, $data);
$action = $this->action(); $action = $this->reconcileAction();
return $action->reconcileRepositories($sourceRepository, $destinationRepository); return $action->reconcileRepositories($sourceRepository, $destinationRepository);
} }
@@ -53,7 +53,7 @@ trait ReconcilesRepositories
* *
* @return ReconcileRepositoriesAction * @return ReconcileRepositoriesAction
*/ */
abstract protected function action(): ReconcileRepositoriesAction; abstract protected function reconcileAction(): ReconcileRepositoriesAction;
/** /**
* Get source repository. * Get source repository.
@@ -43,7 +43,7 @@ trait ReconcilesAudioRepositories
* *
* @return ReconcileRepositoriesAction * @return ReconcileRepositoriesAction
*/ */
protected function action(): ReconcileRepositoriesAction protected function reconcileAction(): ReconcileRepositoriesAction
{ {
return new ReconcileAudioRepositoriesAction(); return new ReconcileAudioRepositoriesAction();
} }
@@ -43,7 +43,7 @@ trait ReconcilesVideoRepositories
* *
* @return ReconcileRepositoriesAction * @return ReconcileRepositoriesAction
*/ */
protected function action(): ReconcileRepositoriesAction protected function reconcileAction(): ReconcileRepositoriesAction
{ {
return new ReconcileVideoRepositoriesAction(); return new ReconcileVideoRepositoriesAction();
} }
@@ -43,7 +43,7 @@ trait ReconcilesScriptRepositories
* *
* @return ReconcileRepositoriesAction * @return ReconcileRepositoriesAction
*/ */
protected function action(): ReconcileRepositoriesAction protected function reconcileAction(): ReconcileRepositoriesAction
{ {
return new ReconcileScriptRepositoriesAction(); return new ReconcileScriptRepositoriesAction();
} }
+18
View File
@@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
namespace App\Contracts\Models;
/**
* Interface HasSubtitle.
*/
interface HasSubtitle
{
/**
* Get subtitle.
*
* @return string
*/
public function getSubtitle(): string;
}
+2
View File
@@ -13,6 +13,8 @@ enum SpecialPermission: string
case BYPASS_FEATURE_FLAGS = 'bypass feature flags'; case BYPASS_FEATURE_FLAGS = 'bypass feature flags';
case VIEW_FILAMENT = 'view filament';
case VIEW_HORIZON = 'view horizon'; case VIEW_HORIZON = 'view horizon';
case VIEW_NOVA = 'view nova'; case VIEW_NOVA = 'view nova';
+32
View File
@@ -5,16 +5,48 @@ declare(strict_types=1);
namespace App\Enums\Models\Wiki; namespace App\Enums\Models\Wiki;
use App\Concerns\Enums\LocalizesName; use App\Concerns\Enums\LocalizesName;
use App\Concerns\Filament\Enums\HasColorOrEmoji;
/** /**
* Enum AnimeSeason. * Enum AnimeSeason.
*/ */
enum AnimeSeason: int enum AnimeSeason: int
{ {
use HasColorOrEmoji;
use LocalizesName; use LocalizesName;
case WINTER = 0; case WINTER = 0;
case SPRING = 1; case SPRING = 1;
case SUMMER = 2; case SUMMER = 2;
case FALL = 3; case FALL = 3;
/**
* Get the rgb color for the enum.
*
* @return string
*/
public function getColor(): string
{
return match ($this) {
static::WINTER => '153, 204, 255',
static::SPRING => '255, 192, 203',
static::SUMMER => '255, 153, 51',
static::FALL => '204, 102, 0',
};
}
/**
* Get the unicode emoji for the enum.
*
* @return string
*/
public function getEmoji(): string
{
return match ($this) {
static::WINTER => "\u{2744}",
static::SPRING => "\u{1F33C}",
static::SUMMER => "\u{2600}",
static::FALL => "\u{1F342}",
};
}
} }
@@ -0,0 +1,51 @@
<?php
declare(strict_types=1);
namespace App\Filament\Actions\Discord;
use App\Actions\Discord\DiscordThreadAction as DiscordThreadActionAction;
use App\Models\Wiki\Anime;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Form;
use Filament\Tables\Actions\Action;
use Illuminate\Database\Eloquent\Model;
/**
* Class DiscordThreadAction.
*/
class DiscordThreadAction extends Action
{
/**
* Initial setup for the action.
*
* @return void
*/
protected function setUp(): void
{
parent::setUp();
$this->fillForm(fn (Anime $record): array => ['name' => $record->getName()]);
$this->action(fn (Anime $record, array $data) => (new DiscordThreadActionAction())->handle($record, $data));
}
/**
* Get the fields available on the action.
*
* @param Form $form
* @return Form
*/
public function getForm(Form $form): Form
{
return $form
->schema([
TextInput::make('name')
->label(__('filament.actions.discord.thread.name'))
->helperText(__('filament.actions.discord.thread.help'))
->required()
->maxlength(100)
->rules(['required', 'max:100']),
]);
}
}
@@ -0,0 +1,65 @@
<?php
declare(strict_types=1);
namespace App\Filament\Actions\Models;
use App\Actions\Models\AssignHashidsAction as AssignHashids;
use App\Contracts\Models\HasHashids;
use App\Models\BaseModel;
use Exception;
use Filament\Tables\Actions\Action;
/**
* Class AssignHashidsAction.
*/
class AssignHashidsAction extends Action
{
protected ?string $connection = null;
/**
* Initial setup for the action.
*
* @return void
*/
protected function setUp(): void
{
parent::setUp();
$this->action(fn (BaseModel $record) => $this->handle($record));
}
/**
* Perform the action on the given models.
*
* @param BaseModel $model
* @return void
*
* @noinspection PhpUnusedParameterInspection
*/
public function handle(BaseModel $model): void
{
$action = new AssignHashids();
if ($model instanceof HasHashids) {
try {
$action->assign($model, $this->connection);
} catch (Exception $e) {
//$this->markAsFailed($model, $e);
}
}
}
/**
* Set the connection.
*
* @param string|null $connection
* @return static
*/
public function setConnection(?string $connection): static
{
$this->connection = $connection;
return $this;
}
}
@@ -0,0 +1,72 @@
<?php
declare(strict_types=1);
namespace App\Filament\Actions\Models\Auth\Permission;
use App\Filament\Components\Fields\Select;
use App\Models\Auth\Permission;
use App\Models\Auth\Role;
use Filament\Forms\Form;
use Filament\Tables\Actions\Action;
use Illuminate\Support\Arr;
/**
* Class GiveRoleAction.
*/
class GiveRoleAction extends Action
{
final public const FIELD_ROLE = 'role';
/**
* Initial setup for the action.
*
* @return void
*/
protected function setUp(): void
{
parent::setUp();
$this->action(fn (Permission $record, array $data) => $this->handle($record, $data));
}
/**
* Perform the action on the given models.
*
* @param Permission $permission
* @param array $data
* @return void
*/
public function handle(Permission $permission, array $data): void
{
$role = Role::findById(intval(Arr::get($data, self::FIELD_ROLE)));
$permission->assignRole($role);
}
/**
* Get the fields available on the action.
*
* @param Form $form
* @return Form
*
* @noinspection PhpMissingParentCallCommonInspection
*/
public function getForm(Form $form): Form
{
$roles = Role::all([Role::ATTRIBUTE_ID, Role::ATTRIBUTE_NAME])
->keyBy(Role::ATTRIBUTE_ID)
->map(fn (Role $role) => $role->name)
->toArray();
return $form
->schema([
Select::make(self::FIELD_ROLE)
->label(__('filament.resources.singularLabel.role'))
->searchable()
->required()
->options($roles)
->rules('required'),
]);
}
}
@@ -0,0 +1,72 @@
<?php
declare(strict_types=1);
namespace App\Filament\Actions\Models\Auth\Permission;
use App\Filament\Components\Fields\Select;
use App\Models\Auth\Permission;
use App\Models\Auth\Role;
use Filament\Forms\Form;
use Filament\Tables\Actions\Action;
use Illuminate\Support\Arr;
/**
* Class RevokeRoleAction.
*/
class RevokeRoleAction extends Action
{
final public const FIELD_ROLE = 'role';
/**
* Initial setup for the action.
*
* @return void
*/
protected function setUp(): void
{
parent::setUp();
$this->action(fn (Permission $record, array $data) => $this->handle($record, $data));
}
/**
* Perform the action on the given models.
*
* @param Permission $permission
* @param array $data
* @return void
*/
public function handle(Permission $permission, array $data): void
{
$role = Role::findById(intval(Arr::get($data, self::FIELD_ROLE)));
$permission->removeRole($role);
}
/**
* Get the fields available on the action.
*
* @param Form $form
* @return Form
*
* @noinspection PhpMissingParentCallCommonInspection
*/
public function getForm(Form $form): Form
{
$roles = Role::all([Role::ATTRIBUTE_ID, Role::ATTRIBUTE_NAME])
->keyBy(Role::ATTRIBUTE_ID)
->map(fn (Role $role) => $role->name)
->toArray();
return $form
->schema([
Select::make(self::FIELD_ROLE)
->label(__('filament.resources.singularLabel.role'))
->searchable()
->required()
->options($roles)
->rules('required'),
]);
}
}
@@ -0,0 +1,72 @@
<?php
declare(strict_types=1);
namespace App\Filament\Actions\Models\Auth\Role;
use App\Filament\Components\Fields\Select;
use App\Models\Auth\Permission;
use App\Models\Auth\Role;
use Filament\Forms\Form;
use Filament\Tables\Actions\Action;
use Illuminate\Support\Arr;
/**
* Class GivePermissionAction.
*/
class GivePermissionAction extends Action
{
final public const FIELD_PERMISSION = 'permission';
/**
* Initial setup for the action.
*
* @return void
*/
protected function setUp(): void
{
parent::setUp();
$this->action(fn (Role $record, array $data) => $this->handle($record, $data));
}
/**
* Perform the action on the given model.
*
* @param Role $role
* @param array $data
* @return void
*/
public function handle(Role $role, array $data): void
{
$permission = Permission::findById(intval(Arr::get($data, self::FIELD_PERMISSION)));
$role->givePermissionTo($permission);
}
/**
* Get the fields available on the action.
*
* @param Form $form
* @return Form
*
* @noinspection PhpMissingParentCallCommonInspection
*/
public function getForm(Form $form): Form
{
$permissions = Permission::all([Permission::ATTRIBUTE_ID, Permission::ATTRIBUTE_NAME])
->keyBy(Permission::ATTRIBUTE_ID)
->map(fn (Permission $permission) => $permission->name)
->toArray();
return $form
->schema([
Select::make(self::FIELD_PERMISSION)
->label(__('filament.resources.singularLabel.permission'))
->searchable()
->required()
->options($permissions)
->rules('required'),
]);
}
}
@@ -0,0 +1,72 @@
<?php
declare(strict_types=1);
namespace App\Filament\Actions\Models\Auth\Role;
use App\Filament\Components\Fields\Select;
use App\Models\Auth\Permission;
use App\Models\Auth\Role;
use Filament\Forms\Form;
use Filament\Tables\Actions\Action;
use Illuminate\Support\Arr;
/**
* Class RevokePermissionAction.
*/
class RevokePermissionAction extends Action
{
final public const FIELD_PERMISSION = 'permission';
/**
* Initial setup for the action.
*
* @return void
*/
protected function setUp(): void
{
parent::setUp();
$this->action(fn (Role $record, array $data) => $this->handle($record, $data));
}
/**
* Perform the action on the given model.
*
* @param Role $role
* @param array $data
* @return void
*/
public function handle(Role $role, array $data): void
{
$permission = Permission::findById(intval(Arr::get($data, self::FIELD_PERMISSION)));
$role->revokePermissionTo($permission);
}
/**
* Get the fields available on the action.
*
* @param Form $form
* @return Form
*
* @noinspection PhpMissingParentCallCommonInspection
*/
public function getForm(Form $form): Form
{
$permissions = Permission::all([Permission::ATTRIBUTE_ID, Permission::ATTRIBUTE_NAME])
->keyBy(Permission::ATTRIBUTE_ID)
->map(fn (Permission $permission) => $permission->name)
->toArray();
return $form
->schema([
Select::make(self::FIELD_PERMISSION)
->label(__('filament.resources.singularLabel.permission'))
->searchable()
->required()
->options($permissions)
->rules('required'),
]);
}
}
@@ -0,0 +1,72 @@
<?php
declare(strict_types=1);
namespace App\Filament\Actions\Models\Auth\User;
use App\Filament\Components\Fields\Select;
use App\Models\Auth\Permission;
use App\Models\Auth\User;
use Filament\Forms\Form;
use Filament\Tables\Actions\Action;
use Illuminate\Support\Arr;
/**
* Class GivePermissionAction.
*/
class GivePermissionAction extends Action
{
final public const FIELD_PERMISSION = 'permission';
/**
* Initial setup for the action.
*
* @return void
*/
protected function setUp(): void
{
parent::setUp();
$this->action(fn (User $record, array $data) => $this->handle($record, $data));
}
/**
* Perform the action on the given model.
*
* @param User $user
* @param array $data
* @return void
*/
public function handle(User $user, array $data): void
{
$permission = Permission::findById(intval(Arr::get($data, self::FIELD_PERMISSION)));
$user->givePermissionTo($permission);
}
/**
* Get the fields available on the action.
*
* @param Form $form
* @return Form
*
* @noinspection PhpMissingParentCallCommonInspection
*/
public function getForm(Form $form): Form
{
$permissions = Permission::all([Permission::ATTRIBUTE_ID, Permission::ATTRIBUTE_NAME])
->keyBy(Permission::ATTRIBUTE_ID)
->map(fn (Permission $permission) => $permission->name)
->toArray();
return $form
->schema([
Select::make(self::FIELD_PERMISSION)
->label(__('filament.resources.singularLabel.permission'))
->searchable()
->required()
->options($permissions)
->rules('required'),
]);
}
}
@@ -0,0 +1,72 @@
<?php
declare(strict_types=1);
namespace App\Filament\Actions\Models\Auth\User;
use App\Filament\Components\Fields\Select;
use App\Models\Auth\Role;
use App\Models\Auth\User;
use Filament\Forms\Form;
use Filament\Tables\Actions\Action;
use Illuminate\Support\Arr;
/**
* Class GiveRoleAction.
*/
class GiveRoleAction extends Action
{
final public const FIELD_ROLE = 'role';
/**
* Initial setup for the action.
*
* @return void
*/
protected function setUp(): void
{
parent::setUp();
$this->action(fn (User $record, array $data) => $this->handle($record, $data));
}
/**
* Perform the action on the given models.
*
* @param User $user
* @param array $data
* @return void
*/
public function handle(User $user, array $data): void
{
$role = Role::findById(intval(Arr::get($data, self::FIELD_ROLE)));
$user->assignRole($role);
}
/**
* Get the fields available on the action.
*
* @param Form $form
* @return Form
*
* @noinspection PhpMissingParentCallCommonInspection
*/
public function getForm(Form $form): Form
{
$roles = Role::all([Role::ATTRIBUTE_ID, Role::ATTRIBUTE_NAME])
->keyBy(Role::ATTRIBUTE_ID)
->map(fn (Role $role) => $role->name)
->toArray();
return $form
->schema([
Select::make(self::FIELD_ROLE)
->label(__('filament.resources.singularLabel.role'))
->searchable()
->required()
->options($roles)
->rules('required'),
]);
}
}
@@ -0,0 +1,72 @@
<?php
declare(strict_types=1);
namespace App\Filament\Actions\Models\Auth\User;
use App\Filament\Components\Fields\Select;
use App\Models\Auth\Permission;
use App\Models\Auth\User;
use Filament\Forms\Form;
use Filament\Tables\Actions\Action;
use Illuminate\Support\Arr;
/**
* Class RevokePermissionAction.
*/
class RevokePermissionAction extends Action
{
final public const FIELD_PERMISSION = 'permission';
/**
* Initial setup for the action.
*
* @return void
*/
protected function setUp(): void
{
parent::setUp();
$this->action(fn (User $record, array $data) => $this->handle($record, $data));
}
/**
* Perform the action on the given model.
*
* @param User $user
* @param array $data
* @return void
*/
public function handle(User $user, array $data): void
{
$permission = Permission::findById(intval(Arr::get($data, self::FIELD_PERMISSION)));
$user->revokePermissionTo($permission);
}
/**
* Get the fields available on the action.
*
* @param Form $form
* @return Form
*
* @noinspection PhpMissingParentCallCommonInspection
*/
public function getForm(Form $form): Form
{
$permissions = Permission::all([Permission::ATTRIBUTE_ID, Permission::ATTRIBUTE_NAME])
->keyBy(Permission::ATTRIBUTE_ID)
->map(fn (Permission $permission) => $permission->name)
->toArray();
return $form
->schema([
Select::make(self::FIELD_PERMISSION)
->label(__('filament.resources.singularLabel.permission'))
->searchable()
->required()
->options($permissions)
->rules('required'),
]);
}
}
@@ -0,0 +1,72 @@
<?php
declare(strict_types=1);
namespace App\Filament\Actions\Models\Auth\User;
use App\Filament\Components\Fields\Select;
use App\Models\Auth\Role;
use App\Models\Auth\User;
use Filament\Forms\Form;
use Filament\Tables\Actions\Action;
use Illuminate\Support\Arr;
/**
* Class RevokeRoleAction.
*/
class RevokeRoleAction extends Action
{
final public const FIELD_ROLE = 'role';
/**
* Initial setup for the action.
*
* @return void
*/
protected function setUp(): void
{
parent::setUp();
$this->action(fn (User $record, array $data) => $this->handle($record, $data));
}
/**
* Perform the action on the given models.
*
* @param User $user
* @param array $data
* @return void
*/
public function handle(User $user, array $data): void
{
$role = Role::findById(intval(Arr::get($data, self::FIELD_ROLE)));
$user->removeRole($role);
}
/**
* Get the fields available on the action.
*
* @param Form $form
* @return Form
*
* @noinspection PhpMissingParentCallCommonInspection
*/
public function getForm(Form $form): Form
{
$roles = Role::all([Role::ATTRIBUTE_ID, Role::ATTRIBUTE_NAME])
->keyBy(Role::ATTRIBUTE_ID)
->map(fn (Role $role) => $role->name)
->toArray();
return $form
->schema([
Select::make(self::FIELD_ROLE)
->label(__('filament.resources.singularLabel.role'))
->searchable()
->required()
->options($roles)
->rules('required'),
]);
}
}
@@ -0,0 +1,27 @@
<?php
declare(strict_types=1);
namespace App\Filament\Actions\Models\Wiki\Anime;
use App\Actions\Models\Wiki\Anime\AttachAnimeImageAction as AttachAnimeImageActionAction;
use App\Filament\Actions\Models\Wiki\AttachImageAction;
use App\Models\Wiki\Anime;
/**
* Class AttachAnimeImageAction.
*/
class AttachAnimeImageAction extends AttachImageAction
{
/**
* Initial setup for the action.
*
* @return void
*/
protected function setUp(): void
{
parent::setUp();
$this->action(fn (Anime $record, array $data) => (new AttachAnimeImageActionAction($this->facets))->handle($record, $data));
}
}
@@ -0,0 +1,41 @@
<?php
declare(strict_types=1);
namespace App\Filament\Actions\Models\Wiki\Anime;
use App\Actions\Models\Wiki\Anime\AttachAnimeResourceAction as AttachAnimeResourceActionAction;
use App\Enums\Models\Wiki\ResourceSite;
use App\Filament\Actions\Models\Wiki\AttachResourceAction;
use App\Models\Wiki\Anime;
use App\Rules\Wiki\Resource\AnimeResourceLinkFormatRule;
use Illuminate\Contracts\Validation\ValidationRule;
/**
* Class AttachAnimeResourceAction.
*/
class AttachAnimeResourceAction extends AttachResourceAction
{
/**
* Initial setup for the action.
*
* @return void
*/
protected function setUp(): void
{
parent::setUp();
$this->action(fn (Anime $record, array $data) => (new AttachAnimeResourceActionAction($this->sites))->handle($record, $data));
}
/**
* Get the format validation rule.
*
* @param ResourceSite $site
* @return ValidationRule
*/
protected function getFormatRule(ResourceSite $site): ValidationRule
{
return new AnimeResourceLinkFormatRule($site);
}
}
@@ -0,0 +1,225 @@
<?php
declare(strict_types=1);
namespace App\Filament\Actions\Models\Wiki\Anime;
use App\Actions\Models\BackfillAction;
use App\Actions\Models\Wiki\Anime\BackfillAnimeOtherResourcesAction;
use App\Actions\Models\Wiki\Anime\BackfillAnimeSynonymsAction;
use App\Actions\Models\Wiki\Anime\Image\BackfillLargeCoverImageAction;
use App\Actions\Models\Wiki\Anime\Image\BackfillSmallCoverImageAction;
use App\Actions\Models\Wiki\Anime\Resource\BackfillAnidbResourceAction;
use App\Actions\Models\Wiki\Anime\Resource\BackfillAnilistResourceAction;
use App\Actions\Models\Wiki\Anime\Resource\BackfillAnnResourceAction;
use App\Actions\Models\Wiki\Anime\Resource\BackfillKitsuResourceAction;
use App\Actions\Models\Wiki\Anime\Resource\BackfillMalResourceAction;
use App\Actions\Models\Wiki\Anime\Studio\BackfillAnimeStudiosAction;
use App\Enums\Models\Wiki\ImageFacet;
use App\Enums\Models\Wiki\ResourceSite;
use App\Models\Wiki\Anime;
use App\Models\Wiki\ExternalResource;
use App\Models\Wiki\Image;
use Exception;
use Filament\Forms\Components\Checkbox;
use Filament\Forms\Components\Section;
use Filament\Forms\Form;
use Filament\Notifications\Actions\Action as NotificationAction;
use Filament\Notifications\Notification;
use Filament\Tables\Actions\Action;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Support\Arr;
use Illuminate\Support\Sleep;
/**
* Class BackfillAnimeAction.
*/
class BackfillAnimeAction extends Action implements ShouldQueue
{
use InteractsWithQueue;
use Queueable;
final public const BACKFILL_ANIDB_RESOURCE = 'backfill_anidb_resource';
final public const BACKFILL_ANILIST_RESOURCE = 'backfill_anilist_resource';
final public const BACKFILL_ANN_RESOURCE = 'backfill_ann_resource';
final public const BACKFILL_KITSU_RESOURCE = 'backfill_kitsu_resource';
final public const BACKFILL_OTHER_RESOURCES = 'backfill_other_resources';
final public const BACKFILL_LARGE_COVER = 'backfill_large_cover';
final public const BACKFILL_MAL_RESOURCE = 'backfill_mal_resource';
final public const BACKFILL_SMALL_COVER = 'backfill_small_cover';
final public const BACKFILL_STUDIOS = 'backfill_studios';
final public const BACKFILL_SYNONYMS = 'backfill_synonyms';
/**
* Initial setup for the action.
*
* @return void
*/
protected function setUp(): void
{
parent::setUp();
$this->action(fn (Anime $record, array $data) => $this->handle($record, $data));
}
/**
* Perform the action on the given models.
*
* @param Anime $anime
* @param array $fields
* @return void
*/
public function handle(Anime $anime, array $fields): void
{
if ($anime->resources()->doesntExist()) {
$this->fail(__('filament.actions.anime.backfill.message.resource_required_failure'));
return;
}
$actions = $this->getActions($fields, $anime);
try {
foreach ($actions as $action) {
$result = $action->handle();
if ($result->hasFailed()) {
Notification::make()
->body($result->getMessage())
->warning()
->actions([
NotificationAction::make('mark-as-read')
->button()
->markAsRead(),
])
->sendToDatabase(auth()->user());
}
}
} catch (Exception $e) {
$this->fail($e);
} finally {
// Try not to upset third-party APIs
Sleep::for(rand(3, 5))->second();
}
}
/**
* Get the fields available on the action.
*
* @param Form $form
* @return Form|null
*
* @noinspection PhpMissingParentCallCommonInspection
*/
public function getForm(Form $form): ?Form
{
$anime = $this->getRecord();
return $form
->schema([
Section::make(__('filament.actions.anime.backfill.fields.resources.name'))
->schema([
Checkbox::make(self::BACKFILL_KITSU_RESOURCE)
->label(__('filament.actions.anime.backfill.fields.resources.kitsu.name'))
->helperText(__('filament.actions.anime.backfill.fields.resources.kitsu.help'))
->default(fn () => $anime instanceof Anime && $anime->resources()->where(ExternalResource::ATTRIBUTE_SITE, ResourceSite::KITSU->value)->doesntExist()),
Checkbox::make(self::BACKFILL_ANILIST_RESOURCE)
->label(__('filament.actions.anime.backfill.fields.resources.anilist.name'))
->helperText(__('filament.actions.anime.backfill.fields.resources.anilist.help'))
->default(fn () => $anime instanceof Anime && $anime->resources()->where(ExternalResource::ATTRIBUTE_SITE, ResourceSite::ANILIST->value)->doesntExist()),
Checkbox::make(self::BACKFILL_MAL_RESOURCE)
->label(__('filament.actions.anime.backfill.fields.resources.mal.name'))
->helperText(__('filament.actions.anime.backfill.fields.resources.mal.help'))
->default(fn () => $anime instanceof Anime && $anime->resources()->where(ExternalResource::ATTRIBUTE_SITE, ResourceSite::MAL->value)->doesntExist()),
Checkbox::make(self::BACKFILL_ANIDB_RESOURCE)
->label(__('filament.actions.anime.backfill.fields.resources.anidb.name'))
->helperText(__('filament.actions.anime.backfill.fields.resources.anidb.help'))
->default(fn () => $anime instanceof Anime && $anime->resources()->where(ExternalResource::ATTRIBUTE_SITE, ResourceSite::ANIDB->value)->doesntExist()),
Checkbox::make(self::BACKFILL_ANN_RESOURCE)
->label(__('filament.actions.anime.backfill.fields.resources.ann.name'))
->helperText(__('filament.actions.anime.backfill.fields.resources.ann.help'))
->default(fn () => $anime instanceof Anime && $anime->resources()->where(ExternalResource::ATTRIBUTE_SITE, ResourceSite::ANN->value)->doesntExist()),
Checkbox::make(self::BACKFILL_OTHER_RESOURCES)
->label(__('filament.actions.anime.backfill.fields.resources.external_links.name'))
->helperText(__('filament.actions.anime.backfill.fields.resources.external_links.help'))
->default(fn () => $anime instanceof Anime),
]),
Section::make(__('filament.actions.anime.backfill.fields.images.name'))
->schema([
Checkbox::make(self::BACKFILL_LARGE_COVER)
->label(__('filament.actions.anime.backfill.fields.images.large_cover.name'))
->helperText(__('filament.actions.anime.backfill.fields.images.large_cover.help'))
->default(fn () => $anime instanceof Anime && $anime->images()->where(Image::ATTRIBUTE_FACET, ImageFacet::COVER_LARGE->value)->doesntExist()),
Checkbox::make(self::BACKFILL_SMALL_COVER)
->label(__('filament.actions.anime.backfill.fields.images.small_cover.name'))
->helperText(__('filament.actions.anime.backfill.fields.images.small_cover.help'))
->default(fn () => $anime instanceof Anime && $anime->images()->where(Image::ATTRIBUTE_FACET, ImageFacet::COVER_SMALL->value)->doesntExist()),
]),
Section::make(__('filament.actions.anime.backfill.fields.studios.name'))
->schema([
Checkbox::make(self::BACKFILL_STUDIOS)
->label(__('filament.actions.anime.backfill.fields.studios.anime.name'))
->helperText(__('filament.actions.anime.backfill.fields.studios.anime.help'))
->default(fn () => $anime instanceof Anime && $anime->studios()->doesntExist()),
]),
Section::make(__('filament.actions.anime.backfill.fields.synonyms.name'))
->schema([
Checkbox::make(self::BACKFILL_STUDIOS)
->label(__('filament.actions.anime.backfill.fields.synonyms.name'))
->helperText(__('filament.actions.anime.backfill.fields.synonyms.help'))
->default(fn () => $anime instanceof Anime && $anime->animesynonyms()->count() === 0),
]),
]);
}
/**
* Get the selected actions for backfilling anime.
*
* @param array $fields
* @param Anime $anime
* @return BackfillAction[]
*/
protected function getActions(array $fields, Anime $anime): array
{
$actions = [];
foreach ($this->getActionMapping($anime) as $field => $action) {
if (Arr::get($fields, $field) === true) {
$actions[] = $action;
}
}
return $actions;
}
/**
* Get the mapping of actions to their form fields.
*
* @param Anime $anime
* @return array<string, BackfillAction>
*/
protected function getActionMapping(Anime $anime): array
{
return [
self::BACKFILL_KITSU_RESOURCE => new BackfillKitsuResourceAction($anime),
self::BACKFILL_ANILIST_RESOURCE => new BackfillAnilistResourceAction($anime),
self::BACKFILL_MAL_RESOURCE => new BackfillMalResourceAction($anime),
self::BACKFILL_ANIDB_RESOURCE => new BackfillAnidbResourceAction($anime),
self::BACKFILL_ANN_RESOURCE => new BackfillAnnResourceAction($anime),
self::BACKFILL_OTHER_RESOURCES => new BackfillAnimeOtherResourcesAction($anime),
self::BACKFILL_LARGE_COVER => new BackfillLargeCoverImageAction($anime),
self::BACKFILL_SMALL_COVER => new BackfillSmallCoverImageAction($anime),
self::BACKFILL_STUDIOS => new BackfillAnimeStudiosAction($anime),
self::BACKFILL_SYNONYMS => new BackfillAnimeSynonymsAction($anime),
];
}
}
@@ -0,0 +1,27 @@
<?php
declare(strict_types=1);
namespace App\Filament\Actions\Models\Wiki\Artist;
use App\Actions\Models\Wiki\Artist\AttachArtistImageAction as AttachArtistImageActionAction;
use App\Filament\Actions\Models\Wiki\AttachImageAction;
use App\Models\Wiki\Artist;
/**
* Class AttachArtistImageAction.
*/
class AttachArtistImageAction extends AttachImageAction
{
/**
* Initial setup for the action.
*
* @return void
*/
protected function setUp(): void
{
parent::setUp();
$this->action(fn (Artist $record, array $data) => (new AttachArtistImageActionAction($this->facets))->handle($record, $data));
}
}
@@ -0,0 +1,41 @@
<?php
declare(strict_types=1);
namespace App\Filament\Actions\Models\Wiki\Artist;
use App\Actions\Models\Wiki\Artist\AttachArtistResourceAction as AttachArtistResourceActionAction;
use App\Enums\Models\Wiki\ResourceSite;
use App\Filament\Actions\Models\Wiki\AttachResourceAction;
use App\Models\Wiki\Artist;
use App\Rules\Wiki\Resource\ArtistResourceLinkFormatRule;
use Illuminate\Contracts\Validation\ValidationRule;
/**
* Class AttachArtistResourceAction.
*/
class AttachArtistResourceAction extends AttachResourceAction
{
/**
* Initial setup for the action.
*
* @return void
*/
protected function setUp(): void
{
parent::setUp();
$this->action(fn (Artist $record, array $data) => (new AttachArtistResourceActionAction($this->sites))->handle($record, $data));
}
/**
* Get the format validation rule.
*
* @param ResourceSite $site
* @return ValidationRule
*/
protected function getFormatRule(ResourceSite $site): ValidationRule
{
return new ArtistResourceLinkFormatRule($site);
}
}
@@ -0,0 +1,62 @@
<?php
declare(strict_types=1);
namespace App\Filament\Actions\Models\Wiki;
use App\Enums\Models\Wiki\ImageFacet;
use App\Models\Wiki\Anime;
use App\Models\Wiki\Artist;
use App\Models\Wiki\Image;
use App\Models\Wiki\Studio;
use Filament\Forms\Components\FileUpload;
use Filament\Forms\Form;
use Filament\Tables\Actions\Action;
/**
* Class AttachImageAction.
*/
abstract class AttachImageAction extends Action
{
protected array $facets = [];
/**
* Get the fields available on the action.
*
* @param Form $form
* @return Form
*/
public function getForm(Form $form): Form
{
$fields = [];
$model = $form->getRecord();
if ($model instanceof Anime || $model instanceof Artist || $model instanceof Studio) {
foreach ($this->facets as $facet) {
$images = $model->images();
if ($images->where(Image::ATTRIBUTE_FACET, $facet->value)->exists()) continue;
$fields[] = FileUpload::make($facet->name)
->label($facet->localize())
->image()
->storeFiles(false);
}
}
return $form
->schema($fields);
}
/**
* Get the facets available for the action.
*
* @param ImageFacet[] $facets
* @return static
*/
public function facets(array $facets): static
{
$this->facets = $facets;
return $this;
}
}
@@ -0,0 +1,76 @@
<?php
declare(strict_types=1);
namespace App\Filament\Actions\Models\Wiki;
use App\Enums\Models\Wiki\ResourceSite;
use App\Models\Wiki\Anime;
use App\Models\Wiki\Artist;
use App\Models\Wiki\ExternalResource;
use App\Models\Wiki\Song;
use App\Models\Wiki\Studio;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Form;
use Filament\Tables\Actions\Action;
use Illuminate\Contracts\Validation\ValidationRule;
/**
* Class AttachResourceAction.
*/
abstract class AttachResourceAction extends Action
{
protected array $sites = [];
/**
* Get the fields available on the action.
*
* @param Form $form
* @return Form|null
*/
public function getForm(Form $form): ?Form
{
$fields = [];
$model = $this->getRecord();
foreach ($this->sites as $resourceSite) {
if ($model instanceof Anime || $model instanceof Artist || $model instanceof Song || $model instanceof Studio) {
$resources = $model->resources();
if ($resources->where(ExternalResource::ATTRIBUTE_SITE, $resourceSite->value)->exists()) continue;
}
$resourceSiteLower = strtolower($resourceSite->name);
$fields[] = TextInput::make($resourceSite->name)
->label($resourceSite->localize())
->helperText(__("filament.actions.models.wiki.attach_resource.fields.{$resourceSiteLower}.help"))
->url()
->maxLength(192)
->rules(['max:192', $this->getFormatRule($resourceSite)]);
}
return $form
->schema($fields);
}
/**
* Get the sites available for the action.
*
* @param ResourceSite[] $sites
* @return static
*/
public function sites($sites): static
{
$this->sites = $sites;
return $this;
}
/**
* Get the format validation rule.
*
* @param ResourceSite $site
* @return ValidationRule
*/
abstract protected function getFormatRule(ResourceSite $site): ValidationRule;
}
@@ -0,0 +1,52 @@
<?php
declare(strict_types=1);
namespace App\Filament\Actions\Models\Wiki\Audio;
use App\Models\Wiki\Anime\Theme\AnimeThemeEntry;
use App\Models\Wiki\Audio;
use App\Models\Wiki\Video;
use Filament\Tables\Actions\Action;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Support\Facades\Log;
/**
* Class AttachAudioToRelatedVideosAction.
*/
class AttachAudioToRelatedVideosAction extends Action implements ShouldQueue
{
/**
* Initial setup for the action.
*
* @return void
*/
protected function setUp(): void
{
parent::setUp();
$this->action(fn (Audio $record) => $this->handle($record));
}
/**
* Perform the action on the given models.
*
* @param Audio $audio
* @return void
*/
public function handle(Audio $audio): void
{
$video = $audio->videos()->first();
$video->animethemeentries()->each(function (AnimeThemeEntry $firstEntry) use ($audio) {
$theme = $firstEntry->animetheme()->first();
$theme->animethemeentries()->each(function (AnimeThemeEntry $entry) use ($audio) {
$entry->videos()->each(function (Video $video) use ($audio) {
Log::info("Associating Audio '{$audio->filename}' with Video '{$video->filename}'");
$video->audio()->associate($audio)->save();
});
});
});
}
}
@@ -0,0 +1,41 @@
<?php
declare(strict_types=1);
namespace App\Filament\Actions\Models\Wiki\Song;
use App\Actions\Models\Wiki\Song\AttachSongResourceAction as AttachSongResourceActionAction;
use App\Enums\Models\Wiki\ResourceSite;
use App\Filament\Actions\Models\Wiki\AttachResourceAction;
use App\Models\Wiki\Song;
use App\Rules\Wiki\Resource\SongResourceLinkFormatRule;
use Illuminate\Contracts\Validation\ValidationRule;
/**
* Class AttachSongResourceAction.
*/
class AttachSongResourceAction extends AttachResourceAction
{
/**
* Initial setup for the action.
*
* @return void
*/
protected function setUp(): void
{
parent::setUp();
$this->action(fn (Song $record, array $data) => (new AttachSongResourceActionAction($this->sites))->handle($record, $data));
}
/**
* Get the format validation rule.
*
* @param ResourceSite $site
* @return ValidationRule
*/
protected function getFormatRule(ResourceSite $site): ValidationRule
{
return new SongResourceLinkFormatRule($site);
}
}
@@ -0,0 +1,27 @@
<?php
declare(strict_types=1);
namespace App\Filament\Actions\Models\Wiki\Studio;
use App\Actions\Models\Wiki\Studio\AttachStudioImageAction as AttachStudioImageActionAction;
use App\Filament\Actions\Models\Wiki\AttachImageAction;
use App\Models\Wiki\Studio;
/**
* Class AttachStudioImageAction.
*/
class AttachStudioImageAction extends AttachImageAction
{
/**
* Initial setup for the action.
*
* @return void
*/
protected function setUp(): void
{
parent::setUp();
$this->action(fn (Studio $record, array $data) => (new AttachStudioImageActionAction($this->facets))->handle($record, $data));
}
}
@@ -0,0 +1,41 @@
<?php
declare(strict_types=1);
namespace App\Filament\Actions\Models\Wiki\Studio;
use App\Actions\Models\Wiki\Studio\AttachStudioResourceAction as AttachStudioResourceActionAction;
use App\Enums\Models\Wiki\ResourceSite;
use App\Filament\Actions\Models\Wiki\AttachResourceAction;
use App\Models\Wiki\Studio;
use App\Rules\Wiki\Resource\StudioResourceLinkFormatRule;
use Illuminate\Contracts\Validation\ValidationRule;
/**
* Class AttachStudioResourceAction.
*/
class AttachStudioResourceAction extends AttachResourceAction
{
/**
* Initial setup for the action.
*
* @return void
*/
protected function setUp(): void
{
parent::setUp();
$this->action(fn (Studio $record, array $data) => (new AttachStudioResourceActionAction($this->sites))->handle($record, $data));
}
/**
* Get the format validation rule.
*
* @param ResourceSite $site
* @return ValidationRule
*/
protected function getFormatRule(ResourceSite $site): ValidationRule
{
return new StudioResourceLinkFormatRule($site);
}
}
@@ -0,0 +1,142 @@
<?php
declare(strict_types=1);
namespace App\Filament\Actions\Models\Wiki\Studio;
use App\Actions\Models\BackfillAction;
use App\Actions\Models\Wiki\Studio\Image\BackfillLargeCoverImageAction;
use App\Enums\Models\Wiki\ImageFacet;
use App\Models\Wiki\Image;
use App\Models\Wiki\Studio;
use Exception;
use Filament\Forms\Components\Checkbox;
use Filament\Forms\Components\Section;
use Filament\Forms\Form;
use Filament\Notifications\Actions\Action as NotificationAction;
use Filament\Notifications\Notification;
use Filament\Tables\Actions\Action;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Support\Arr;
use Illuminate\Support\Sleep;
/**
* Class BackfillStudioAction.
*/
class BackfillStudioAction extends Action implements ShouldQueue
{
use InteractsWithQueue;
use Queueable;
final public const BACKFILL_LARGE_COVER = 'backfill_large_cover';
/**
* Initial setup for the action.
*
* @return void
*/
protected function setUp(): void
{
parent::setUp();
$this->action(fn (Studio $record, array $data) => $this->handle($record, $data));
}
/**
* Perform the action on the given models.
*
* @param Studio $studio
* @param array $fields
* @return void
*/
public function handle(Studio $studio, array $fields): void
{
if ($studio->resources()->doesntExist()) {
//$this->markAsFailed($studio, __('filament.actions.studio.backfill.message.resource_required_failure'));
return;
}
$actions = $this->getActions($fields, $studio);
try {
foreach ($actions as $action) {
$result = $action->handle();
if ($result->hasFailed()) {
Notification::make()
->body($result->getMessage())
->warning()
->actions([
NotificationAction::make('mark-as-read')
->button()
->markAsRead(),
])
->sendToDatabase(auth()->user());
}
}
} catch (Exception $e) {
//$this->markAsFailed($studio, $e);
} finally {
// Try not to upset third-party APIs
Sleep::for(rand(3, 5))->second();
}
}
/**
* Get the fields available on the action.
*
* @param Form $form
* @return Form
*
* @noinspection PhpMissingParentCallCommonInspection
*/
public function getForm(Form $form): Form
{
$studio = $this->getRecord();
return $form
->schema([
Section::make(__('filament.actions.studio.backfill.fields.images.name'))
->schema([
Checkbox::make(self::BACKFILL_LARGE_COVER)
->label(__('filament.actions.studio.backfill.fields.images.large_cover.name'))
->helperText(__('filament.actions.studio.backfill.fields.images.large_cover.help'))
->default(fn () => $studio instanceof Studio && $studio->images()->where(Image::ATTRIBUTE_FACET, ImageFacet::COVER_LARGE->value)->doesntExist()),
]),
]);
}
/**
* Get the selected actions for backfilling studios.
*
* @param array $fields
* @param Studio $studio
* @return BackfillAction[]
*/
protected function getActions(array $fields, Studio $studio): array
{
$actions = [];
foreach ($this->getActionMapping($studio) as $field => $action) {
if (Arr::get($fields, $field) === true) {
$actions[] = $action;
}
}
return $actions;
}
/**
* Get the mapping of actions to their form fields.
*
* @param Studio $studio
* @return array<string, BackfillAction>
*/
protected function getActionMapping(Studio $studio): array
{
return [
self::BACKFILL_LARGE_COVER => new BackfillLargeCoverImageAction($studio),
];
}
}
@@ -0,0 +1,105 @@
<?php
declare(strict_types=1);
namespace App\Filament\Actions\Models\Wiki\Video;
use App\Actions\Models\Wiki\Video\Audio\BackfillAudioAction as BackfillAudio;
use App\Enums\Actions\Models\Wiki\Video\DeriveSourceVideo;
use App\Enums\Actions\Models\Wiki\Video\OverwriteAudio;
use App\Filament\Components\Fields\Select;
use App\Models\Wiki\Video;
use Exception;
use Filament\Forms\Form;
use Filament\Notifications\Actions\Action as NotificationAction;
use Filament\Notifications\Notification;
use Filament\Tables\Actions\Action;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Support\Arr;
use Illuminate\Validation\Rules\Enum;
/**
* Class BackfillAudioAction.
*/
class BackfillAudioAction extends Action implements ShouldQueue
{
use InteractsWithQueue;
use Queueable;
final public const DERIVE_SOURCE_VIDEO = 'derive_source_video';
final public const OVERWRITE_AUDIO = 'overwrite_audio';
/**
* Initial setup for the action.
*
* @return void
*/
protected function setUp(): void
{
parent::setUp();
$this->action(fn (Video $record, array $data) => $this->handle($record, $data));
}
/**
* Perform the action on the given models.
*
* @param Video $video
* @param array $data
* @return void
*/
public function handle(Video $video, array $data): void
{
$deriveSourceVideo = DeriveSourceVideo::from(intval(Arr::get($data, self::DERIVE_SOURCE_VIDEO)));
$overwriteAudio = OverwriteAudio::from(intval(Arr::get($data, self::OVERWRITE_AUDIO)));
$action = new BackfillAudio($video, $deriveSourceVideo, $overwriteAudio);
try {
$result = $action->handle();
if ($result->hasFailed()) {
Notification::make()
->body($result->getMessage())
->warning()
->actions([
NotificationAction::make('mark-as-read')
->button()
->markAsRead(),
])
->sendToDatabase(auth()->user());
}
} catch (Exception $e) {
//$this->markAsFailed($video, $e);
}
}
/**
* Get the fields available on the action.
*
* @param Form $form
* @return Form
*
* @noinspection PhpMissingParentCallCommonInspection
*/
public function getForm(Form $form): Form
{
return $form
->schema([
Select::make(self::DERIVE_SOURCE_VIDEO)
->label(__('filament.actions.video.backfill.fields.derive_source.name'))
->options(DeriveSourceVideo::asSelectArray())
->rules(['required', new Enum(DeriveSourceVideo::class)])
->default(DeriveSourceVideo::YES->value)
->helperText(__('filament.actions.video.backfill.fields.derive_source.help')),
Select::make(self::OVERWRITE_AUDIO)
->label(__('filament.actions.video.backfill.fields.overwrite.name'))
->options(OverwriteAudio::asSelectArray())
->rules(['required', new Enum(OverwriteAudio::class)])
->default(OverwriteAudio::NO->value)
->helperText(__('filament.actions.video.backfill.fields.overwrite.help')),
]);
}
}
@@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
namespace App\Filament\Actions\Storage\Base;
use App\Actions\Storage\Base\DeleteAction as BaseDeleteAction;
use App\Filament\Actions\Storage\StorageAction;
use App\Models\BaseModel;
/**
* Class DeleteAction.
*/
abstract class DeleteAction extends StorageAction
{
/**
* Get the underlying storage action.
*
* @param BaseModel $model
* @param array $fields
* @return BaseDeleteAction
*/
abstract protected function storageAction(BaseModel $model, array $fields): BaseDeleteAction;
}
@@ -0,0 +1,68 @@
<?php
declare(strict_types=1);
namespace App\Filament\Actions\Storage\Base;
use App\Actions\Storage\Base\MoveAction as BaseMoveAction;
use App\Contracts\Storage\InteractsWithDisk;
use App\Filament\Actions\Storage\StorageAction;
use App\Models\BaseModel;
use App\Rules\Storage\StorageFileDirectoryExistsRule;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Form;
use Illuminate\Support\Facades\Storage;
/**
* Class MoveAction.
*/
abstract class MoveAction extends StorageAction implements InteractsWithDisk
{
/**
* Get the fields available on the action.
*
* @param Form $form
* @return Form
*
* @noinspection PhpMissingParentCallCommonInspection
*/
public function getForm(Form $form): Form
{
$defaultPath = $this->defaultPath();
$fs = Storage::disk($this->disk());
return $form
->schema([
TextInput::make('path')
->label(__('filament.actions.storage.move.fields.path.name'))
->helperText(__('filament.actions.storage.move.fields.path.help'))
->required()
->rules(['required', 'string', 'doesnt_start_with:/', "ends_with:{$this->allowedFileExtension()}", new StorageFileDirectoryExistsRule($fs)])
->default(fn () => $defaultPath),
]);
}
/**
* Get the underlying storage action.
*
* @param BaseModel $model
* @param array $fields
* @return BaseMoveAction
*/
abstract protected function storageAction(BaseModel $model, array $fields): BaseMoveAction;
/**
* Resolve the default value for the path field.
*
* @return string|null
*/
abstract protected function defaultPath(): ?string;
/**
* The file extension that the path must end with.
*
* @return string
*/
abstract protected function allowedFileExtension(): string;
}
@@ -0,0 +1,56 @@
<?php
declare(strict_types=1);
namespace App\Filament\Actions\Storage;
use App\Contracts\Actions\Storage\StorageAction as BaseStorageAction;
use App\Models\BaseModel;
use Filament\Tables\Actions\Action;
/**
* Class StorageAction.
*/
abstract class StorageAction extends Action
{
/**
* Initial setup for the action.
*
* @return void
*/
protected function setUp(): void
{
parent::setUp();
$this->action(fn (BaseModel $record, array $data) => $this->handle($record, $data));
}
/**
* Get the underlying storage action.
*
* @param BaseModel $model
* @param array $fields
* @return BaseStorageAction
*/
abstract protected function storageAction(BaseModel $model, array $fields): BaseStorageAction;
/**
* Perform the action on the given models.
*
* @param BaseModel $model
* @param array $fields
* @return void
*/
public function handle(BaseModel $model, array $fields): void
{
$action = $this->storageAction($model, $fields);
$storageResults = $action->handle();
$storageResults->toLog();
$action->then($storageResults);
$actionResult = $storageResults->toActionResult();
}
}
@@ -0,0 +1,28 @@
<?php
declare(strict_types=1);
namespace App\Filament\Actions\Storage\Wiki\Audio;
use App\Actions\Storage\Wiki\Audio\DeleteAudioAction as DeleteAudio;
use App\Filament\Actions\Storage\Base\DeleteAction;
use App\Models\BaseModel;
use App\Models\Wiki\Audio;
/**
* Class DeleteAudioAction.
*/
class DeleteAudioAction extends DeleteAction
{
/**
* Get the underlying storage action.
*
* @param Audio $audio
* @param array $fields
* @return DeleteAudio
*/
protected function storageAction(BaseModel $audio, array $fields): DeleteAudio
{
return new DeleteAudio($audio);
}
}
@@ -0,0 +1,68 @@
<?php
declare(strict_types=1);
namespace App\Filament\Actions\Storage\Wiki\Audio;
use App\Actions\Storage\Wiki\Audio\MoveAudioAction as MoveAudio;
use App\Constants\Config\AudioConstants;
use App\Models\Wiki\Audio;
use App\Filament\Actions\Storage\Base\MoveAction;
use App\Models\BaseModel;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Config;
/**
* Class MoveAudioAction.
*/
class MoveAudioAction extends MoveAction
{
/**
* Get the underlying storage action.
*
* @param Audio $audio
* @param array $fields
* @return MoveAudio
*/
protected function storageAction(BaseModel $audio, array $fields): MoveAudio
{
/** @var string $path */
$path = Arr::get($fields, 'path');
return new MoveAudio($audio, $path);
}
/**
* The name of the disk.
*
* @return string
*/
public function disk(): string
{
return Config::get(AudioConstants::DEFAULT_DISK_QUALIFIED);
}
/**
* Resolve the default value for the path field.
*
* @return string|null
*/
protected function defaultPath(): ?string
{
$audio = $this->getRecord();
return $audio instanceof Audio
? $audio->path
: null;
}
/**
* The file extension that the path must end with.
*
* @return string
*/
protected function allowedFileExtension(): string
{
return '.ogg';
}
}
@@ -0,0 +1,28 @@
<?php
declare(strict_types=1);
namespace App\Filament\Actions\Storage\Wiki\Video;
use App\Actions\Storage\Wiki\Video\DeleteVideoAction as DeleteVideo;
use App\Filament\Actions\Storage\Base\DeleteAction;
use App\Models\BaseModel;
use App\Models\Wiki\Video;
/**
* Class DeleteVideoAction.
*/
class DeleteVideoAction extends DeleteAction
{
/**
* Get the underlying storage action.
*
* @param Video $video
* @param array $fields
* @return DeleteVideo
*/
protected function storageAction(BaseModel $video, array $fields): DeleteVideo
{
return new DeleteVideo($video);
}
}
@@ -0,0 +1,68 @@
<?php
declare(strict_types=1);
namespace App\Filament\Actions\Storage\Wiki\Video;
use App\Actions\Storage\Wiki\Video\MoveVideoAction as MoveVideo;
use App\Constants\Config\VideoConstants;
use App\Models\Wiki\Video;
use App\Filament\Actions\Storage\Base\MoveAction;
use App\Models\BaseModel;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Config;
/**
* Class MoveVideoAction.
*/
class MoveVideoAction extends MoveAction
{
/**
* Get the underlying storage action.
*
* @param Video $video
* @param array $fields
* @return MoveVideo
*/
protected function storageAction(BaseModel $video, array $fields): MoveVideo
{
/** @var string $path */
$path = Arr::get($fields, 'path');
return new MoveVideo($video, $path);
}
/**
* The name of the disk.
*
* @return string
*/
public function disk(): string
{
return Config::get(VideoConstants::DEFAULT_DISK_QUALIFIED);
}
/**
* Resolve the default value for the path field.
*
* @return string|null
*/
protected function defaultPath(): ?string
{
$video = $this->getRecord();
return $video instanceof Video
? $video->path
: null;
}
/**
* The file extension that the path must end with.
*
* @return string
*/
protected function allowedFileExtension(): string
{
return '.webm';
}
}
@@ -0,0 +1,28 @@
<?php
declare(strict_types=1);
namespace App\Filament\Actions\Storage\Wiki\Video\Script;
use App\Actions\Storage\Wiki\Video\Script\DeleteScriptAction as DeleteScript;
use App\Filament\Actions\Storage\Base\DeleteAction;
use App\Models\BaseModel;
use App\Models\Wiki\Video\VideoScript;
/**
* Class DeleteScriptAction.
*/
class DeleteScriptAction extends DeleteAction
{
/**
* Get the underlying storage action.
*
* @param VideoScript $script
* @param array $fields
* @return DeleteScript
*/
protected function storageAction(BaseModel $script, array $fields): DeleteScript
{
return new DeleteScript($script);
}
}
@@ -0,0 +1,68 @@
<?php
declare(strict_types=1);
namespace App\Filament\Actions\Storage\Wiki\Video\Script;
use App\Actions\Storage\Wiki\Video\Script\MoveScriptAction as MoveScript;
use App\Constants\Config\VideoConstants;
use App\Models\Wiki\Video\VideoScript;
use App\Filament\Actions\Storage\Base\MoveAction;
use App\Models\BaseModel;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Config;
/**
* Class MoveScriptAction.
*/
class MoveScriptAction extends MoveAction
{
/**
* Get the underlying storage action.
*
* @param VideoScript $script
* @param array $fields
* @return MoveScript
*/
protected function storageAction(BaseModel $script, array $fields): MoveScript
{
/** @var string $path */
$path = Arr::get($fields, 'path');
return new MoveScript($script, $path);
}
/**
* The name of the disk.
*
* @return string
*/
public function disk(): string
{
return Config::get(VideoConstants::SCRIPT_DISK_QUALIFIED);
}
/**
* Resolve the default value for the path field.
*
* @return string|null
*/
protected function defaultPath(): ?string
{
$script = $this->getRecord();
return $script instanceof VideoScript
? $script->path
: null;
}
/**
* The file extension that the path must end with.
*
* @return string
*/
protected function allowedFileExtension(): string
{
return '.txt';
}
}
@@ -0,0 +1,58 @@
<?php
declare(strict_types=1);
namespace App\Filament\Components\Columns;
use App\Filament\Resources\BaseRelationManager;
use App\Filament\Resources\BaseResource;
use App\Models\BaseModel;
use Filament\Support\Enums\FontWeight;
use Filament\Tables\Columns\TextColumn as ColumnsTextColumn;
/**
* Class TextColumn.
*/
class TextColumn extends ColumnsTextColumn
{
/**
* Used for column relationships.
*
* @param class-string<BaseResource> $resourceRelated
* @param string $relation
* @param bool|null $shouldUseName
* @return static
*/
public function urlToRelated(string $resourceRelated, string $relation, ?bool $shouldUseName = false): static
{
return $this
->weight(FontWeight::SemiBold)
->html()
->hiddenOn(BaseRelationManager::class)
->url(function (BaseModel $record) use ($resourceRelated, $relation, $shouldUseName) {
foreach (explode('.', $relation) as $element) {
$record = $record->$element;
}
$this->formatStateUsing(function ($state) use ($shouldUseName, $record) {
$name = $shouldUseName ? $record->getName() : $state;
return "<p style='color: rgb(64, 184, 166);'>{$name}</p>";
});
return $record !== null ? (new $resourceRelated)::getUrl('edit', ['record' => $record]) : null;
});
}
/**
* Make the column copyable.
*
* @param bool $condition
* @return static
*/
public function copyableWithMessage(bool $condition = true): static
{
return $this
->copyable($condition)
->copyMessage(__('filament.actions.base.copied'));
}
}
+56
View File
@@ -0,0 +1,56 @@
<?php
declare(strict_types=1);
namespace App\Filament\Components\Fields;
use App\Models\Auth\User;
use App\Models\BaseModel;
use Filament\Forms\Components\Select as ComponentsSelect;
use Laravel\Scout\Searchable;
/**
* Class Select.
*/
class Select extends ComponentsSelect
{
/**
* Use laravel scout to make fields searchable.
*
* @param class-string<BaseModel> $model
* @param string|null $loadRelation
* @return static
*/
public function useScout(string $model, ?string $loadRelation = null): static
{
if (in_array(Searchable::class, class_uses_recursive($model))) {
return $this
->allowHtml()
->searchable()
->getSearchResultsUsing(function (string $search) use ($model, $loadRelation) {
return (new $model)::search($search)
->get()
->load($loadRelation ?? [])
->mapWithKeys(fn (BaseModel $model) => [$model->getKey() => static::getSearchLabelWithBlade($model)])
->toArray();
});
}
return $this->searchable();
}
/**
* Use the blade to make the results.
*
* @param BaseModel|User $model
* @return string
*/
public static function getSearchLabelWithBlade(BaseModel|User $model): string
{
return view('filament.components.select')
->with('name', $model->getName())
->with('subtitle', $model->getSubtitle())
->with('image', $model instanceof User ? $model->getFilamentAvatarUrl() : null)
->render();
}
}
@@ -0,0 +1,85 @@
<?php
declare(strict_types=1);
namespace App\Filament\Components\Filters;
use App\Enums\Http\Api\Filter\ComparisonOperator;
use Filament\Forms\Components\DatePicker;
use Filament\Tables\Filters\Filter;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Arr;
/**
* Class DateFilter.
*/
class DateFilter extends Filter
{
protected string $attribute = '';
protected string $fromLabel = '';
protected string $toLabel = '';
/**
* Get the attribute used for filter.
*
* @param string $attribute
* @return static
*/
public function attribute(string $attribute): static
{
$this->attribute = $attribute;
return $this;
}
/**
* Get the label for the filters.
*
* @param string $fromLabel
* @param string $toLabel
* @return static
*/
public function labels(string $fromLabel, string $toLabel): static
{
$this->fromLabel = $fromLabel;
$this->toLabel = $toLabel;
return $this;
}
/**
* Get the form for the filter.
*
* @return array
*/
public function getFormSchema(): array
{
return [
DatePicker::make($this->attribute.'_'.'from')
->label($this->fromLabel),
DatePicker::make($this->attribute.'_'.'to')
->label($this->toLabel),
];
}
/**
* Apply the query used to the filter.
*
* @param Builder $query
* @param array $data
* @return Builder
*/
public function applyToBaseQuery(Builder $query, array $data = []): Builder
{
return $query
->when(
Arr::get($data, $this->attribute.'_'.'from'),
fn (Builder $query, $date): Builder => $query->whereDate($this->attribute, ComparisonOperator::GTE->value, $date),
)
->when(
Arr::get($data, $this->attribute.'_'.'to'),
fn (Builder $query, $date): Builder => $query->whereDate($this->attribute, ComparisonOperator::LTE->value, $date),
);
}
}
@@ -0,0 +1,88 @@
<?php
declare(strict_types=1);
namespace App\Filament\Components\Filters;
use App\Enums\Http\Api\Filter\ComparisonOperator;
use Filament\Forms\Components\TextInput;
use Filament\Tables\Filters\Filter;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Arr;
/**
* Class NumberFilter.
*/
class NumberFilter extends Filter
{
protected string $attribute = '';
protected string $fromLabel = '';
protected string $toLabel = '';
/**
* Get the attribute used for filter.
*
* @param string $attribute
* @return static
*/
public function attribute(string $attribute): static
{
$this->attribute = $attribute;
return $this;
}
/**
* Get the label for the filters.
*
* @param string $fromLabel
* @param string $toLabel
*
* @return static
*/
public function labels(string $fromLabel, string $toLabel): static
{
$this->fromLabel = $fromLabel;
$this->toLabel = $toLabel;
return $this;
}
/**
* Get the form for the filter.
*
* @return array
*/
public function getFormSchema(): array
{
return [
TextInput::make($this->attribute.'_'.'from')
->label($this->fromLabel)
->integer(),
TextInput::make($this->attribute.'_'.'to')
->label($this->toLabel)
->integer(),
];
}
/**
* Apply the query used to the filter.
*
* @param Builder $query
* @param array $data
* @return Builder
*/
public function applyToBaseQuery(Builder $query, array $data = []): Builder
{
return $query
->when(
Arr::get($data, $this->attribute.'_'.'from'),
fn (Builder $query, $value): Builder => $query->where($this->attribute, ComparisonOperator::GTE->value, $value),
)
->when(
Arr::get($data, $this->attribute.'_'.'to'),
fn (Builder $query, $value): Builder => $query->where($this->attribute, ComparisonOperator::LTE->value, $value),
);
}
}
@@ -0,0 +1,56 @@
<?php
declare(strict_types=1);
namespace App\Filament\Components\Filters;
use Filament\Forms\Components\TextInput;
use Filament\Tables\Filters\Filter;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Arr;
/**
* Class TextFilter.
*/
class TextFilter extends Filter
{
protected string $attribute = '';
/**
* Get the attribute used for filter.
*
* @param string $attribute
* @return static
*/
public function attribute(string $attribute): static
{
$this->attribute = $attribute;
return $this;
}
/**
* Get the form for the filter.
*
* @return array
*/
public function getFormSchema(): array
{
return [
TextInput::make($this->attribute)
->label($this->label),
];
}
/**
* Apply the query used to the filter.
*
* @param Builder $query
* @param array $data
* @return Builder
*/
public function applyToBaseQuery(Builder $query, array $data = []): Builder
{
return $query->where($this->attribute, Arr::get($data, $this->attribute));
}
}
@@ -0,0 +1,56 @@
<?php
declare(strict_types=1);
namespace App\Filament\Components\Infolist;
use App\Filament\Resources\BaseResource;
use App\Models\BaseModel;
use Filament\Infolists\Components\TextEntry as ComponentsTextEntry;
use Filament\Support\Enums\FontWeight;
/**
* Class TextEntry.
*/
class TextEntry extends ComponentsTextEntry
{
/**
* Used for entry relationships.
*
* @param class-string<BaseResource>|string $resourceRelated
* @param string $relation
* @param bool|null $shouldUseName
* @return static
*/
public function urlToRelated(string $resourceRelated, string $relation, ?bool $shouldUseName = false): static
{
return $this
->weight(FontWeight::SemiBold)
->html()
->url(function (BaseModel $record) use ($resourceRelated, $relation, $shouldUseName) {
foreach (explode('.', $relation) as $element) {
$record = $record->$element;
}
$this->formatStateUsing(function ($state) use ($shouldUseName, $record) {
$name = $shouldUseName ? $record->getName() : $state;
return "<p style='color: rgb(64, 184, 166);'>{$name}</p>";
});
return $record !== null ? (new $resourceRelated)::getUrl('edit', ['record' => $record]) : null;
});
}
/**
* Make the entry copyable.
*
* @param bool $condition
* @return static
*/
public function copyableWithMessage(bool $condition = true): static
{
return $this
->copyable($condition)
->copyMessage(__('filament.actions.base.copied'));
}
}
@@ -0,0 +1,70 @@
<?php
declare(strict_types=1);
namespace App\Filament\Dashboards;
use App\Filament\Widgets\Auth\UserChart;
use App\Filament\Widgets\List\PlaylistChart;
use App\Filament\Widgets\List\PlaylistTrackChart;
use App\Models\Auth\User;
/**
* Class AdminDashboard.
*/
class AdminDashboard extends BaseDashboard
{
/**
* Get the slug used to the dashboard route path.
*
* @return string
*/
public static function getSlug(): string
{
return 'admin';
}
/**
* Determine if the user can access the dashboard.
*
* @return bool
*/
public static function canAccess(): bool
{
return User::find(auth()->user()->id)->hasRole('Admin');
}
/**
* Get the displayed label for the dashboard.
*
* @return string
*/
public static function getNavigationLabel(): string
{
return __('filament.dashboards.label.admin');
}
/**
* Get the icon for the dashboard.
*
* @return string
*/
public static function getNavigationIcon(): string
{
return __('filament.dashboards.icon.admin');;
}
/**
* Get the widgets available for the dashboard.
*
* @return class-string[]
*/
public function getWidgets(): array
{
return [
UserChart::class,
PlaylistChart::class,
PlaylistTrackChart::class,
];
}
}
+33
View File
@@ -0,0 +1,33 @@
<?php
declare(strict_types=1);
namespace App\Filament\Dashboards;
use Filament\Pages\Dashboard;
/**
* Class BaseDashboard.
*/
abstract class BaseDashboard extends Dashboard
{
/**
* Get the route path for the dashboard.
*
* @return string
*/
public static function getRoutePath(): string
{
return 'dashboards/'.static::getSlug();
}
/**
* Get the title for the dashboard.
*
* @return string
*/
public function getTitle(): string
{
return static::getNavigationLabel();
}
}
+61
View File
@@ -0,0 +1,61 @@
<?php
declare(strict_types=1);
namespace App\Filament\Dashboards;
use App\Filament\Widgets\Wiki\Anime\AnimeChart;
use App\Filament\Widgets\Wiki\Artist\ArtistChart;
use App\Filament\Widgets\Wiki\Series\SeriesChart;
use App\Filament\Widgets\Wiki\Video\VideoChart;
/**
* Class WikiDashboard.
*/
class WikiDashboard extends BaseDashboard
{
/**
* Get the slug used to the dashboard route path.
*
* @return string
*/
public static function getSlug(): string
{
return 'wiki';
}
/**
* Get the displayed label for the dashboard.
*
* @return string
*/
public static function getNavigationLabel(): string
{
return __('filament.dashboards.label.wiki');
}
/**
* Get the icon for the dashboard.
*
* @return string
*/
public static function getNavigationIcon(): string
{
return __('filament.dashboards.icon.wiki');;
}
/**
* Get the widgets available for the dashboard.
*
* @return class-string[]
*/
public function getWidgets(): array
{
return [
AnimeChart::class,
ArtistChart::class,
SeriesChart::class,
VideoChart::class,
];
}
}
@@ -0,0 +1,50 @@
<?php
declare(strict_types=1);
namespace App\Filament\HeaderActions\Discord;
use App\Actions\Discord\DiscordThreadAction as DiscordThreadActionAction;
use App\Models\Wiki\Anime;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Form;
use Filament\Actions\Action;
/**
* Class DiscordThreadAction.
*/
class DiscordThreadHeaderAction extends Action
{
/**
* Initial setup for the action.
*
* @return void
*/
protected function setUp(): void
{
parent::setUp();
$this->fillForm(fn (Anime $record): array => ['name' => $record->getName()]);
$this->action(fn (Anime $record, array $data) => (new DiscordThreadActionAction())->handle($record, $data));
}
/**
* Get the fields available on the action.
*
* @param Form $form
* @return Form
*/
public function getForm(Form $form): Form
{
return $form
->schema([
TextInput::make('name')
->label(__('filament.actions.discord.thread.name'))
->helperText(__('filament.actions.discord.thread.help'))
->required()
->maxlength(100)
->rules(['required', 'max:100']),
]);
}
}
@@ -0,0 +1,65 @@
<?php
declare(strict_types=1);
namespace App\Filament\HeaderActions\Models;
use App\Actions\Models\AssignHashidsAction as AssignHashids;
use App\Contracts\Models\HasHashids;
use App\Models\BaseModel;
use Exception;
use Filament\Actions\Action;
/**
* Class AssignHashidsHeaderAction.
*/
class AssignHashidsHeaderAction extends Action
{
protected ?string $connection = null;
/**
* Initial setup for the action.
*
* @return void
*/
protected function setUp(): void
{
parent::setUp();
$this->action(fn (BaseModel $record) => $this->handle($record));
}
/**
* Perform the action on the given models.
*
* @param BaseModel $model
* @return void
*
* @noinspection PhpUnusedParameterInspection
*/
public function handle(BaseModel $model): void
{
$action = new AssignHashids();
if ($model instanceof HasHashids) {
try {
$action->assign($model, $this->connection);
} catch (Exception $e) {
//$this->markAsFailed($model, $e);
}
}
}
/**
* Set the connection.
*
* @param string|null $connection
* @return static
*/
public function setConnection(?string $connection): static
{
$this->connection = $connection;
return $this;
}
}
@@ -0,0 +1,72 @@
<?php
declare(strict_types=1);
namespace App\Filament\HeaderActions\Models\Auth\Role;
use App\Filament\Components\Fields\Select;
use App\Models\Auth\Permission;
use App\Models\Auth\Role;
use Filament\Forms\Form;
use Filament\Actions\Action;
use Illuminate\Support\Arr;
/**
* Class GivePermissionHeaderAction.
*/
class GivePermissionHeaderAction extends Action
{
final public const FIELD_PERMISSION = 'permission';
/**
* Initial setup for the action.
*
* @return void
*/
protected function setUp(): void
{
parent::setUp();
$this->action(fn (Role $record, array $data) => $this->handle($record, $data));
}
/**
* Perform the action on the given model.
*
* @param Role $role
* @param array $data
* @return void
*/
public function handle(Role $role, array $data): void
{
$permission = Permission::findById(intval(Arr::get($data, self::FIELD_PERMISSION)));
$role->givePermissionTo($permission);
}
/**
* Get the fields available on the action.
*
* @param Form $form
* @return Form
*
* @noinspection PhpMissingParentCallCommonInspection
*/
public function getForm(Form $form): Form
{
$permissions = Permission::all([Permission::ATTRIBUTE_ID, Permission::ATTRIBUTE_NAME])
->keyBy(Permission::ATTRIBUTE_ID)
->map(fn (Permission $permission) => $permission->name)
->toArray();
return $form
->schema([
Select::make(self::FIELD_PERMISSION)
->label(__('filament.resources.singularLabel.permission'))
->searchable()
->required()
->options($permissions)
->rules('required'),
]);
}
}
@@ -0,0 +1,72 @@
<?php
declare(strict_types=1);
namespace App\Filament\HeaderActions\Models\Auth\Role;
use App\Filament\Components\Fields\Select;
use App\Models\Auth\Permission;
use App\Models\Auth\Role;
use Filament\Forms\Form;
use Filament\Actions\Action;
use Illuminate\Support\Arr;
/**
* Class RevokePermissionHeaderAction.
*/
class RevokePermissionHeaderAction extends Action
{
final public const FIELD_PERMISSION = 'permission';
/**
* Initial setup for the action.
*
* @return void
*/
protected function setUp(): void
{
parent::setUp();
$this->action(fn (Role $record, array $data) => $this->handle($record, $data));
}
/**
* Perform the action on the given model.
*
* @param Role $role
* @param array $data
* @return void
*/
public function handle(Role $role, array $data): void
{
$permission = Permission::findById(intval(Arr::get($data, self::FIELD_PERMISSION)));
$role->revokePermissionTo($permission);
}
/**
* Get the fields available on the action.
*
* @param Form $form
* @return Form
*
* @noinspection PhpMissingParentCallCommonInspection
*/
public function getForm(Form $form): Form
{
$permissions = Permission::all([Permission::ATTRIBUTE_ID, Permission::ATTRIBUTE_NAME])
->keyBy(Permission::ATTRIBUTE_ID)
->map(fn (Permission $permission) => $permission->name)
->toArray();
return $form
->schema([
Select::make(self::FIELD_PERMISSION)
->label(__('filament.resources.singularLabel.permission'))
->searchable()
->required()
->options($permissions)
->rules('required'),
]);
}
}
@@ -0,0 +1,73 @@
<?php
declare(strict_types=1);
namespace App\Filament\HeaderActions\Models\Auth\User;
use App\Filament\Components\Fields\Select;
use App\Models\Auth\Permission;
use App\Models\Auth\User;
use Filament\Forms\Form;
use Filament\Actions\Action;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Arr;
/**
* Class GivePermissionHeaderAction.
*/
class GivePermissionHeaderAction extends Action
{
final public const FIELD_PERMISSION = 'permission';
/**
* Initial setup for the action.
*
* @return void
*/
protected function setUp(): void
{
parent::setUp();
$this->action(fn (User $record, array $data) => $this->handle($record, $data));
}
/**
* Perform the action on the given model.
*
* @param User $user
* @param array $data
* @return void
*/
public function handle(User $user, array $data): void
{
$permission = Permission::findById(intval(Arr::get($data, self::FIELD_PERMISSION)));
$user->givePermissionTo($permission);
}
/**
* Get the fields available on the action.
*
* @param Form $form
* @return Form
*
* @noinspection PhpMissingParentCallCommonInspection
*/
public function getForm(Form $form): Form
{
$permissions = Permission::all([Permission::ATTRIBUTE_ID, Permission::ATTRIBUTE_NAME])
->keyBy(Permission::ATTRIBUTE_ID)
->map(fn (Permission $permission) => $permission->name)
->toArray();
return $form
->schema([
Select::make(self::FIELD_PERMISSION)
->label(__('filament.resources.singularLabel.permission'))
->searchable()
->required()
->options($permissions)
->rules('required'),
]);
}
}
@@ -0,0 +1,73 @@
<?php
declare(strict_types=1);
namespace App\Filament\HeaderActions\Models\Auth\User;
use App\Filament\Components\Fields\Select;
use App\Models\Auth\Role;
use App\Models\Auth\User;
use Filament\Forms\Form;
use Filament\Actions\Action;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Arr;
/**
* Class GiveRoleHeaderAction.
*/
class GiveRoleHeaderAction extends Action
{
final public const FIELD_ROLE = 'role';
/**
* Initial setup for the action.
*
* @return void
*/
protected function setUp(): void
{
parent::setUp();
$this->action(fn (User $record, array $data) => $this->handle($record, $data));
}
/**
* Perform the action on the given models.
*
* @param User $user
* @param array $data
* @return void
*/
public function handle(User $user, array $data): void
{
$role = Role::findById(intval(Arr::get($data, self::FIELD_ROLE)));
$user->assignRole($role);
}
/**
* Get the fields available on the action.
*
* @param Form $form
* @return Form
*
* @noinspection PhpMissingParentCallCommonInspection
*/
public function getForm(Form $form): Form
{
$roles = Role::all([Role::ATTRIBUTE_ID, Role::ATTRIBUTE_NAME])
->keyBy(Role::ATTRIBUTE_ID)
->map(fn (Role $role) => $role->name)
->toArray();
return $form
->schema([
Select::make(self::FIELD_ROLE)
->label(__('filament.resources.singularLabel.role'))
->searchable()
->required()
->options($roles)
->rules('required'),
]);
}
}
@@ -0,0 +1,72 @@
<?php
declare(strict_types=1);
namespace App\Filament\HeaderActions\Models\Auth\User;
use App\Filament\Components\Fields\Select;
use App\Models\Auth\Permission;
use App\Models\Auth\User;
use Filament\Forms\Form;
use Filament\Actions\Action;
use Illuminate\Support\Arr;
/**
* Class RevokePermissionHeaderAction.
*/
class RevokePermissionHeaderAction extends Action
{
final public const FIELD_PERMISSION = 'permission';
/**
* Initial setup for the action.
*
* @return void
*/
protected function setUp(): void
{
parent::setUp();
$this->action(fn (User $record, array $data) => $this->handle($record, $data));
}
/**
* Perform the action on the given model.
*
* @param User $user
* @param array $data
* @return void
*/
public function handle(User $user, array $data): void
{
$permission = Permission::findById(intval(Arr::get($data, self::FIELD_PERMISSION)));
$user->revokePermissionTo($permission);
}
/**
* Get the fields available on the action.
*
* @param Form $form
* @return Form
*
* @noinspection PhpMissingParentCallCommonInspection
*/
public function getForm(Form $form): Form
{
$permissions = Permission::all([Permission::ATTRIBUTE_ID, Permission::ATTRIBUTE_NAME])
->keyBy(Permission::ATTRIBUTE_ID)
->map(fn (Permission $permission) => $permission->name)
->toArray();
return $form
->schema([
Select::make(self::FIELD_PERMISSION)
->label(__('filament.resources.singularLabel.permission'))
->searchable()
->required()
->options($permissions)
->rules('required'),
]);
}
}
@@ -0,0 +1,72 @@
<?php
declare(strict_types=1);
namespace App\Filament\HeaderActions\Models\Auth\User;
use App\Filament\Components\Fields\Select;
use App\Models\Auth\Role;
use App\Models\Auth\User;
use Filament\Forms\Form;
use Filament\Actions\Action;
use Illuminate\Support\Arr;
/**
* Class RevokeRoleHeaderAction.
*/
class RevokeRoleHeaderAction extends Action
{
final public const FIELD_ROLE = 'role';
/**
* Initial setup for the action.
*
* @return void
*/
protected function setUp(): void
{
parent::setUp();
$this->action(fn (User $record, array $data) => $this->handle($record, $data));
}
/**
* Perform the action on the given models.
*
* @param User $user
* @param array $data
* @return void
*/
public function handle(User $user, array $data): void
{
$role = Role::findById(intval(Arr::get($data, self::FIELD_ROLE)));
$user->removeRole($role);
}
/**
* Get the fields available on the action.
*
* @param Form $form
* @return Form
*
* @noinspection PhpMissingParentCallCommonInspection
*/
public function getForm(Form $form): Form
{
$roles = Role::all([Role::ATTRIBUTE_ID, Role::ATTRIBUTE_NAME])
->keyBy(Role::ATTRIBUTE_ID)
->map(fn (Role $role) => $role->name)
->toArray();
return $form
->schema([
Select::make(self::FIELD_ROLE)
->label(__('filament.resources.singularLabel.role'))
->searchable()
->required()
->options($roles)
->rules('required'),
]);
}
}
@@ -0,0 +1,27 @@
<?php
declare(strict_types=1);
namespace App\Filament\HeaderActions\Models\Wiki\Anime;
use App\Actions\Models\Wiki\Anime\AttachAnimeImageAction as AttachAnimeImageActionAction;
use App\Filament\HeaderActions\Models\Wiki\AttachImageHeaderAction;
use App\Models\Wiki\Anime;
/**
* Class AttachAnimeImageHeaderAction.
*/
class AttachAnimeImageHeaderAction extends AttachImageHeaderAction
{
/**
* Initial setup for the action.
*
* @return void
*/
protected function setUp(): void
{
parent::setUp();
$this->action(fn (Anime $record, array $data) => (new AttachAnimeImageActionAction($this->facets))->handle($record, $data));
}
}
@@ -0,0 +1,41 @@
<?php
declare(strict_types=1);
namespace App\Filament\HeaderActions\Models\Wiki\Anime;
use App\Actions\Models\Wiki\Anime\AttachAnimeResourceAction as AttachAnimeResourceActionAction;
use App\Enums\Models\Wiki\ResourceSite;
use App\Filament\HeaderActions\Models\Wiki\AttachResourceHeaderAction;
use App\Models\Wiki\Anime;
use App\Rules\Wiki\Resource\AnimeResourceLinkFormatRule;
use Illuminate\Contracts\Validation\ValidationRule;
/**
* Class AttachAnimeResourceHeaderAction.
*/
class AttachAnimeResourceHeaderAction extends AttachResourceHeaderAction
{
/**
* Initial setup for the action.
*
* @return void
*/
protected function setUp(): void
{
parent::setUp();
$this->action(fn (Anime $record, array $data) => (new AttachAnimeResourceActionAction($this->sites))->handle($record, $data));
}
/**
* Get the format validation rule.
*
* @param ResourceSite $site
* @return ValidationRule
*/
protected function getFormatRule(ResourceSite $site): ValidationRule
{
return new AnimeResourceLinkFormatRule($site);
}
}
@@ -0,0 +1,226 @@
<?php
declare(strict_types=1);
namespace App\Filament\HeaderActions\Models\Wiki\Anime;
use App\Actions\Models\BackfillAction;
use App\Actions\Models\Wiki\Anime\BackfillAnimeOtherResourcesAction;
use App\Actions\Models\Wiki\Anime\BackfillAnimeSynonymsAction;
use App\Actions\Models\Wiki\Anime\Image\BackfillLargeCoverImageAction;
use App\Actions\Models\Wiki\Anime\Image\BackfillSmallCoverImageAction;
use App\Actions\Models\Wiki\Anime\Resource\BackfillAnidbResourceAction;
use App\Actions\Models\Wiki\Anime\Resource\BackfillAnilistResourceAction;
use App\Actions\Models\Wiki\Anime\Resource\BackfillAnnResourceAction;
use App\Actions\Models\Wiki\Anime\Resource\BackfillKitsuResourceAction;
use App\Actions\Models\Wiki\Anime\Resource\BackfillMalResourceAction;
use App\Actions\Models\Wiki\Anime\Studio\BackfillAnimeStudiosAction;
use App\Enums\Models\Wiki\ImageFacet;
use App\Enums\Models\Wiki\ResourceSite;
use App\Models\Wiki\Anime;
use App\Models\Wiki\ExternalResource;
use App\Models\Wiki\Image;
use Exception;
use Filament\Forms\Components\Checkbox;
use Filament\Forms\Components\Section;
use Filament\Forms\Form;
use Filament\Notifications\Actions\Action as NotificationAction;
use Filament\Notifications\Notification;
use Filament\Actions\Action;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Support\Arr;
use Illuminate\Support\Sleep;
/**
* Class BackfillAnimeHeaderAction.
*/
class BackfillAnimeHeaderAction extends Action implements ShouldQueue
{
use InteractsWithQueue;
use Queueable;
final public const BACKFILL_ANIDB_RESOURCE = 'backfill_anidb_resource';
final public const BACKFILL_ANILIST_RESOURCE = 'backfill_anilist_resource';
final public const BACKFILL_ANN_RESOURCE = 'backfill_ann_resource';
final public const BACKFILL_KITSU_RESOURCE = 'backfill_kitsu_resource';
final public const BACKFILL_OTHER_RESOURCES = 'backfill_other_resources';
final public const BACKFILL_LARGE_COVER = 'backfill_large_cover';
final public const BACKFILL_MAL_RESOURCE = 'backfill_mal_resource';
final public const BACKFILL_SMALL_COVER = 'backfill_small_cover';
final public const BACKFILL_STUDIOS = 'backfill_studios';
final public const BACKFILL_SYNONYMS = 'backfill_synonyms';
/**
* Initial setup for the action.
*
* @return void
*/
protected function setUp(): void
{
parent::setUp();
$this->action(fn (Anime $record, array $data) => $this->handle($record, $data));
}
/**
* Perform the action on the given models.
*
* @param Anime $anime
* @param array $fields
* @return void
*/
public function handle(Anime $anime, array $fields): void
{
if ($anime->resources()->doesntExist()) {
$this->fail(__('filament.actions.anime.backfill.message.resource_required_failure'));
return;
}
$actions = $this->getActions($fields, $anime);
try {
foreach ($actions as $action) {
$result = $action->handle();
if ($result->hasFailed()) {
Notification::make()
->body($result->getMessage())
->warning()
->actions([
NotificationAction::make('mark-as-read')
->button()
->markAsRead(),
])
->sendToDatabase(auth()->user());
}
}
} catch (Exception $e) {
$this->fail($e);
} finally {
// Try not to upset third-party APIs
Sleep::for(rand(3, 5))->second();
}
}
/**
* Get the fields available on the action.
*
* @param Form $form
* @return Form|null
*
* @noinspection PhpMissingParentCallCommonInspection
*/
public function getForm(Form $form): ?Form
{
$anime = $this->getRecord();
return $form
->schema([
Section::make(__('filament.actions.anime.backfill.fields.resources.name'))
->schema([
Checkbox::make(self::BACKFILL_KITSU_RESOURCE)
->label(__('filament.actions.anime.backfill.fields.resources.kitsu.name'))
->helperText(__('filament.actions.anime.backfill.fields.resources.kitsu.help'))
->default(fn () => $anime instanceof Anime && $anime->resources()->where(ExternalResource::ATTRIBUTE_SITE, ResourceSite::KITSU->value)->doesntExist()),
Checkbox::make(self::BACKFILL_ANILIST_RESOURCE)
->label(__('filament.actions.anime.backfill.fields.resources.anilist.name'))
->helperText(__('filament.actions.anime.backfill.fields.resources.anilist.help'))
->default(fn () => $anime instanceof Anime && $anime->resources()->where(ExternalResource::ATTRIBUTE_SITE, ResourceSite::ANILIST->value)->doesntExist()),
Checkbox::make(self::BACKFILL_MAL_RESOURCE)
->label(__('filament.actions.anime.backfill.fields.resources.mal.name'))
->helperText(__('filament.actions.anime.backfill.fields.resources.mal.help'))
->default(fn () => $anime instanceof Anime && $anime->resources()->where(ExternalResource::ATTRIBUTE_SITE, ResourceSite::MAL->value)->doesntExist()),
Checkbox::make(self::BACKFILL_ANIDB_RESOURCE)
->label(__('filament.actions.anime.backfill.fields.resources.anidb.name'))
->helperText(__('filament.actions.anime.backfill.fields.resources.anidb.help'))
->default(fn () => $anime instanceof Anime && $anime->resources()->where(ExternalResource::ATTRIBUTE_SITE, ResourceSite::ANIDB->value)->doesntExist()),
Checkbox::make(self::BACKFILL_ANN_RESOURCE)
->label(__('filament.actions.anime.backfill.fields.resources.ann.name'))
->helperText(__('filament.actions.anime.backfill.fields.resources.ann.help'))
->default(fn () => $anime instanceof Anime && $anime->resources()->where(ExternalResource::ATTRIBUTE_SITE, ResourceSite::ANN->value)->doesntExist()),
Checkbox::make(self::BACKFILL_OTHER_RESOURCES)
->label(__('filament.actions.anime.backfill.fields.resources.external_links.name'))
->helperText(__('filament.actions.anime.backfill.fields.resources.external_links.help'))
->default(fn () => $anime instanceof Anime),
]),
Section::make(__('filament.actions.anime.backfill.fields.images.name'))
->schema([
Checkbox::make(self::BACKFILL_LARGE_COVER)
->label(__('filament.actions.anime.backfill.fields.images.large_cover.name'))
->helperText(__('filament.actions.anime.backfill.fields.images.large_cover.help'))
->default(fn () => $anime instanceof Anime && $anime->images()->where(Image::ATTRIBUTE_FACET, ImageFacet::COVER_LARGE->value)->doesntExist()),
Checkbox::make(self::BACKFILL_SMALL_COVER)
->label(__('filament.actions.anime.backfill.fields.images.small_cover.name'))
->helperText(__('filament.actions.anime.backfill.fields.images.small_cover.help'))
->default(fn () => $anime instanceof Anime && $anime->images()->where(Image::ATTRIBUTE_FACET, ImageFacet::COVER_SMALL->value)->doesntExist()),
]),
Section::make(__('filament.actions.anime.backfill.fields.studios.name'))
->schema([
Checkbox::make(self::BACKFILL_STUDIOS)
->label(__('filament.actions.anime.backfill.fields.studios.anime.name'))
->helperText(__('filament.actions.anime.backfill.fields.studios.anime.help'))
->default(fn () => $anime instanceof Anime && $anime->studios()->doesntExist()),
]),
Section::make(__('filament.actions.anime.backfill.fields.synonyms.name'))
->schema([
Checkbox::make(self::BACKFILL_STUDIOS)
->label(__('filament.actions.anime.backfill.fields.synonyms.name'))
->helperText(__('filament.actions.anime.backfill.fields.synonyms.help'))
->default(fn () => $anime instanceof Anime && $anime->animesynonyms()->count() === 0),
]),
]);
}
/**
* Get the selected actions for backfilling anime.
*
* @param array $fields
* @param Anime $anime
* @return BackfillAction[]
*/
protected function getActions(array $fields, Anime $anime): array
{
$actions = [];
foreach ($this->getActionMapping($anime) as $field => $action) {
if (Arr::get($fields, $field) === true) {
$actions[] = $action;
}
}
return $actions;
}
/**
* Get the mapping of actions to their form fields.
*
* @param Anime $anime
* @return array<string, BackfillAction>
*/
protected function getActionMapping(Anime $anime): array
{
return [
self::BACKFILL_KITSU_RESOURCE => new BackfillKitsuResourceAction($anime),
self::BACKFILL_ANILIST_RESOURCE => new BackfillAnilistResourceAction($anime),
self::BACKFILL_MAL_RESOURCE => new BackfillMalResourceAction($anime),
self::BACKFILL_ANIDB_RESOURCE => new BackfillAnidbResourceAction($anime),
self::BACKFILL_ANN_RESOURCE => new BackfillAnnResourceAction($anime),
self::BACKFILL_OTHER_RESOURCES => new BackfillAnimeOtherResourcesAction($anime),
self::BACKFILL_LARGE_COVER => new BackfillLargeCoverImageAction($anime),
self::BACKFILL_SMALL_COVER => new BackfillSmallCoverImageAction($anime),
self::BACKFILL_STUDIOS => new BackfillAnimeStudiosAction($anime),
self::BACKFILL_SYNONYMS => new BackfillAnimeSynonymsAction($anime),
];
}
}
@@ -0,0 +1,27 @@
<?php
declare(strict_types=1);
namespace App\Filament\HeaderActions\Models\Wiki\Artist;
use App\Actions\Models\Wiki\Artist\AttachArtistImageAction as AttachArtistImageActionAction;
use App\Filament\HeaderActions\Models\Wiki\AttachImageHeaderAction;
use App\Models\Wiki\Artist;
/**
* Class AttachArtistImageHeaderAction.
*/
class AttachArtistImageHeaderAction extends AttachImageHeaderAction
{
/**
* Initial setup for the action.
*
* @return void
*/
protected function setUp(): void
{
parent::setUp();
$this->action(fn (Artist $record, array $data) => (new AttachArtistImageActionAction($this->facets))->handle($record, $data));
}
}
@@ -0,0 +1,41 @@
<?php
declare(strict_types=1);
namespace App\Filament\HeaderActions\Models\Wiki\Artist;
use App\Actions\Models\Wiki\Artist\AttachArtistResourceAction as AttachArtistResourceActionAction;
use App\Enums\Models\Wiki\ResourceSite;
use App\Filament\HeaderActions\Models\Wiki\AttachResourceHeaderAction;
use App\Models\Wiki\Artist;
use App\Rules\Wiki\Resource\ArtistResourceLinkFormatRule;
use Illuminate\Contracts\Validation\ValidationRule;
/**
* Class AttachArtistResourceHeaderAction.
*/
class AttachArtistResourceHeaderAction extends AttachResourceHeaderAction
{
/**
* Initial setup for the action.
*
* @return void
*/
protected function setUp(): void
{
parent::setUp();
$this->action(fn (Artist $record, array $data) => (new AttachArtistResourceActionAction($this->sites))->handle($record, $data));
}
/**
* Get the format validation rule.
*
* @param ResourceSite $site
* @return ValidationRule
*/
protected function getFormatRule(ResourceSite $site): ValidationRule
{
return new ArtistResourceLinkFormatRule($site);
}
}
@@ -0,0 +1,62 @@
<?php
declare(strict_types=1);
namespace App\Filament\HeaderActions\Models\Wiki;
use App\Enums\Models\Wiki\ImageFacet;
use App\Models\Wiki\Anime;
use App\Models\Wiki\Artist;
use App\Models\Wiki\Image;
use App\Models\Wiki\Studio;
use Filament\Forms\Components\FileUpload;
use Filament\Forms\Form;
use Filament\Actions\Action;
/**
* Class AttachImageHeaderAction.
*/
abstract class AttachImageHeaderAction extends Action
{
protected array $facets = [];
/**
* Get the fields available on the action.
*
* @param Form $form
* @return Form
*/
public function getForm(Form $form): Form
{
$fields = [];
$model = $form->getRecord();
if ($model instanceof Anime || $model instanceof Artist || $model instanceof Studio) {
foreach ($this->facets as $facet) {
$images = $model->images();
if ($images->where(Image::ATTRIBUTE_FACET, $facet->value)->exists()) continue;
$fields[] = FileUpload::make($facet->name)
->label($facet->localize())
->image()
->storeFiles(false);
}
}
return $form
->schema($fields);
}
/**
* Get the facets available for the action.
*
* @param ImageFacet[] $facets
* @return static
*/
public function facets(array $facets): static
{
$this->facets = $facets;
return $this;
}
}
@@ -0,0 +1,76 @@
<?php
declare(strict_types=1);
namespace App\Filament\HeaderActions\Models\Wiki;
use App\Enums\Models\Wiki\ResourceSite;
use App\Models\Wiki\Anime;
use App\Models\Wiki\Artist;
use App\Models\Wiki\ExternalResource;
use App\Models\Wiki\Song;
use App\Models\Wiki\Studio;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Form;
use Filament\Actions\Action;
use Illuminate\Contracts\Validation\ValidationRule;
/**
* Class AttachResourceHeaderAction.
*/
abstract class AttachResourceHeaderAction extends Action
{
protected array $sites = [];
/**
* Get the fields available on the action.
*
* @param Form $form
* @return Form|null
*/
public function getForm(Form $form): ?Form
{
$fields = [];
$model = $this->getRecord();
foreach ($this->sites as $resourceSite) {
if ($model instanceof Anime || $model instanceof Artist || $model instanceof Song || $model instanceof Studio) {
$resources = $model->resources();
if ($resources->where(ExternalResource::ATTRIBUTE_SITE, $resourceSite->value)->exists()) continue;
}
$resourceSiteLower = strtolower($resourceSite->name);
$fields[] = TextInput::make($resourceSite->name)
->label($resourceSite->localize())
->helperText(__("filament.actions.models.wiki.attach_resource.fields.{$resourceSiteLower}.help"))
->url()
->maxLength(192)
->rules(['max:192', $this->getFormatRule($resourceSite)]);
}
return $form
->schema($fields);
}
/**
* Get the sites available for the action.
*
* @param ResourceSite[] $sites
* @return static
*/
public function sites($sites): static
{
$this->sites = $sites;
return $this;
}
/**
* Get the format validation rule.
*
* @param ResourceSite $site
* @return ValidationRule
*/
abstract protected function getFormatRule(ResourceSite $site): ValidationRule;
}
@@ -0,0 +1,53 @@
<?php
declare(strict_types=1);
namespace App\Filament\HeaderActions\Models\Wiki\Audio;
use App\Models\BaseModel;
use App\Models\Wiki\Anime\Theme\AnimeThemeEntry;
use App\Models\Wiki\Audio;
use App\Models\Wiki\Video;
use Filament\Actions\Action;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Support\Facades\Log;
/**
* Class AttachAudioToRelatedVideosHeaderAction.
*/
class AttachAudioToRelatedVideosHeaderAction extends Action implements ShouldQueue
{
/**
* Initial setup for the action.
*
* @return void
*/
protected function setUp(): void
{
parent::setUp();
$this->action(fn (Audio $record) => $this->handle($record));
}
/**
* Perform the action on the given models.
*
* @param Audio $audio
* @return void
*/
public function handle(Audio $audio): void
{
$video = $audio->videos()->first();
$video->animethemeentries()->each(function (AnimeThemeEntry $firstEntry) use ($audio) {
$theme = $firstEntry->animetheme()->first();
$theme->animethemeentries()->each(function (AnimeThemeEntry $entry) use ($audio) {
$entry->videos()->each(function (Video $video) use ($audio) {
Log::info("Associating Audio '{$audio->filename}' with Video '{$video->filename}'");
$video->audio()->associate($audio)->save();
});
});
});
}
}
@@ -0,0 +1,41 @@
<?php
declare(strict_types=1);
namespace App\Filament\HeaderActions\Models\Wiki\Song;
use App\Actions\Models\Wiki\Song\AttachSongResourceAction as AttachSongResourceActionAction;
use App\Enums\Models\Wiki\ResourceSite;
use App\Filament\HeaderActions\Models\Wiki\AttachResourceHeaderAction;
use App\Models\Wiki\Song;
use App\Rules\Wiki\Resource\SongResourceLinkFormatRule;
use Illuminate\Contracts\Validation\ValidationRule;
/**
* Class AttachSongResourceHeaderAction.
*/
class AttachSongResourceHeaderAction extends AttachResourceHeaderAction
{
/**
* Initial setup for the action.
*
* @return void
*/
protected function setUp(): void
{
parent::setUp();
$this->action(fn (Song $record, array $data) => (new AttachSongResourceActionAction($this->sites))->handle($record, $data));
}
/**
* Get the format validation rule.
*
* @param ResourceSite $site
* @return ValidationRule
*/
protected function getFormatRule(ResourceSite $site): ValidationRule
{
return new SongResourceLinkFormatRule($site);
}
}
@@ -0,0 +1,27 @@
<?php
declare(strict_types=1);
namespace App\Filament\HeaderActions\Models\Wiki\Studio;
use App\Actions\Models\Wiki\Studio\AttachStudioImageAction as AttachStudioImageActionAction;
use App\Filament\HeaderActions\Models\Wiki\AttachImageHeaderAction;
use App\Models\Wiki\Studio;
/**
* Class AttachStudioImageHeaderAction.
*/
class AttachStudioImageHeaderAction extends AttachImageHeaderAction
{
/**
* Initial setup for the action.
*
* @return void
*/
protected function setUp(): void
{
parent::setUp();
$this->action(fn (Studio $record, array $data) => (new AttachStudioImageActionAction($this->facets))->handle($record, $data));
}
}
@@ -0,0 +1,41 @@
<?php
declare(strict_types=1);
namespace App\Filament\HeaderActions\Models\Wiki\Studio;
use App\Actions\Models\Wiki\Studio\AttachStudioResourceAction as AttachStudioResourceActionAction;
use App\Enums\Models\Wiki\ResourceSite;
use App\Filament\HeaderActions\Models\Wiki\AttachResourceHeaderAction;
use App\Models\Wiki\Studio;
use App\Rules\Wiki\Resource\StudioResourceLinkFormatRule;
use Illuminate\Contracts\Validation\ValidationRule;
/**
* Class AttachStudioResourceHeaderAction.
*/
class AttachStudioResourceHeaderAction extends AttachResourceHeaderAction
{
/**
* Initial setup for the action.
*
* @return void
*/
protected function setUp(): void
{
parent::setUp();
$this->action(fn (Studio $record, array $data) => (new AttachStudioResourceActionAction($this->sites))->handle($record, $data));
}
/**
* Get the format validation rule.
*
* @param ResourceSite $site
* @return ValidationRule
*/
protected function getFormatRule(ResourceSite $site): ValidationRule
{
return new StudioResourceLinkFormatRule($site);
}
}
@@ -0,0 +1,143 @@
<?php
declare(strict_types=1);
namespace App\Filament\HeaderActions\Models\Wiki\Studio;
use App\Actions\Models\BackfillAction;
use App\Actions\Models\Wiki\Studio\Image\BackfillLargeCoverImageAction;
use App\Enums\Models\Wiki\ImageFacet;
use App\Models\BaseModel;
use App\Models\Wiki\Image;
use App\Models\Wiki\Studio;
use Exception;
use Filament\Forms\Components\Checkbox;
use Filament\Forms\Components\Section;
use Filament\Forms\Form;
use Filament\Notifications\Actions\Action as NotificationAction;
use Filament\Notifications\Notification;
use Filament\Actions\Action;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Support\Arr;
use Illuminate\Support\Sleep;
/**
* Class BackfillStudioHeaderAction.
*/
class BackfillStudioHeaderAction extends Action implements ShouldQueue
{
use InteractsWithQueue;
use Queueable;
final public const BACKFILL_LARGE_COVER = 'backfill_large_cover';
/**
* Initial setup for the action.
*
* @return void
*/
protected function setUp(): void
{
parent::setUp();
$this->action(fn (Studio $record, array $data) => $this->handle($record, $data));
}
/**
* Perform the action on the given models.
*
* @param Studio $studio
* @param array $fields
* @return void
*/
public function handle(Studio $studio, array $fields): void
{
if ($studio->resources()->doesntExist()) {
//$this->markAsFailed($studio, __('filament.actions.studio.backfill.message.resource_required_failure'));
return;
}
$actions = $this->getActions($fields, $studio);
try {
foreach ($actions as $action) {
$result = $action->handle();
if ($result->hasFailed()) {
Notification::make()
->body($result->getMessage())
->warning()
->actions([
NotificationAction::make('mark-as-read')
->button()
->markAsRead(),
])
->sendToDatabase(auth()->user());
}
}
} catch (Exception $e) {
//$this->markAsFailed($studio, $e);
} finally {
// Try not to upset third-party APIs
Sleep::for(rand(3, 5))->second();
}
}
/**
* Get the fields available on the action.
*
* @param Form $form
* @return Form
*
* @noinspection PhpMissingParentCallCommonInspection
*/
public function getForm(Form $form): Form
{
$studio = $this->getRecord();
return $form
->schema([
Section::make(__('filament.actions.studio.backfill.fields.images.name'))
->schema([
Checkbox::make(self::BACKFILL_LARGE_COVER)
->label(__('filament.actions.studio.backfill.fields.images.large_cover.name'))
->helperText(__('filament.actions.studio.backfill.fields.images.large_cover.help'))
->default(fn () => $studio instanceof Studio && $studio->images()->where(Image::ATTRIBUTE_FACET, ImageFacet::COVER_LARGE->value)->doesntExist()),
]),
]);
}
/**
* Get the selected actions for backfilling studios.
*
* @param array $fields
* @param Studio $studio
* @return BackfillAction[]
*/
protected function getActions(array $fields, Studio $studio): array
{
$actions = [];
foreach ($this->getActionMapping($studio) as $field => $action) {
if (Arr::get($fields, $field) === true) {
$actions[] = $action;
}
}
return $actions;
}
/**
* Get the mapping of actions to their form fields.
*
* @param Studio $studio
* @return array<string, BackfillAction>
*/
protected function getActionMapping(Studio $studio): array
{
return [
self::BACKFILL_LARGE_COVER => new BackfillLargeCoverImageAction($studio),
];
}
}
@@ -0,0 +1,105 @@
<?php
declare(strict_types=1);
namespace App\Filament\HeaderActions\Models\Wiki\Video;
use App\Actions\Models\Wiki\Video\Audio\BackfillAudioAction as BackfillAudio;
use App\Enums\Actions\Models\Wiki\Video\DeriveSourceVideo;
use App\Enums\Actions\Models\Wiki\Video\OverwriteAudio;
use App\Filament\Components\Fields\Select;
use App\Models\Wiki\Video;
use Exception;
use Filament\Forms\Form;
use Filament\Notifications\Actions\Action as NotificationAction;
use Filament\Notifications\Notification;
use Filament\Actions\Action;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Support\Arr;
use Illuminate\Validation\Rules\Enum;
/**
* Class BackfillAudioHeaderAction.
*/
class BackfillAudioHeaderAction extends Action implements ShouldQueue
{
use InteractsWithQueue;
use Queueable;
final public const DERIVE_SOURCE_VIDEO = 'derive_source_video';
final public const OVERWRITE_AUDIO = 'overwrite_audio';
/**
* Initial setup for the action.
*
* @return void
*/
protected function setUp(): void
{
parent::setUp();
$this->action(fn (Video $record, array $data) => $this->handle($record, $data));
}
/**
* Perform the action on the given models.
*
* @param Video $video
* @param array $data
* @return void
*/
public function handle(Video $video, array $data): void
{
$deriveSourceVideo = DeriveSourceVideo::from(intval(Arr::get($data, self::DERIVE_SOURCE_VIDEO)));
$overwriteAudio = OverwriteAudio::from(intval(Arr::get($data, self::OVERWRITE_AUDIO)));
$action = new BackfillAudio($video, $deriveSourceVideo, $overwriteAudio);
try {
$result = $action->handle();
if ($result->hasFailed()) {
Notification::make()
->body($result->getMessage())
->warning()
->actions([
NotificationAction::make('mark-as-read')
->button()
->markAsRead(),
])
->sendToDatabase(auth()->user());
}
} catch (Exception $e) {
//$this->markAsFailed($video, $e);
}
}
/**
* Get the fields available on the action.
*
* @param Form $form
* @return Form
*
* @noinspection PhpMissingParentCallCommonInspection
*/
public function getForm(Form $form): Form
{
return $form
->schema([
Select::make(self::DERIVE_SOURCE_VIDEO)
->label(__('filament.actions.video.backfill.fields.derive_source.name'))
->options(DeriveSourceVideo::asSelectArray())
->rules(['required', new Enum(DeriveSourceVideo::class)])
->default(DeriveSourceVideo::YES->value)
->helperText(__('filament.actions.video.backfill.fields.derive_source.help')),
Select::make(self::OVERWRITE_AUDIO)
->label(__('filament.actions.video.backfill.fields.overwrite.name'))
->options(OverwriteAudio::asSelectArray())
->rules(['required', new Enum(OverwriteAudio::class)])
->default(OverwriteAudio::NO->value)
->helperText(__('filament.actions.video.backfill.fields.overwrite.help')),
]);
}
}
@@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
namespace App\Filament\HeaderActions\Storage\Base;
use App\Actions\Storage\Base\DeleteAction as BaseDeleteAction;
use App\Filament\HeaderActions\Storage\StorageHeaderAction;
use App\Models\BaseModel;
/**
* Class DeleteHeaderAction.
*/
abstract class DeleteHeaderAction extends StorageHeaderAction
{
/**
* Get the underlying storage action.
*
* @param BaseModel $model
* @param array $fields
* @return BaseDeleteAction
*/
abstract protected function storageAction(BaseModel $model, array $fields): BaseDeleteAction;
}
@@ -0,0 +1,68 @@
<?php
declare(strict_types=1);
namespace App\Filament\HeaderActions\Storage\Base;
use App\Actions\Storage\Base\MoveAction as BaseMoveAction;
use App\Contracts\Storage\InteractsWithDisk;
use App\Filament\HeaderActions\Storage\StorageHeaderAction;
use App\Models\BaseModel;
use App\Rules\Storage\StorageFileDirectoryExistsRule;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Form;
use Illuminate\Support\Facades\Storage;
/**
* Class MoveHeaderAction.
*/
abstract class MoveHeaderAction extends StorageHeaderAction implements InteractsWithDisk
{
/**
* Get the fields available on the action.
*
* @param Form $form
* @return Form
*
* @noinspection PhpMissingParentCallCommonInspection
*/
public function getForm(Form $form): Form
{
$defaultPath = $this->defaultPath();
$fs = Storage::disk($this->disk());
return $form
->schema([
TextInput::make('path')
->label(__('filament.actions.storage.move.fields.path.name'))
->helperText(__('filament.actions.storage.move.fields.path.help'))
->required()
->rules(['required', 'string', 'doesnt_start_with:/', "ends_with:{$this->allowedFileExtension()}", new StorageFileDirectoryExistsRule($fs)])
->default(fn () => $defaultPath),
]);
}
/**
* Get the underlying storage action.
*
* @param BaseModel $model
* @param array $fields
* @return BaseMoveAction
*/
abstract protected function storageAction(BaseModel $model, array $fields): BaseMoveAction;
/**
* Resolve the default value for the path field.
*
* @return string|null
*/
abstract protected function defaultPath(): ?string;
/**
* The file extension that the path must end with.
*
* @return string
*/
abstract protected function allowedFileExtension(): string;
}
@@ -0,0 +1,57 @@
<?php
declare(strict_types=1);
namespace App\Filament\HeaderActions\Storage;
use App\Contracts\Actions\Storage\StorageAction as BaseStorageAction;
use App\Models\BaseModel;
use Filament\Actions\Action;
use Illuminate\Database\Eloquent\Model;
/**
* Class StorageHeaderAction.
*/
abstract class StorageHeaderAction extends Action
{
/**
* Initial setup for the action.
*
* @return void
*/
protected function setUp(): void
{
parent::setUp();
$this->action(fn (BaseModel $record, array $data) => $this->handle($record, $data));
}
/**
* Get the underlying storage action.
*
* @param BaseModel $model
* @param array $fields
* @return BaseStorageAction
*/
abstract protected function storageAction(BaseModel $model, array $fields): BaseStorageAction;
/**
* Perform the action on the given models.
*
* @param BaseModel $model
* @param array $fields
* @return void
*/
public function handle(BaseModel $model, array $fields): void
{
$action = $this->storageAction($model, $fields);
$storageResults = $action->handle();
$storageResults->toLog();
$action->then($storageResults);
$actionResult = $storageResults->toActionResult();
}
}
@@ -0,0 +1,28 @@
<?php
declare(strict_types=1);
namespace App\Filament\HeaderActions\Storage\Wiki\Audio;
use App\Actions\Storage\Wiki\Audio\DeleteAudioAction as DeleteAudio;
use App\Filament\HeaderActions\Storage\Base\DeleteHeaderAction;
use App\Models\BaseModel;
use App\Models\Wiki\Audio;
/**
* Class DeleteAudioHeaderAction.
*/
class DeleteAudioHeaderAction extends DeleteHeaderAction
{
/**
* Get the underlying storage action.
*
* @param Audio $audio
* @param array $fields
* @return DeleteAudio
*/
protected function storageAction(BaseModel $audio, array $fields): DeleteAudio
{
return new DeleteAudio($audio);
}
}
@@ -0,0 +1,68 @@
<?php
declare(strict_types=1);
namespace App\Filament\HeaderActions\Storage\Wiki\Audio;
use App\Actions\Storage\Wiki\Audio\MoveAudioAction as MoveAudio;
use App\Constants\Config\AudioConstants;
use App\Models\Wiki\Audio;
use App\Filament\HeaderActions\Storage\Base\MoveHeaderAction;
use App\Models\BaseModel;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Config;
/**
* Class MoveAudioHeaderAction.
*/
class MoveAudioHeaderAction extends MoveHeaderAction
{
/**
* Get the underlying storage action.
*
* @param Audio $audio
* @param array $fields
* @return MoveAudio
*/
protected function storageAction(BaseModel $audio, array $fields): MoveAudio
{
/** @var string $path */
$path = Arr::get($fields, 'path');
return new MoveAudio($audio, $path);
}
/**
* The name of the disk.
*
* @return string
*/
public function disk(): string
{
return Config::get(AudioConstants::DEFAULT_DISK_QUALIFIED);
}
/**
* Resolve the default value for the path field.
*
* @return string|null
*/
protected function defaultPath(): ?string
{
$audio = $this->getRecord();
return $audio instanceof Audio
? $audio->path
: null;
}
/**
* The file extension that the path must end with.
*
* @return string
*/
protected function allowedFileExtension(): string
{
return '.ogg';
}
}
@@ -0,0 +1,28 @@
<?php
declare(strict_types=1);
namespace App\Filament\HeaderActions\Storage\Wiki\Video;
use App\Actions\Storage\Wiki\Video\DeleteVideoAction as DeleteVideo;
use App\Filament\HeaderActions\Storage\Base\DeleteHeaderAction;
use App\Models\BaseModel;
use App\Models\Wiki\Video;
/**
* Class DeleteVideoHeaderAction.
*/
class DeleteVideoHeaderAction extends DeleteHeaderAction
{
/**
* Get the underlying storage action.
*
* @param Video $video
* @param array $fields
* @return DeleteVideo
*/
protected function storageAction(BaseModel $video, array $fields): DeleteVideo
{
return new DeleteVideo($video);
}
}
@@ -0,0 +1,68 @@
<?php
declare(strict_types=1);
namespace App\Filament\HeaderActions\Storage\Wiki\Video;
use App\Actions\Storage\Wiki\Video\MoveVideoAction as MoveVideo;
use App\Constants\Config\VideoConstants;
use App\Models\Wiki\Video;
use App\Filament\HeaderActions\Storage\Base\MoveHeaderAction;
use App\Models\BaseModel;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Config;
/**
* Class MoveVideoHeaderAction.
*/
class MoveVideoHeaderAction extends MoveHeaderAction
{
/**
* Get the underlying storage action.
*
* @param Video $video
* @param array $fields
* @return MoveVideo
*/
protected function storageAction(BaseModel $video, array $fields): MoveVideo
{
/** @var string $path */
$path = Arr::get($fields, 'path');
return new MoveVideo($video, $path);
}
/**
* The name of the disk.
*
* @return string
*/
public function disk(): string
{
return Config::get(VideoConstants::DEFAULT_DISK_QUALIFIED);
}
/**
* Resolve the default value for the path field.
*
* @return string|null
*/
protected function defaultPath(): ?string
{
$video = $this->getRecord();
return $video instanceof Video
? $video->path
: null;
}
/**
* The file extension that the path must end with.
*
* @return string
*/
protected function allowedFileExtension(): string
{
return '.webm';
}
}
@@ -0,0 +1,28 @@
<?php
declare(strict_types=1);
namespace App\Filament\HeaderActions\Storage\Wiki\Video\Script;
use App\Actions\Storage\Wiki\Video\Script\DeleteScriptAction as DeleteScript;
use App\Filament\HeaderActions\Storage\Base\DeleteHeaderAction;
use App\Models\BaseModel;
use App\Models\Wiki\Video\VideoScript;
/**
* Class DeleteScriptHeaderAction.
*/
class DeleteScriptHeaderAction extends DeleteHeaderAction
{
/**
* Get the underlying storage action.
*
* @param VideoScript $script
* @param array $fields
* @return DeleteScript
*/
protected function storageAction(BaseModel $script, array $fields): DeleteScript
{
return new DeleteScript($script);
}
}
@@ -0,0 +1,69 @@
<?php
declare(strict_types=1);
namespace App\Filament\HeaderActions\Storage\Wiki\Video\Script;
use App\Actions\Storage\Wiki\Video\Script\MoveScriptAction as MoveScript;
use App\Constants\Config\VideoConstants;
use App\Models\Wiki\Video\VideoScript;
use App\Filament\HeaderActions\Storage\Base\MoveHeaderAction;
use App\Models\BaseModel;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Config;
/**
* Class MoveScriptHeaderAction.
*/
class MoveScriptHeaderAction extends MoveHeaderAction
{
/**
* Get the underlying storage action.
*
* @param VideoScript $script
* @param array $fields
* @return MoveScript
*/
protected function storageAction(BaseModel $script, array $fields): MoveScript
{
/** @var string $path */
$path = Arr::get($fields, 'path');
return new MoveScript($script, $path);
}
/**
* The name of the disk.
*
* @return string
*/
public function disk(): string
{
return Config::get(VideoConstants::SCRIPT_DISK_QUALIFIED);
}
/**
* Resolve the default value for the path field.
*
* @return string|null
*/
protected function defaultPath(): ?string
{
$script = $this->getRecord();
return $script instanceof VideoScript
? $script->path
: null;
}
/**
* The file extension that the path must end with.
*
* @return string
*/
protected function allowedFileExtension(): string
{
return '.txt';
}
}
@@ -0,0 +1,63 @@
<?php
declare(strict_types=1);
namespace App\Filament\Providers;
use App\Models\BaseModel;
use Filament\Facades\Filament;
use Filament\GlobalSearch\Contracts\GlobalSearchProvider;
use Filament\GlobalSearch\GlobalSearchResult;
use Filament\GlobalSearch\GlobalSearchResults;
use Elastic\ScoutDriverPlus\Searchable;
/**
* Class GlobalSearchScoutProvider.
*/
class GlobalSearchScoutProvider implements GlobalSearchProvider
{
/**
* Get the results for the global search.
*
* @param string $query
* @return GlobalSearchResults|null
*/
public function getResults(string $query): ?GlobalSearchResults
{
$builder = GlobalSearchResults::make();
foreach (Filament::getResources() as $resource) {
if (!$resource::canGloballySearch() || !in_array(Searchable::class, class_uses_recursive($resource::getModel()))) {
continue;
}
$search = $resource::getModel()::search($query);
$resourceResults = $search
->get()
->map(function (BaseModel $record) use ($resource): ?GlobalSearchResult {
$url = $resource::getGlobalSearchResultUrl($record);
if (blank($url)) {
return null;
}
return new GlobalSearchResult(
title: $resource::getGlobalSearchResultTitle($record),
url: $url,
details: $resource::getGlobalSearchResultDetails($record),
actions: $resource::getGlobalSearchResultActions($record),
);
})
->filter();
if (!$resourceResults->count()) {
continue;
}
$builder->category($resource::getPluralModelLabel(), $resourceResults);
}
return $builder;
}
}
@@ -0,0 +1,259 @@
<?php
declare(strict_types=1);
namespace App\Filament\Resources\Admin;
use App\Filament\Components\Columns\TextColumn;
use App\Filament\Components\Infolist\TextEntry;
use App\Filament\Resources\BaseResource;
use App\Filament\Resources\Admin\Announcement\Pages\CreateAnnouncement;
use App\Filament\Resources\Admin\Announcement\Pages\EditAnnouncement;
use App\Filament\Resources\Admin\Announcement\Pages\ListAnnouncements;
use App\Filament\Resources\Admin\Announcement\Pages\ViewAnnouncement;
use App\Models\Admin\Announcement as AnnouncementModel;
use Filament\Forms\Components\MarkdownEditor;
use Filament\Forms\Form;
use Filament\Infolists\Components\Section;
use Filament\Infolists\Infolist;
use Filament\Tables\Table;
/**
* Class Announcement.
*/
class Announcement extends BaseResource
{
/**
* The model the resource corresponds to.
*
* @var string|null
*/
protected static ?string $model = AnnouncementModel::class;
/**
* Get the displayable singular label of the resource.
*
* @return string
*
* @noinspection PhpMissingParentCallCommonInspection
*/
public static function getLabel(): string
{
return __('filament.resources.singularLabel.announcement');
}
/**
* Get the displayable label of the resource.
*
* @return string
*
* @noinspection PhpMissingParentCallCommonInspection
*/
public static function getPluralLabel(): string
{
return __('filament.resources.label.announcements');
}
/**
* The logical group associated with the resource.
*
* @return string
*
* @noinspection PhpMissingParentCallCommonInspection
*/
public static function getNavigationGroup(): string
{
return __('filament.resources.group.admin');
}
/**
* The icon displayed to the resource.
*
* @return string
*
* @noinspection PhpMissingParentCallCommonInspection
*/
public static function getNavigationIcon(): string
{
return __('filament.resources.icon.announcements');
}
/**
* Get the slug (URI key) for the resource.
*
* @return string
*
* @noinspection PhpMissingParentCallCommonInspection
*/
public static function getSlug(): string
{
return static::getDefaultSlug().'announcements';
}
/**
* Get the route key for the resource.
*
* @return string
*
* @noinspection PhpMissingParentCallCommonInspection
*/
public static function getRecordRouteKeyName(): string
{
return AnnouncementModel::ATTRIBUTE_ID;
}
/**
* The form to the actions.
*
* @param Form $form
* @return Form
*
* @noinspection PhpMissingParentCallCommonInspection
*/
public static function form(Form $form): Form
{
return $form
->schema([
MarkdownEditor::make(AnnouncementModel::ATTRIBUTE_CONTENT)
->label(__('filament.fields.announcement.content'))
->required()
->rules(['required', 'max:65535'])
->maxLength(65535),
])
->columns(1);
}
/**
* The index page of the resource.
*
* @param Table $table
* @return Table
*
* @noinspection PhpMissingParentCallCommonInspection
*/
public static function table(Table $table): Table
{
return parent::table($table)
->columns([
TextColumn::make(AnnouncementModel::ATTRIBUTE_ID)
->label(__('filament.fields.base.id'))
->sortable(),
TextColumn::make(AnnouncementModel::ATTRIBUTE_CONTENT)
->label(__('filament.fields.announcement.content'))
->sortable()
->searchable()
->copyableWithMessage(),
])
->defaultSort(AnnouncementModel::ATTRIBUTE_ID, 'desc')
->filters(static::getFilters())
->filtersFormMaxHeight('400px')
->actions(static::getActions())
->bulkActions(static::getBulkActions());
}
/**
* Get the infolist available for the resource.
*
* @param Infolist $infolist
* @return Infolist
*
* @noinspection PhpMissingParentCallCommonInspection
*/
public static function infolist(Infolist $infolist): Infolist
{
return $infolist
->schema([
Section::make(static::getRecordTitle($infolist->getRecord()))
->schema([
TextEntry::make(AnnouncementModel::ATTRIBUTE_ID)
->label(__('filament.fields.base.id')),
TextEntry::make(AnnouncementModel::ATTRIBUTE_CONTENT)
->label(__('filament.fields.announcement.content'))
->markdown()
->columnSpanFull()
->copyableWithMessage(),
])
->columns(3),
Section::make(__('filament.fields.base.timestamps'))
->schema(parent::timestamps())
->columns(3),
]);
}
/**
* Get the relationships available for the resource.
*
* @return array
*
* @noinspection PhpMissingParentCallCommonInspection
*/
public static function getRelations(): array
{
return [];
}
/**
* Get the filters available for the resource.
*
* @return array
*
* @noinspection PhpMissingParentCallCommonInspection
*/
public static function getFilters(): array
{
return array_merge(
parent::getFilters(),
[]
);
}
/**
* Get the actions available for the resource.
*
* @return array
*
* @noinspection PhpMissingParentCallCommonInspection
*/
public static function getActions(): array
{
return array_merge(
parent::getActions(),
[],
);
}
/**
* Get the bulk actions available for the resource.
*
* @return array
*
* @noinspection PhpMissingParentCallCommonInspection
*/
public static function getBulkActions(): array
{
return array_merge(
parent::getBulkActions(),
[],
);
}
/**
* Get the pages available for the resource.
*
* @return array
*
* @noinspection PhpMissingParentCallCommonInspection
*/
public static function getPages(): array
{
return [
'index' => ListAnnouncements::route('/'),
'create' => CreateAnnouncement::route('/create'),
'view' => ViewAnnouncement::route('/{record:announcement_id}'),
'edit' => EditAnnouncement::route('/{record:announcement_id}/edit'),
];
}
}
@@ -0,0 +1,16 @@
<?php
declare(strict_types=1);
namespace App\Filament\Resources\Admin\Announcement\Pages;
use App\Filament\Resources\Base\BaseCreateResource;
use App\Filament\Resources\Admin\Announcement;
/**
* Class CreateAnnouncement.
*/
class CreateAnnouncement extends BaseCreateResource
{
protected static string $resource = Announcement::class;
}
@@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
namespace App\Filament\Resources\Admin\Announcement\Pages;
use App\Filament\Resources\Admin\Announcement;
use App\Filament\Resources\Base\BaseEditResource;
/**
* Class EditAnnouncement.
*/
class EditAnnouncement extends BaseEditResource
{
protected static string $resource = Announcement::class;
/**
* Get the header actions available.
*
* @return array
*
* @noinspection PhpMissingParentCallCommonInspection
*/
protected function getHeaderActions(): array
{
return array_merge(
parent::getHeaderActions(),
[],
);
}
}
@@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
namespace App\Filament\Resources\Admin\Announcement\Pages;
use App\Filament\Resources\Base\BaseListResources;
use App\Filament\Resources\Admin\Announcement;
/**
* Class ListAnnouncements.
*/
class ListAnnouncements extends BaseListResources
{
protected static string $resource = Announcement::class;
/**
* Get the header actions available.
*
* @return array
*
* @noinspection PhpMissingParentCallCommonInspection
*/
protected function getHeaderActions(): array
{
return array_merge(
parent::getHeaderActions(),
[],
);
}
}
@@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
namespace App\Filament\Resources\Admin\Announcement\Pages;
use App\Filament\Resources\Base\BaseViewResource;
use App\Filament\Resources\Admin\Announcement;
/**
* Class ViewAnnouncement.
*/
class ViewAnnouncement extends BaseViewResource
{
protected static string $resource = Announcement::class;
/**
* Get the header actions available.
*
* @return array
*
* @noinspection PhpMissingParentCallCommonInspection
*/
protected function getHeaderActions(): array
{
return array_merge(
parent::getHeaderActions(),
[],
);
}
}

Some files were not shown because too many files have changed in this diff Show More