feat: external lists (#710)

This commit is contained in:
Kyrch authored and GitHub committed 2024-07-01 23:46:37 -03:00
1 parent 294ab4fb75
commit 9238fd3d57
179 files changed
+5481 -414

No files matched your search

+4
View File
@@ -94,6 +94,10 @@ ELASTIC_SCOUT_DRIVER_REFRESH_DOCUMENTS=false
# elastic migrations
ELASTIC_MIGRATIONS_TABLE=elastic_migrations
# external profile
PROFILE_MAX_ENTRIES=1000
USER_MAX_PROFILES=5
# ffmpeg
FFMPEG_BINARIES=
FFPROBE_BINARIES=
@@ -0,0 +1,106 @@
<?php
declare(strict_types=1);
namespace App\Actions\Models\List\ExternalProfile;
use App\Actions\Http\Api\StoreAction;
use App\Enums\Models\List\AnimeWatchStatus;
use App\Enums\Models\List\ExternalProfileSite;
use App\Models\List\External\ExternalEntry;
use App\Models\Wiki\Anime;
use App\Models\Wiki\ExternalResource;
use Exception;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
/**
* Class StoreExternalProfileAction.
*/
class StoreExternalProfileAction
{
/**
* Store external profile and its entries.
*
* @param Builder $builder
* @param array $profileParameters
* @return Model
*
* @throws Exception
*/
public function store(Builder $builder, array $profileParameters): Model
{
try {
DB::beginTransaction();
// $getEntriesAction = new GetEntries();
// $entries = $getEntriesAction->get($profileParameters);
$entries = [
'name' => 'Kyrch Profile',
'entries' => [
[
ExternalResource::ATTRIBUTE_EXTERNAL_ID => 101573,
ExternalEntry::ATTRIBUTE_SCORE => 10,
ExternalEntry::ATTRIBUTE_IS_FAVORITE => true,
ExternalEntry::ATTRIBUTE_WATCH_STATUS => AnimeWatchStatus::COMPLETED->value,
],
[
ExternalResource::ATTRIBUTE_EXTERNAL_ID => 477,
ExternalEntry::ATTRIBUTE_SCORE => 9.5,
ExternalEntry::ATTRIBUTE_IS_FAVORITE => false,
ExternalEntry::ATTRIBUTE_WATCH_STATUS => AnimeWatchStatus::WATCHING->value,
],
[
ExternalResource::ATTRIBUTE_EXTERNAL_ID => 934,
ExternalEntry::ATTRIBUTE_SCORE => 8,
ExternalEntry::ATTRIBUTE_IS_FAVORITE => false,
ExternalEntry::ATTRIBUTE_WATCH_STATUS => AnimeWatchStatus::PAUSED->value,
],
],
];
$storeProfileAction = new StoreAction();
$profile = $storeProfileAction->store($builder, $profileParameters);
foreach (Arr::get($entries, 'entries') as $entryParameters) {
$storeEntryAction = new StoreAction();
$externalSite = Arr::get($profileParameters, 'site');
$external_id = Arr::get($entryParameters, 'external_id');
$animes = Anime::query()->whereHas(ExternalResource::TABLE, function (Builder $query) use ($externalSite, $external_id) {
$query->where(ExternalResource::ATTRIBUTE_SITE, ExternalProfileSite::getResourceSite($externalSite)->value)
->where(ExternalResource::ATTRIBUTE_EXTERNAL_ID, $external_id);
})->get();
foreach ($animes as $anime) {
if ($anime instanceof Anime) {
$storeEntryAction->store(ExternalEntry::query(), array_merge(
$entryParameters,
[
ExternalEntry::ATTRIBUTE_ANIME => $anime->getKey(),
ExternalEntry::ATTRIBUTE_EXTERNAL_PROFILE => $profile->getKey(),
]
));
}
}
}
DB::commit();
return $storeProfileAction->cleanup($profile);
} catch (Exception $e) {
Log::error($e->getMessage());
DB::rollBack();
throw $e;
}
}
}
@@ -0,0 +1,15 @@
<?php
declare(strict_types=1);
namespace App\Constants\Config;
/**
* Class ExternalProfileConstants.
*/
class ExternalProfileConstants
{
final public const MAX_PROFILES_QUALIFIED = 'externalprofile.user_max_profiles';
final public const MAX_ENTRIES_QUALIFIED = 'externalprofile.profile_max_entries';
}
@@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
namespace App\Enums\Models\List;
use App\Concerns\Enums\LocalizesName;
/**
* Enum AnimeWatchStatus.
*/
enum AnimeWatchStatus: int
{
use LocalizesName;
case WATCHING = 0;
case COMPLETED = 1;
case PAUSED = 2;
case DROPPED = 3;
case PLAN_TO_WATCH = 4;
}
@@ -0,0 +1,36 @@
<?php
declare(strict_types=1);
namespace App\Enums\Models\List;
use App\Concerns\Enums\LocalizesName;
use App\Enums\Models\Wiki\ResourceSite;
/**
* Enum ExternalProfileSite.
*/
enum ExternalProfileSite: int
{
use LocalizesName;
case MAL = 0;
case ANILIST = 1;
case KITSU = 2;
/**
* Get the ResourceSite by the ExternalProfileSite value.
*
* @param int|null $value
* @return ResourceSite|null
*/
public static function getResourceSite(?int $value): ?ResourceSite
{
return match ($value) {
static::MAL->value => ResourceSite::MAL,
static::ANILIST->value => ResourceSite::ANILIST,
static::KITSU->value => ResourceSite::KITSU,
default => null,
};
}
}
@@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
namespace App\Enums\Models\List;
use App\Concerns\Enums\LocalizesName;
/**
* Enum ExternalProfileVisibility.
*/
enum ExternalProfileVisibility: int
{
use LocalizesName;
case PUBLIC = 0;
case PRIVATE = 1;
}
+41
View File
@@ -0,0 +1,41 @@
<?php
declare(strict_types=1);
namespace App\Events\Base\List;
use App\Constants\Config\ServiceConstants;
use App\Events\Base\BaseCreatedEvent;
use Illuminate\Support\Facades\Config;
/**
* Class ListCreatedEvent.
*
* @template TModel of \App\Models\BaseModel
*
* @extends BaseCreatedEvent<TModel>
*/
abstract class ListCreatedEvent extends BaseCreatedEvent
{
/**
* Get Discord channel the message will be sent to.
*
* @return string
*/
public function getDiscordChannel(): string
{
return Config::get(ServiceConstants::ADMIN_DISCORD_CHANNEL_QUALIFIED);
}
/**
* Determine if the message should be sent.
*
* @return bool
*
* @noinspection PhpMissingParentCallCommonInspection
*/
public function shouldSendDiscordMessage(): bool
{
return true;
}
}
+41
View File
@@ -0,0 +1,41 @@
<?php
declare(strict_types=1);
namespace App\Events\Base\List;
use App\Constants\Config\ServiceConstants;
use App\Events\Base\BaseDeletedEvent;
use Illuminate\Support\Facades\Config;
/**
* Class ListDeletedEvent.
*
* @template TModel of \App\Models\BaseModel
*
* @extends BaseDeletedEvent<TModel>
*/
abstract class ListDeletedEvent extends BaseDeletedEvent
{
/**
* Get Discord channel the message will be sent to.
*
* @return string
*/
public function getDiscordChannel(): string
{
return Config::get(ServiceConstants::ADMIN_DISCORD_CHANNEL_QUALIFIED);
}
/**
* Determine if the message should be sent.
*
* @return bool
*
* @noinspection PhpMissingParentCallCommonInspection
*/
public function shouldSendDiscordMessage(): bool
{
return true;
}
}
@@ -0,0 +1,41 @@
<?php
declare(strict_types=1);
namespace App\Events\Base\List;
use App\Constants\Config\ServiceConstants;
use App\Events\Base\BaseRestoredEvent;
use Illuminate\Support\Facades\Config;
/**
* Class ListRestoredEvent.
*
* @template TModel of \App\Models\BaseModel
*
* @extends BaseRestoredEvent<TModel>
*/
abstract class ListRestoredEvent extends BaseRestoredEvent
{
/**
* Get Discord channel the message will be sent to.
*
* @return string
*/
public function getDiscordChannel(): string
{
return Config::get(ServiceConstants::ADMIN_DISCORD_CHANNEL_QUALIFIED);
}
/**
* Determine if the message should be sent.
*
* @return bool
*
* @noinspection PhpMissingParentCallCommonInspection
*/
public function shouldSendDiscordMessage(): bool
{
return true;
}
}
+41
View File
@@ -0,0 +1,41 @@
<?php
declare(strict_types=1);
namespace App\Events\Base\List;
use App\Constants\Config\ServiceConstants;
use App\Events\Base\BaseUpdatedEvent;
use Illuminate\Support\Facades\Config;
/**
* Class ListUpdatedEvent.
*
* @template TModel of \App\Models\BaseModel
*
* @extends BaseUpdatedEvent<TModel>
*/
abstract class ListUpdatedEvent extends BaseUpdatedEvent
{
/**
* Get Discord channel the message will be sent to.
*
* @return string
*/
public function getDiscordChannel(): string
{
return Config::get(ServiceConstants::ADMIN_DISCORD_CHANNEL_QUALIFIED);
}
/**
* Determine if the message should be sent.
*
* @return bool
*
* @noinspection PhpMissingParentCallCommonInspection
*/
public function shouldSendDiscordMessage(): bool
{
return true;
}
}
@@ -0,0 +1,67 @@
<?php
declare(strict_types=1);
namespace App\Events\List\ExternalProfile\ExternalEntry;
use App\Events\Base\List\ListCreatedEvent;
use App\Models\List\External\ExternalEntry;
use App\Models\List\ExternalProfile;
/**
* Class ExternalEntryCreated.
*
* @extends ListCreatedEvent<ExternalEntry>
*/
class ExternalEntryCreated extends ListCreatedEvent
{
/**
* The profile the entry belongs to.
*
* @var ExternalProfile
*/
protected ExternalProfile $profile;
/**
* Create a new event instance.
*
* @param ExternalEntry $entry
*/
public function __construct(ExternalEntry $entry)
{
parent::__construct($entry);
$this->profile = $entry->externalprofile;
}
/**
* Determine if the message should be sent.
*
* @return bool
*
* @noinspection PhpMissingParentCallCommonInspection
*/
public function shouldSendDiscordMessage(): bool
{
return false;
}
/**
* Get the model that has fired this event.
*
* @return ExternalEntry
*/
public function getModel(): ExternalEntry
{
return $this->model;
}
/**
* Get the description for the Discord message payload.
*
* @return string
*/
protected function getDiscordMessageDescription(): string
{
return "Entry '**{$this->getModel()->getName()}**' has been created for External Profile '**{$this->profile->getName()}**'.";
}
}
@@ -0,0 +1,67 @@
<?php
declare(strict_types=1);
namespace App\Events\List\ExternalProfile\ExternalEntry;
use App\Events\Base\List\ListDeletedEvent;
use App\Models\List\ExternalProfile;
use App\Models\List\External\ExternalEntry;
/**
* Class ExternalEntryDeleted.
*
* @extends ListDeletedEvent<ExternalEntry>
*/
class ExternalEntryDeleted extends ListDeletedEvent
{
/**
* The profile the entry belongs to.
*
* @var ExternalProfile
*/
protected ExternalProfile $profile;
/**
* Create a new event instance.
*
* @param ExternalEntry $entry
*/
public function __construct(ExternalEntry $entry)
{
parent::__construct($entry);
$this->profile = $entry->externalprofile;
}
/**
* Determine if the message should be sent.
*
* @return bool
*
* @noinspection PhpMissingParentCallCommonInspection
*/
public function shouldSendDiscordMessage(): bool
{
return false;
}
/**
* Get the model that has fired this event.
*
* @return ExternalEntry
*/
public function getModel(): ExternalEntry
{
return $this->model;
}
/**
* Get the description for the Discord message payload.
*
* @return string
*/
protected function getDiscordMessageDescription(): string
{
return "Entry '**{$this->getModel()->getName()}**' has been deleted for External Profile '**{$this->profile->getName()}**'.";
}
}
@@ -0,0 +1,67 @@
<?php
declare(strict_types=1);
namespace App\Events\List\ExternalProfile\ExternalEntry;
use App\Events\Base\List\ListRestoredEvent;
use App\Models\List\ExternalProfile;
use App\Models\List\External\ExternalEntry;
/**
* Class ExternalEntryRestored.
*
* @extends ListRestoredEvent<ExternalEntry>
*/
class ExternalEntryRestored extends ListRestoredEvent
{
/**
* The profile the entry belongs to.
*
* @var ExternalProfile
*/
protected ExternalProfile $profile;
/**
* Create a new event instance.
*
* @param ExternalEntry $entry
*/
public function __construct(ExternalEntry $entry)
{
parent::__construct($entry);
$this->profile = $entry->externalprofile;
}
/**
* Determine if the message should be sent.
*
* @return bool
*
* @noinspection PhpMissingParentCallCommonInspection
*/
public function shouldSendDiscordMessage(): bool
{
return false;
}
/**
* Get the model that has fired this event.
*
* @return ExternalEntry
*/
public function getModel(): ExternalEntry
{
return $this->model;
}
/**
* Get the description for the Discord message payload.
*
* @return string
*/
protected function getDiscordMessageDescription(): string
{
return "Entry '**{$this->getModel()->getName()}**' has been restored for External Profile '**{$this->profile->getName()}**'.";
}
}
@@ -0,0 +1,68 @@
<?php
declare(strict_types=1);
namespace App\Events\List\ExternalProfile\ExternalEntry;
use App\Events\Base\List\ListUpdatedEvent;
use App\Models\List\ExternalProfile;
use App\Models\List\External\ExternalEntry;
/**
* Class ExternalEntryUpdated.
*
* @extends ListUpdatedEvent<ExternalEntry>
*/
class ExternalEntryUpdated extends ListUpdatedEvent
{
/**
* The profile the entry belongs to.
*
* @var ExternalProfile
*/
protected ExternalProfile $profile;
/**
* Create a new event instance.
*
* @param ExternalEntry $entry
*/
public function __construct(ExternalEntry $entry)
{
parent::__construct($entry);
$this->profile = $entry->externalprofile;
$this->initializeEmbedFields($entry);
}
/**
* Determine if the message should be sent.
*
* @return bool
*
* @noinspection PhpMissingParentCallCommonInspection
*/
public function shouldSendDiscordMessage(): bool
{
return false;
}
/**
* Get the model that has fired this event.
*
* @return ExternalEntry
*/
public function getModel(): ExternalEntry
{
return $this->model;
}
/**
* Get the description for the Discord message payload.
*
* @return string
*/
protected function getDiscordMessageDescription(): string
{
return "Entry '**{$this->getModel()->getName()}**' has been updated for External Profile '**{$this->profile->getName()}**'.";
}
}
@@ -0,0 +1,46 @@
<?php
declare(strict_types=1);
namespace App\Events\List\ExternalProfile;
use App\Events\Base\List\ListCreatedEvent;
use App\Models\List\ExternalProfile;
/**
* Class ExternalProfileCreated.
*
* @extends ListCreatedEvent<ExternalProfile>
*/
class ExternalProfileCreated extends ListCreatedEvent
{
/**
* Create a new event instance.
*
* @param ExternalProfile $profile
*/
public function __construct(ExternalProfile $profile)
{
parent::__construct($profile);
}
/**
* Get the model that has fired this event.
*
* @return ExternalProfile
*/
public function getModel(): ExternalProfile
{
return $this->model;
}
/**
* Get the description for the Discord message payload.
*
* @return string
*/
protected function getDiscordMessageDescription(): string
{
return "External Profile '**{$this->getModel()->getName()}**' has been created.";
}
}
@@ -0,0 +1,58 @@
<?php
declare(strict_types=1);
namespace App\Events\List\ExternalProfile;
use App\Events\Base\List\ListDeletedEvent;
use App\Models\List\ExternalProfile;
/**
* Class ExternalProfileDeleted.
*
* @extends ListDeletedEvent<ExternalProfile>
*/
class ExternalProfileDeleted extends ListDeletedEvent
{
/**
* Create a new event instance.
*
* @param ExternalProfile $profile
*/
public function __construct(ExternalProfile $profile)
{
parent::__construct($profile);
}
/**
* Determine if the message should be sent.
*
* @return bool
*
* @noinspection PhpMissingParentCallCommonInspection
*/
public function shouldSendDiscordMessage(): bool
{
return false;
}
/**
* Get the model that has fired this event.
*
* @return ExternalProfile
*/
public function getModel(): ExternalProfile
{
return $this->model;
}
/**
* Get the description for the Discord message payload.
*
* @return string
*/
protected function getDiscordMessageDescription(): string
{
return "External Profile '**{$this->getModel()->getName()}**' has been deleted.";
}
}
@@ -0,0 +1,58 @@
<?php
declare(strict_types=1);
namespace App\Events\List\ExternalProfile;
use App\Events\Base\List\ListRestoredEvent;
use App\Models\List\ExternalProfile;
/**
* Class ExternalProfileRestored.
*
* @extends ListRestoredEvent<ExternalProfile>
*/
class ExternalProfileRestored extends ListRestoredEvent
{
/**
* Create a new event instance.
*
* @param ExternalProfile $profile
*/
public function __construct(ExternalProfile $profile)
{
parent::__construct($profile);
}
/**
* Determine if the message should be sent.
*
* @return bool
*
* @noinspection PhpMissingParentCallCommonInspection
*/
public function shouldSendDiscordMessage(): bool
{
return false;
}
/**
* Get the model that has fired this event.
*
* @return ExternalProfile
*/
public function getModel(): ExternalProfile
{
return $this->model;
}
/**
* Get the description for the Discord message payload.
*
* @return string
*/
protected function getDiscordMessageDescription(): string
{
return "External Profile '**{$this->getModel()->getName()}**' has been restored.";
}
}
@@ -0,0 +1,59 @@
<?php
declare(strict_types=1);
namespace App\Events\List\ExternalProfile;
use App\Events\Base\List\ListUpdatedEvent;
use App\Models\List\ExternalProfile;
/**
* Class ExternalProfileUpdated.
*
* @extends ListUpdatedEvent<ExternalProfile>
*/
class ExternalProfileUpdated extends ListUpdatedEvent
{
/**
* Create a new event instance.
*
* @param ExternalProfile $profile
*/
public function __construct(ExternalProfile $profile)
{
parent::__construct($profile);
$this->initializeEmbedFields($profile);
}
/**
* Determine if the message should be sent.
*
* @return bool
*
* @noinspection PhpMissingParentCallCommonInspection
*/
public function shouldSendDiscordMessage(): bool
{
return false;
}
/**
* Get the model that has fired this event.
*
* @return ExternalProfile
*/
public function getModel(): ExternalProfile
{
return $this->model;
}
/**
* Get the description for the Discord message payload.
*
* @return string
*/
protected function getDiscordMessageDescription(): string
{
return "External Profile '**{$this->getModel()->getName()}**' has been updated.";
}
}
+3 -27
View File
@@ -4,18 +4,16 @@ declare(strict_types=1);
namespace App\Events\List\Playlist;
use App\Constants\Config\ServiceConstants;
use App\Contracts\Events\AssignHashidsEvent;
use App\Events\Base\BaseCreatedEvent;
use App\Events\Base\List\ListCreatedEvent;
use App\Models\List\Playlist;
use Illuminate\Support\Facades\Config;
/**
* Class PlaylistCreated.
*
* @extends BaseCreatedEvent<Playlist>
* @extends ListCreatedEvent<Playlist>
*/
class PlaylistCreated extends BaseCreatedEvent implements AssignHashidsEvent
class PlaylistCreated extends ListCreatedEvent implements AssignHashidsEvent
{
/**
* Create a new event instance.
@@ -27,28 +25,6 @@ class PlaylistCreated extends BaseCreatedEvent implements AssignHashidsEvent
parent::__construct($playlist);
}
/**
* Get Discord channel the message will be sent to.
*
* @return string
*/
public function getDiscordChannel(): string
{
return Config::get(ServiceConstants::ADMIN_DISCORD_CHANNEL_QUALIFIED);
}
/**
* Determine if the message should be sent.
*
* @return bool
*
* @noinspection PhpMissingParentCallCommonInspection
*/
public function shouldSendDiscordMessage(): bool
{
return true;
}
/**
* Get the model that has fired this event.
*
+3 -15
View File
@@ -4,17 +4,15 @@ declare(strict_types=1);
namespace App\Events\List\Playlist;
use App\Constants\Config\ServiceConstants;
use App\Events\Base\BaseDeletedEvent;
use App\Events\Base\List\ListDeletedEvent;
use App\Models\List\Playlist;
use Illuminate\Support\Facades\Config;
/**
* Class PlaylistDeleted.
*
* @extends BaseDeletedEvent<Playlist>
* @extends ListDeletedEvent<Playlist>
*/
class PlaylistDeleted extends BaseDeletedEvent
class PlaylistDeleted extends ListDeletedEvent
{
/**
* Create a new event instance.
@@ -26,16 +24,6 @@ class PlaylistDeleted extends BaseDeletedEvent
parent::__construct($playlist);
}
/**
* Get Discord channel the message will be sent to.
*
* @return string
*/
public function getDiscordChannel(): string
{
return Config::get(ServiceConstants::ADMIN_DISCORD_CHANNEL_QUALIFIED);
}
/**
* Determine if the message should be sent.
*
+3 -15
View File
@@ -4,17 +4,15 @@ declare(strict_types=1);
namespace App\Events\List\Playlist;
use App\Constants\Config\ServiceConstants;
use App\Events\Base\BaseRestoredEvent;
use App\Events\Base\List\ListRestoredEvent;
use App\Models\List\Playlist;
use Illuminate\Support\Facades\Config;
/**
* Class PlaylistRestored.
*
* @extends BaseRestoredEvent<Playlist>
* @extends ListRestoredEvent<Playlist>
*/
class PlaylistRestored extends BaseRestoredEvent
class PlaylistRestored extends ListRestoredEvent
{
/**
* Create a new event instance.
@@ -26,16 +24,6 @@ class PlaylistRestored extends BaseRestoredEvent
parent::__construct($playlist);
}
/**
* Get Discord channel the message will be sent to.
*
* @return string
*/
public function getDiscordChannel(): string
{
return Config::get(ServiceConstants::ADMIN_DISCORD_CHANNEL_QUALIFIED);
}
/**
* Determine if the message should be sent.
*
+3 -27
View File
@@ -4,17 +4,15 @@ declare(strict_types=1);
namespace App\Events\List\Playlist;
use App\Constants\Config\ServiceConstants;
use App\Events\Base\BaseUpdatedEvent;
use App\Events\Base\List\ListUpdatedEvent;
use App\Models\List\Playlist;
use Illuminate\Support\Facades\Config;
/**
* Class PlaylistUpdated.
*
* @extends BaseUpdatedEvent<Playlist>
* @extends ListUpdatedEvent<Playlist>
*/
class PlaylistUpdated extends BaseUpdatedEvent
class PlaylistUpdated extends ListUpdatedEvent
{
/**
* Create a new event instance.
@@ -27,28 +25,6 @@ class PlaylistUpdated extends BaseUpdatedEvent
$this->initializeEmbedFields($playlist);
}
/**
* Get Discord channel the message will be sent to.
*
* @return string
*/
public function getDiscordChannel(): string
{
return Config::get(ServiceConstants::ADMIN_DISCORD_CHANNEL_QUALIFIED);
}
/**
* Determine if the message should be sent.
*
* @return bool
*
* @noinspection PhpMissingParentCallCommonInspection
*/
public function shouldSendDiscordMessage(): bool
{
return false;
}
/**
* Get the model that has fired this event.
*
@@ -4,19 +4,17 @@ declare(strict_types=1);
namespace App\Events\List\Playlist\Track;
use App\Constants\Config\ServiceConstants;
use App\Contracts\Events\AssignHashidsEvent;
use App\Events\Base\BaseCreatedEvent;
use App\Events\Base\List\ListCreatedEvent;
use App\Models\List\Playlist;
use App\Models\List\Playlist\PlaylistTrack;
use Illuminate\Support\Facades\Config;
/**
* Class TrackCreated.
*
* @extends BaseCreatedEvent<PlaylistTrack>
* @extends ListCreatedEvent<PlaylistTrack>
*/
class TrackCreated extends BaseCreatedEvent implements AssignHashidsEvent
class TrackCreated extends ListCreatedEvent implements AssignHashidsEvent
{
/**
* The playlist the track belongs to.
@@ -36,16 +34,6 @@ class TrackCreated extends BaseCreatedEvent implements AssignHashidsEvent
$this->playlist = $track->playlist;
}
/**
* Get Discord channel the message will be sent to.
*
* @return string
*/
public function getDiscordChannel(): string
{
return Config::get(ServiceConstants::ADMIN_DISCORD_CHANNEL_QUALIFIED);
}
/**
* Determine if the message should be sent.
*
@@ -4,18 +4,16 @@ declare(strict_types=1);
namespace App\Events\List\Playlist\Track;
use App\Constants\Config\ServiceConstants;
use App\Events\Base\BaseDeletedEvent;
use App\Events\Base\List\ListDeletedEvent;
use App\Models\List\Playlist;
use App\Models\List\Playlist\PlaylistTrack;
use Illuminate\Support\Facades\Config;
/**
* Class TrackDeleted.
*
* @extends BaseDeletedEvent<PlaylistTrack>
* @extends ListDeletedEvent<PlaylistTrack>
*/
class TrackDeleted extends BaseDeletedEvent
class TrackDeleted extends ListDeletedEvent
{
/**
* The playlist the track belongs to.
@@ -35,16 +33,6 @@ class TrackDeleted extends BaseDeletedEvent
$this->playlist = $track->playlist;
}
/**
* Get Discord channel the message will be sent to.
*
* @return string
*/
public function getDiscordChannel(): string
{
return Config::get(ServiceConstants::ADMIN_DISCORD_CHANNEL_QUALIFIED);
}
/**
* Determine if the message should be sent.
*
@@ -4,18 +4,16 @@ declare(strict_types=1);
namespace App\Events\List\Playlist\Track;
use App\Constants\Config\ServiceConstants;
use App\Events\Base\BaseRestoredEvent;
use App\Events\Base\List\ListRestoredEvent;
use App\Models\List\Playlist;
use App\Models\List\Playlist\PlaylistTrack;
use Illuminate\Support\Facades\Config;
/**
* Class TrackRestored.
*
* @extends BaseRestoredEvent<PlaylistTrack>
* @extends ListRestoredEvent<PlaylistTrack>
*/
class TrackRestored extends BaseRestoredEvent
class TrackRestored extends ListRestoredEvent
{
/**
* The playlist the track belongs to.
@@ -35,16 +33,6 @@ class TrackRestored extends BaseRestoredEvent
$this->playlist = $track->playlist;
}
/**
* Get Discord channel the message will be sent to.
*
* @return string
*/
public function getDiscordChannel(): string
{
return Config::get(ServiceConstants::ADMIN_DISCORD_CHANNEL_QUALIFIED);
}
/**
* Determine if the message should be sent.
*
@@ -4,18 +4,16 @@ declare(strict_types=1);
namespace App\Events\List\Playlist\Track;
use App\Constants\Config\ServiceConstants;
use App\Events\Base\BaseUpdatedEvent;
use App\Events\Base\List\ListUpdatedEvent;
use App\Models\List\Playlist;
use App\Models\List\Playlist\PlaylistTrack;
use Illuminate\Support\Facades\Config;
/**
* Class TrackUpdated.
*
* @extends BaseUpdatedEvent<PlaylistTrack>
* @extends ListUpdatedEvent<PlaylistTrack>
*/
class TrackUpdated extends BaseUpdatedEvent
class TrackUpdated extends ListUpdatedEvent
{
/**
* The playlist the track belongs to.
@@ -36,16 +34,6 @@ class TrackUpdated extends BaseUpdatedEvent
$this->initializeEmbedFields($track);
}
/**
* Get Discord channel the message will be sent to.
*
* @return string
*/
public function getDiscordChannel(): string
{
return Config::get(ServiceConstants::ADMIN_DISCORD_CHANNEL_QUALIFIED);
}
/**
* Determine if the message should be sent.
*
@@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
namespace App\Features;
use App\Enums\Auth\SpecialPermission;
use App\Models\Auth\User;
use Laravel\Pennant\Feature;
/**
* Class AllowExternalProfileManagement.
*/
class AllowExternalProfileManagement
{
/**
* Resolve feature for scope.
*
* @param User|null $user
* @return bool
*/
public function resolve(?User $user): bool
{
if (! empty($user?->can(SpecialPermission::BYPASS_FEATURE_FLAGS->value))) {
return true;
}
return Feature::for(null)->value(static::class);
}
}
@@ -1,71 +0,0 @@
<?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 $fromLabel = '';
protected string $toLabel = '';
/**
* 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->getName().'_'.'from')
->label($this->fromLabel),
DatePicker::make($this->getName().'_'.'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->getName().'_'.'from'),
fn (Builder $query, $date): Builder => $query->whereDate($this->getName(), ComparisonOperator::GTE->value, $date),
)
->when(
Arr::get($data, $this->getName().'_'.'to'),
fn (Builder $query, $date): Builder => $query->whereDate($this->getName(), ComparisonOperator::LTE->value, $date),
);
}
}
@@ -5,6 +5,9 @@ declare(strict_types=1);
namespace App\Filament\Components\Filters;
use App\Enums\Http\Api\Filter\ComparisonOperator;
use Filament\Forms\Components\Fieldset;
use Filament\Forms\Components\Grid;
use Filament\Forms\Components\Section;
use Filament\Forms\Components\TextInput;
use Filament\Tables\Filters\Filter;
use Illuminate\Database\Eloquent\Builder;
@@ -15,25 +18,6 @@ use Illuminate\Support\Arr;
*/
class NumberFilter extends Filter
{
protected string $fromLabel = '';
protected string $toLabel = '';
/**
* 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.
*
@@ -42,13 +26,22 @@ class NumberFilter extends Filter
public function getFormSchema(): array
{
return [
TextInput::make($this->getName().'_'.'from')
->label($this->fromLabel)
->integer(),
TextInput::make($this->getName().'_'.'to')
->label($this->toLabel)
->integer(),
Fieldset::make($this->getLabel())
->label($this->getLabel())
->schema([
Grid::make([
'sm' => 2,
])
->schema([
TextInput::make('from')
->label(__('filament.filters.base.from'))
->integer(),
TextInput::make('to')
->label(__('filament.filters.base.to'))
->integer(),
])
]),
];
}
@@ -63,11 +56,11 @@ class NumberFilter extends Filter
{
return $query
->when(
Arr::get($data, $this->getName().'_'.'from'),
Arr::get($data, 'from'),
fn (Builder $query, $value): Builder => $query->where($this->getName(), ComparisonOperator::GTE->value, $value),
)
->when(
Arr::get($data, $this->getName().'_'.'to'),
Arr::get($data, 'to'),
fn (Builder $query, $value): Builder => $query->where($this->getName(), ComparisonOperator::LTE->value, $value),
);
}
@@ -6,6 +6,7 @@ namespace App\Filament\Dashboards;
use App\Enums\Auth\Role as RoleEnum;
use App\Filament\Widgets\Auth\UserChart;
use App\Filament\Widgets\List\ExternalProfileChart;
use App\Filament\Widgets\List\PlaylistChart;
use App\Filament\Widgets\List\PlaylistTrackChart;
use App\Models\Auth\User;
@@ -65,6 +66,7 @@ class AdminDashboard extends BaseDashboard
{
return [
UserChart::class,
ExternalProfileChart::class,
PlaylistChart::class,
PlaylistTrackChart::class,
];
@@ -0,0 +1,110 @@
<?php
declare(strict_types=1);
namespace App\Filament\RelationManagers\List\External;
use App\Filament\RelationManagers\BaseRelationManager;
use App\Filament\Resources\List\External\ExternalEntry as ExternalEntryResource;
use App\Models\List\External\ExternalEntry;
use Filament\Forms\Form;
use Filament\Tables\Table;
/**
* Class ExternalEntryRelationManager.
*/
abstract class ExternalEntryRelationManager extends BaseRelationManager
{
/**
* The form to the actions.
*
* @param Form $form
* @return Form
*
* @noinspection PhpMissingParentCallCommonInspection
*/
public function form(Form $form): Form
{
return ExternalEntryResource::form($form);
}
/**
* The index page of the resource.
*
* @param Table $table
* @return Table
*
* @noinspection PhpMissingParentCallCommonInspection
*/
public function table(Table $table): Table
{
return parent::table(
$table
->heading(ExternalEntryResource::getPluralLabel())
->modelLabel(ExternalEntryResource::getLabel())
->recordTitleAttribute(ExternalEntry::ATTRIBUTE_ID)
->columns(ExternalEntryResource::table($table)->getColumns())
->defaultSort(ExternalEntry::TABLE . '.' . ExternalEntry::ATTRIBUTE_ID, 'desc')
);
}
/**
* Get the filters available for the relation.
*
* @return array
*
* @noinspection PhpMissingParentCallCommonInspection
*/
public static function getFilters(): array
{
return array_merge(
[],
ExternalEntryResource::getFilters(),
);
}
/**
* Get the actions available for the relation.
*
* @return array
*
* @noinspection PhpMissingParentCallCommonInspection
*/
public static function getActions(): array
{
return array_merge(
parent::getActions(),
ExternalEntryResource::getActions(),
);
}
/**
* Get the bulk actions available for the relation.
*
* @return array
*
* @noinspection PhpMissingParentCallCommonInspection
*/
public static function getBulkActions(): array
{
return array_merge(
parent::getBulkActions(),
ExternalEntryResource::getBulkActions(),
);
}
/**
* Get the header actions available for the relation.
*
* @return array
*
* @noinspection PhpMissingParentCallCommonInspection
*/
public static function getHeaderActions(): array
{
return array_merge(
parent::getHeaderActions(),
ExternalEntryResource::getHeaderActions(),
);
}
}
@@ -0,0 +1,110 @@
<?php
declare(strict_types=1);
namespace App\Filament\RelationManagers\List;
use App\Filament\RelationManagers\BaseRelationManager;
use App\Filament\Resources\List\ExternalProfile as ExternalProfileResource;
use App\Models\List\ExternalProfile;
use Filament\Forms\Form;
use Filament\Tables\Table;
/**
* Class ExternalProfileRelationManager.
*/
abstract class ExternalProfileRelationManager extends BaseRelationManager
{
/**
* The form to the actions.
*
* @param Form $form
* @return Form
*
* @noinspection PhpMissingParentCallCommonInspection
*/
public function form(Form $form): Form
{
return ExternalProfileResource::form($form);
}
/**
* The index page of the resource.
*
* @param Table $table
* @return Table
*
* @noinspection PhpMissingParentCallCommonInspection
*/
public function table(Table $table): Table
{
return parent::table(
$table
->heading(ExternalProfileResource::getPluralLabel())
->modelLabel(ExternalProfileResource::getLabel())
->recordTitleAttribute(ExternalProfile::ATTRIBUTE_NAME)
->columns(ExternalProfileResource::table($table)->getColumns())
->defaultSort(ExternalProfile::TABLE . '.' . ExternalProfile::ATTRIBUTE_ID, 'desc')
);
}
/**
* Get the filters available for the relation.
*
* @return array
*
* @noinspection PhpMissingParentCallCommonInspection
*/
public static function getFilters(): array
{
return array_merge(
[],
ExternalProfileResource::getFilters(),
);
}
/**
* Get the actions available for the relation.
*
* @return array
*
* @noinspection PhpMissingParentCallCommonInspection
*/
public static function getActions(): array
{
return array_merge(
parent::getActions(),
ExternalProfileResource::getActions(),
);
}
/**
* Get the bulk actions available for the relation.
*
* @return array
*
* @noinspection PhpMissingParentCallCommonInspection
*/
public static function getBulkActions(): array
{
return array_merge(
parent::getBulkActions(),
ExternalProfileResource::getBulkActions(),
);
}
/**
* Get the header actions available for the relation.
*
* @return array
*
* @noinspection PhpMissingParentCallCommonInspection
*/
public static function getHeaderActions(): array
{
return array_merge(
parent::getHeaderActions(),
ExternalProfileResource::getHeaderActions(),
);
}
}
+5 -5
View File
@@ -7,7 +7,6 @@ namespace App\Filament\Resources\Admin;
use App\Enums\Actions\ActionLogStatus;
use App\Enums\Auth\Role;
use App\Filament\Components\Columns\TextColumn;
use App\Filament\Components\Filters\DateFilter;
use App\Filament\Components\Infolist\TextEntry;
use App\Filament\Resources\BaseResource;
use App\Filament\Resources\Admin\ActionLog\Pages\ListActionLogs;
@@ -25,6 +24,7 @@ use Filament\Resources\RelationManagers\RelationGroup;
use Filament\Tables\Filters\SelectFilter;
use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Model;
use Malzariey\FilamentDaterangepickerFilter\Filters\DateRangeFilter;
/**
* Class ActionLog.
@@ -269,11 +269,11 @@ class ActionLog extends BaseResource
->label(__('filament.fields.action_log.status'))
->options(ActionLogStatus::asSelectArray()),
DateFilter::make(BaseModel::ATTRIBUTE_CREATED_AT)
->labels(__('filament.filters.action_log.happened_at_from'), __('filament.filters.action_log.happened_at_to')),
DateRangeFilter::make(BaseModel::ATTRIBUTE_CREATED_AT)
->label(__('filament.fields.action_log.happened_at')),
DateFilter::make(ActionLogModel::ATTRIBUTE_FINISHED_AT)
->labels(__('filament.filters.action_log.finished_at_from'), __('filament.filters.action_log.finished_at_to')),
DateRangeFilter::make(ActionLogModel::ATTRIBUTE_FINISHED_AT)
->label(__('filament.fields.action_log.finished_at')),
];
}
+24 -30
View File
@@ -22,15 +22,16 @@ use App\Models\Auth\User;
use App\Models\Wiki\Anime\Theme\AnimeThemeEntry as EntryModel;
use App\Models\Wiki\Video;
use App\Pivots\Wiki\AnimeThemeEntryVideo;
use Filament\Forms\Components\DatePicker;
use App\Rules\Admin\StartDateBeforeEndDateRule;
use Filament\Forms\Form;
use Filament\Forms\Get;
use Filament\Infolists\Components\Section;
use Filament\Infolists\Infolist;
use Filament\Resources\RelationManagers\RelationGroup;
use Filament\Tables\Table;
use Illuminate\Support\Str;
use Illuminate\Support\Carbon;
use Illuminate\Validation\Rule;
use Malzariey\FilamentDaterangepickerFilter\Fields\DateRangePicker;
/**
* Class FeaturedTheme.
@@ -130,33 +131,26 @@ class FeaturedTheme extends BaseResource
return $form
->schema([
DatePicker::make(FeaturedThemeModel::ATTRIBUTE_START_AT)
->label(__('filament.fields.featured_theme.start_at.name'))
->helperText(__('filament.fields.featured_theme.start_at.help'))
DateRangePicker::make('date')
->label(__('filament.fields.featured_theme.date.name'))
->helperText(__('filament.fields.featured_theme.date.help'))
->timezone('UTC')
->displayFormat('MM/DD/YYYY')
->format(AllowedDateFormat::YMD->value)
->required()
->rules([
'required',
Str::of('date_format:')
->append(implode(',', $allowedDateFormats))
->__toString(),
Str::of('before:')
->append(FeaturedThemeModel::ATTRIBUTE_END_AT)
->__toString(),
]),
DatePicker::make(FeaturedThemeModel::ATTRIBUTE_END_AT)
->label(__('filament.fields.featured_theme.end_at.name'))
->helperText(__('filament.fields.featured_theme.end_at.help'))
->required()
->rules([
'required',
Str::of('date_format:')
->append(implode(',', $allowedDateFormats))
->__toString(),
Str::of('after:')
->append(FeaturedThemeModel::ATTRIBUTE_START_AT)
->__toString(),
]),
'string',
'regex:/^\d{2}\/\d{2}\/\d{4} - \d{2}\/\d{2}\/\d{4}$/',
new StartDateBeforeEndDateRule(),
])
->saveRelationshipsUsing(function (FeaturedThemeModel $record, string $state) {
$dates = explode(' - ', $state);
$record->start_at = Carbon::createFromFormat('m/d/Y', $dates[0]);
$record->end_at = Carbon::createFromFormat('m/d/Y', $dates[1]);
$record->save();
}),
Select::make(FeaturedThemeModel::ATTRIBUTE_ENTRY)
->label(__('filament.resources.singularLabel.anime_theme_entry'))
@@ -234,13 +228,13 @@ class FeaturedTheme extends BaseResource
->sortable(),
TextColumn::make(FeaturedThemeModel::ATTRIBUTE_START_AT)
->label(__('filament.fields.featured_theme.start_at.name'))
->label(__('filament.fields.featured_theme.start_at'))
->sortable()
->date()
->toggleable(),
TextColumn::make(FeaturedThemeModel::ATTRIBUTE_END_AT)
->label(__('filament.fields.featured_theme.end_at.name'))
->label(__('filament.fields.featured_theme.end_at'))
->sortable()
->date()
->toggleable(),
@@ -284,11 +278,11 @@ class FeaturedTheme extends BaseResource
->label(__('filament.fields.base.id')),
TextEntry::make(FeaturedThemeModel::ATTRIBUTE_START_AT)
->label(__('filament.fields.featured_theme.start_at.name'))
->label(__('filament.fields.featured_theme.start_at'))
->date(),
TextEntry::make(FeaturedThemeModel::ATTRIBUTE_END_AT)
->label(__('filament.fields.featured_theme.end_at.name'))
->label(__('filament.fields.featured_theme.end_at'))
->date(),
TextEntry::make(FeaturedThemeModel::RELATION_VIDEO.'.'.Video::ATTRIBUTE_FILENAME)
+1 -1
View File
@@ -272,7 +272,7 @@ class Role extends BaseResource
->checkbox(),
NumberFilter::make(RoleModel::ATTRIBUTE_PRIORITY)
->labels(__('filament.filters.role.priority_from'), __('filament.filters.role.priority_to')),
->label(__('filament.fields.role.priority.name')),
];
}
+7 -7
View File
@@ -13,7 +13,6 @@ use App\Filament\Actions\Base\ViewAction;
use App\Filament\BulkActions\Base\DeleteBulkAction;
use App\Filament\BulkActions\Base\ForceDeleteBulkAction;
use App\Filament\BulkActions\Base\RestoreBulkAction;
use App\Filament\Components\Filters\DateFilter;
use App\Filament\RelationManagers\Base\ActionLogRelationManager;
use App\Models\BaseModel;
use Filament\Infolists\Components\TextEntry;
@@ -25,6 +24,7 @@ use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletingScope;
use Malzariey\FilamentDaterangepickerFilter\Filters\DateRangeFilter;
/**
* Class BaseResource.
@@ -100,14 +100,14 @@ abstract class BaseResource extends Resource
return [
TrashedFilter::make(),
DateFilter::make(BaseModel::ATTRIBUTE_CREATED_AT)
->labels(__('filament.filters.base.created_at_from'), __('filament.filters.base.created_at_to')),
DateRangeFilter::make(BaseModel::ATTRIBUTE_CREATED_AT)
->label(__('filament.fields.base.created_at')),
DateFilter::make(BaseModel::ATTRIBUTE_UPDATED_AT)
->labels(__('filament.filters.base.updated_at_from'), __('filament.filters.base.updated_at_to')),
DateRangeFilter::make(BaseModel::ATTRIBUTE_UPDATED_AT)
->label(__('filament.fields.base.updated_at')),
DateFilter::make(BaseModel::ATTRIBUTE_DELETED_AT)
->labels(__('filament.filters.base.deleted_at_from'), __('filament.filters.base.deleted_at_to')),
DateRangeFilter::make(BaseModel::ATTRIBUTE_DELETED_AT)
->label(__('filament.fields.base.deleted_at')),
];
}
+349
View File
@@ -0,0 +1,349 @@
<?php
declare(strict_types=1);
namespace App\Filament\Resources\List\External;
use App\Enums\Models\List\AnimeWatchStatus;
use App\Filament\Components\Columns\TextColumn;
use App\Filament\Components\Fields\Select;
use App\Filament\Components\Infolist\TextEntry;
use App\Filament\Resources\BaseResource;
use App\Filament\Resources\List\External\ExternalEntry\Pages\CreateExternalEntry;
use App\Filament\Resources\List\External\ExternalEntry\Pages\EditExternalEntry;
use App\Filament\Resources\List\External\ExternalEntry\Pages\ListExternalEntries;
use App\Filament\Resources\List\External\ExternalEntry\Pages\ViewExternalEntry;
use App\Filament\Resources\List\External\RelationManagers\ExternalEntryExternalProfileRelationManager;
use App\Filament\Resources\List\ExternalProfile as ExternalProfileResource;
use App\Filament\Resources\Wiki\Anime;
use App\Models\List\External\ExternalEntry as ExternalEntryModel;
use App\Models\List\ExternalProfile as ExternalProfileModel;
use App\Models\Wiki\Anime as AnimeModel;
use Filament\Forms\Components\Checkbox;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Form;
use Filament\Infolists\Components\IconEntry;
use Filament\Infolists\Components\Section;
use Filament\Infolists\Infolist;
use Filament\Resources\RelationManagers\RelationGroup;
use Filament\Tables\Columns\IconColumn;
use Filament\Tables\Table;
use Illuminate\Validation\Rules\Enum;
/**
* Class ExternalEntry.
*/
class ExternalEntry extends BaseResource
{
/**
* The model the resource corresponds to.
*
* @var string|null
*/
protected static ?string $model = ExternalEntryModel::class;
/**
* Get the displayable singular label of the resource.
*
* @return string
*
* @noinspection PhpMissingParentCallCommonInspection
*/
public static function getLabel(): string
{
return __('filament.resources.singularLabel.external_entry');
}
/**
* Get the displayable label of the resource.
*
* @return string
*
* @noinspection PhpMissingParentCallCommonInspection
*/
public static function getPluralLabel(): string
{
return __('filament.resources.label.external_entries');
}
/**
* The logical group associated with the resource.
*
* @return string
*
* @noinspection PhpMissingParentCallCommonInspection
*/
public static function getNavigationGroup(): string
{
return __('filament.resources.group.list');
}
/**
* The icon displayed to the resource.
*
* @return string
*
* @noinspection PhpMissingParentCallCommonInspection
*/
public static function getNavigationIcon(): string
{
return __('filament.resources.icon.external_entries');
}
/**
* Get the slug (URI key) for the resource.
*
* @return string
*
* @noinspection PhpMissingParentCallCommonInspection
*/
public static function getRecordSlug(): string
{
return 'external-entries';
}
/**
* Get the route key for the resource.
*
* @return string
*
* @noinspection PhpMissingParentCallCommonInspection
*/
public static function getRecordRouteKeyName(): string
{
return ExternalEntryModel::ATTRIBUTE_ID;
}
/**
* The form to the actions.
*
* @param Form $form
* @return Form
*
* @noinspection PhpMissingParentCallCommonInspection
*/
public static function form(Form $form): Form
{
return $form
->schema([
Select::make(ExternalEntryModel::ATTRIBUTE_EXTERNAL_PROFILE)
->label(__('filament.resources.singularLabel.external_profile'))
->useScout(ExternalProfileModel::class)
->required()
->rules(['required'])
->hiddenOn([ExternalEntryExternalProfileRelationManager::class])
->createOptionForm(ExternalProfileResource::form($form)->getComponents()),
Select::make(ExternalEntryModel::ATTRIBUTE_ANIME)
->label(__('filament.resources.singularLabel.anime'))
->useScout(AnimeModel::class)
->required()
->rules(['required']),
TextInput::make(ExternalEntryModel::ATTRIBUTE_SCORE)
->label(__('filament.fields.external_entry.score.name'))
->helperText(__('filament.fields.external_entry.score.help'))
->numeric(),
Select::make(ExternalEntryModel::ATTRIBUTE_WATCH_STATUS)
->label(__('filament.fields.external_entry.watch_status.name'))
->helperText(__('filament.fields.external_entry.watch_status.help'))
->options(AnimeWatchStatus::asSelectArray())
->rules([new Enum(AnimeWatchStatus::class)]),
Checkbox::make(ExternalEntryModel::ATTRIBUTE_IS_FAVORITE)
->label(__('filament.fields.external_entry.is_favorite.name'))
->helperText(__('filament.fields.external_entry.is_favorite.help'))
->rules(['boolean']),
])
->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(ExternalEntryModel::ATTRIBUTE_EXTERNAL_PROFILE)
->label(__('filament.resources.singularLabel.external_profile'))
->urlToRelated(ExternalProfileResource::class, ExternalEntryModel::RELATION_EXTERNAL_PROFILE, true)
->sortable()
->toggleable(),
TextColumn::make(ExternalEntryModel::ATTRIBUTE_ANIME)
->label(__('filament.resources.singularLabel.anime'))
->urlToRelated(Anime::class, ExternalEntryModel::RELATION_ANIME, true)
->sortable()
->toggleable(),
IconColumn::make(ExternalEntryModel::ATTRIBUTE_IS_FAVORITE)
->label(__('filament.fields.external_entry.is_favorite.name'))
->boolean()
->sortable()
->toggleable(),
TextColumn::make(ExternalEntryModel::ATTRIBUTE_SCORE)
->label(__('filament.fields.external_entry.score.name'))
->sortable()
->toggleable(),
TextColumn::make(ExternalEntryModel::ATTRIBUTE_WATCH_STATUS)
->label(__('filament.fields.external_entry.watch_status.name'))
->formatStateUsing(fn (AnimeWatchStatus $state) => $state->localize())
->sortable()
->toggleable(),
TextColumn::make(ExternalEntryModel::ATTRIBUTE_ID)
->label(__('filament.fields.base.id'))
->sortable()
->toggleable(),
]);
}
/**
* 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(ExternalEntryModel::ATTRIBUTE_EXTERNAL_PROFILE)
->label(__('filament.resources.singularLabel.external_profile'))
->urlToRelated(ExternalProfileResource::class, ExternalEntryModel::RELATION_EXTERNAL_PROFILE, true),
TextEntry::make(ExternalEntryModel::ATTRIBUTE_ANIME)
->label(__('filament.resources.singularLabel.anime'))
->urlToRelated(Anime::class, ExternalEntryModel::RELATION_ANIME, true),
IconEntry::make(ExternalEntryModel::ATTRIBUTE_IS_FAVORITE)
->label(__('filament.fields.external_entry.is_favorite.name')),
TextEntry::make(ExternalEntryModel::ATTRIBUTE_SCORE)
->label(__('filament.fields.external_entry.score.name')),
TextEntry::make(ExternalEntryModel::ATTRIBUTE_WATCH_STATUS)
->label(__('filament.fields.external_entry.watch_status.name'))
->formatStateUsing(fn (AnimeWatchStatus $state) => $state->localize()),
TextEntry::make(ExternalEntryModel::ATTRIBUTE_ID)
->label(__('filament.fields.base.id')),
])
->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 [
RelationGroup::make(
static::getLabel(),
array_merge(
[],
parent::getBaseRelations(),
)
),
];
}
/**
* 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 header actions available for the resource.
*
* @return array
*
* @noinspection PhpMissingParentCallCommonInspection
*/
public static function getHeaderActions(): array
{
return array_merge(
parent::getHeaderActions(),
[],
);
}
/**
* Get the pages available for the resource.
*
* @return array
*
* @noinspection PhpMissingParentCallCommonInspection
*/
public static function getPages(): array
{
return [
'index' => ListExternalEntries::route('/'),
'create' => CreateExternalEntry::route('/create'),
'view' => ViewExternalEntry::route('/{record:entry_id}'),
'edit' => EditExternalEntry::route('/{record:entry_id}/edit'),
];
}
}
@@ -0,0 +1,16 @@
<?php
declare(strict_types=1);
namespace App\Filament\Resources\List\External\ExternalEntry\Pages;
use App\Filament\Resources\Base\BaseCreateResource;
use App\Filament\Resources\List\External\ExternalEntry;
/**
* Class CreateExternalEntry.
*/
class CreateExternalEntry extends BaseCreateResource
{
protected static string $resource = ExternalEntry::class;
}
@@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
namespace App\Filament\Resources\List\External\ExternalEntry\Pages;
use App\Filament\Resources\List\External\ExternalEntry;
use App\Filament\Resources\Base\BaseEditResource;
/**
* Class EditExternalEntry.
*/
class EditExternalEntry extends BaseEditResource
{
protected static string $resource = ExternalEntry::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\List\External\ExternalEntry\Pages;
use App\Filament\Resources\Base\BaseListResources;
use App\Filament\Resources\List\External\ExternalEntry;
/**
* Class ListExternalEntries.
*/
class ListExternalEntries extends BaseListResources
{
protected static string $resource = ExternalEntry::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\List\External\ExternalEntry\Pages;
use App\Filament\Resources\Base\BaseViewResource;
use App\Filament\Resources\List\External\ExternalEntry;
/**
* Class ViewExternalEntry.
*/
class ViewExternalEntry extends BaseViewResource
{
protected static string $resource = ExternalEntry::class;
/**
* Get the header actions available.
*
* @return array
*
* @noinspection PhpMissingParentCallCommonInspection
*/
protected function getHeaderActions(): array
{
return array_merge(
parent::getHeaderActions(),
[],
);
}
}
@@ -0,0 +1,16 @@
<?php
declare(strict_types=1);
namespace App\Filament\Resources\List\External\Pages;
use App\Filament\Resources\Base\BaseCreateResource;
use App\Filament\Resources\List\ExternalProfile;
/**
* Class CreateExternalProfile.
*/
class CreateExternalProfile extends BaseCreateResource
{
protected static string $resource = ExternalProfile::class;
}
@@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
namespace App\Filament\Resources\List\External\Pages;
use App\Filament\Resources\List\ExternalProfile;
use App\Filament\Resources\Base\BaseEditResource;
/**
* Class EditExternalProfile.
*/
class EditExternalProfile extends BaseEditResource
{
protected static string $resource = ExternalProfile::class;
/**
* Get the header actions available.
*
* @return array
*
* @noinspection PhpMissingParentCallCommonInspection
*/
protected function getHeaderActions(): array
{
return array_merge(
parent::getHeaderActions(),
[],
);
}
}
@@ -0,0 +1,50 @@
<?php
declare(strict_types=1);
namespace App\Filament\Resources\List\External\Pages;
use App\Filament\Resources\Base\BaseListResources;
use App\Filament\Resources\List\ExternalProfile;
use App\Models\List\ExternalProfile as ExternalProfileModel;
use Illuminate\Database\Eloquent\Builder;
/**
* Class ListExternalProfiles.
*/
class ListExternalProfiles extends BaseListResources
{
protected static string $resource = ExternalProfile::class;
/**
* Get the header actions available.
*
* @return array
*
* @noinspection PhpMissingParentCallCommonInspection
*/
protected function getHeaderActions(): array
{
return array_merge(
parent::getHeaderActions(),
[],
);
}
/**
* Using Laravel Scout to search.
*
* @param Builder $query
* @return Builder
*/
protected function applySearchToTableQuery(Builder $query): Builder
{
$this->applyColumnSearchesToTableQuery($query);
if (filled($search = $this->getTableSearch())) {
$query->whereIn(ExternalProfileModel::ATTRIBUTE_ID, ExternalProfileModel::search($search)->take(25)->keys());
}
return $query;
}
}
@@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
namespace App\Filament\Resources\List\External\Pages;
use App\Filament\Resources\Base\BaseViewResource;
use App\Filament\Resources\List\ExternalProfile;
/**
* Class ViewExternalProfile.
*/
class ViewExternalProfile extends BaseViewResource
{
protected static string $resource = ExternalProfile::class;
/**
* Get the header actions available.
*
* @return array
*
* @noinspection PhpMissingParentCallCommonInspection
*/
protected function getHeaderActions(): array
{
return array_merge(
parent::getHeaderActions(),
[],
);
}
}
@@ -0,0 +1,99 @@
<?php
declare(strict_types=1);
namespace App\Filament\Resources\List\External\RelationManagers;
use App\Filament\RelationManagers\List\External\ExternalEntryRelationManager;
use App\Models\List\ExternalProfile;
use App\Models\List\External\ExternalEntry;
use Filament\Tables\Table;
/**
* Class ExternalEntryExternalProfileRelationManager.
*/
class ExternalEntryExternalProfileRelationManager extends ExternalEntryRelationManager
{
/**
* The relationship the relation manager corresponds to.
*
* @return string
*/
protected static string $relationship = ExternalProfile::RELATION_EXTERNAL_ENTRIES;
/**
* The index page of the resource.
*
* @param Table $table
* @return Table
*
* @noinspection PhpMissingParentCallCommonInspection
*/
public function table(Table $table): Table
{
return parent::table(
$table
->inverseRelationship(ExternalEntry::RELATION_EXTERNAL_PROFILE)
);
}
/**
* Get the filters available for the relation.
*
* @return array
*
* @noinspection PhpMissingParentCallCommonInspection
*/
public static function getFilters(): array
{
return array_merge(
[],
parent::getFilters(),
);
}
/**
* Get the actions available for the relation.
*
* @return array
*
* @noinspection PhpMissingParentCallCommonInspection
*/
public static function getActions(): array
{
return array_merge(
parent::getActions(),
[],
);
}
/**
* Get the bulk actions available for the relation.
*
* @return array
*
* @noinspection PhpMissingParentCallCommonInspection
*/
public static function getBulkActions(): array
{
return array_merge(
parent::getBulkActions(),
[],
);
}
/**
* Get the header actions available for the relation.
*
* @return array
*
* @noinspection PhpMissingParentCallCommonInspection
*/
public static function getHeaderActions(): array
{
return array_merge(
parent::getHeaderActions(),
[],
);
}
}
@@ -0,0 +1,346 @@
<?php
declare(strict_types=1);
namespace App\Filament\Resources\List;
use App\Enums\Http\Api\Filter\ComparisonOperator;
use App\Enums\Models\List\ExternalProfileSite;
use App\Enums\Models\List\ExternalProfileVisibility;
use App\Filament\Components\Columns\TextColumn;
use App\Filament\Components\Fields\Select;
use App\Filament\Components\Infolist\TextEntry;
use App\Filament\Resources\Auth\User;
use App\Filament\Resources\BaseResource;
use App\Filament\Resources\List\External\Pages\CreateExternalProfile;
use App\Filament\Resources\List\External\Pages\EditExternalProfile;
use App\Filament\Resources\List\External\Pages\ListExternalProfiles;
use App\Filament\Resources\List\External\Pages\ViewExternalProfile;
use App\Filament\Resources\List\External\RelationManagers\ExternalEntryExternalProfileRelationManager;
use App\Models\Auth\User as UserModel;
use App\Models\List\ExternalProfile as ExternalProfileModel;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Form;
use Filament\Infolists\Components\Section;
use Filament\Infolists\Infolist;
use Filament\Resources\RelationManagers\RelationGroup;
use Filament\Tables\Table;
use Illuminate\Validation\Rules\Enum;
/**
* Class ExternalProfile.
*/
class ExternalProfile extends BaseResource
{
/**
* The model the resource corresponds to.
*
* @var string|null
*/
protected static ?string $model = ExternalProfileModel::class;
/**
* Get the displayable singular label of the resource.
*
* @return string
*
* @noinspection PhpMissingParentCallCommonInspection
*/
public static function getLabel(): string
{
return __('filament.resources.singularLabel.external_profile');
}
/**
* Get the displayable label of the resource.
*
* @return string
*
* @noinspection PhpMissingParentCallCommonInspection
*/
public static function getPluralLabel(): string
{
return __('filament.resources.label.external_profiles');
}
/**
* The logical group associated with the resource.
*
* @return string
*
* @noinspection PhpMissingParentCallCommonInspection
*/
public static function getNavigationGroup(): string
{
return __('filament.resources.group.list');
}
/**
* The icon displayed to the resource.
*
* @return string
*
* @noinspection PhpMissingParentCallCommonInspection
*/
public static function getNavigationIcon(): string
{
return __('filament.resources.icon.external_profiles');
}
/**
* Get the slug (URI key) for the resource.
*
* @return string
*
* @noinspection PhpMissingParentCallCommonInspection
*/
public static function getRecordSlug(): string
{
return 'external-profiles';
}
/**
* Get the route key for the resource.
*
* @return string
*
* @noinspection PhpMissingParentCallCommonInspection
*/
public static function getRecordRouteKeyName(): string
{
return ExternalProfileModel::ATTRIBUTE_ID;
}
/**
* The form to the actions.
*
* @param Form $form
* @return Form
*
* @noinspection PhpMissingParentCallCommonInspection
*/
public static function form(Form $form): Form
{
return $form
->schema([
Select::make(ExternalProfileModel::ATTRIBUTE_USER)
->label(__('filament.resources.singularLabel.user'))
->relationship(ExternalProfileModel::RELATION_USER, UserModel::ATTRIBUTE_NAME)
->allowHtml()
->searchable()
->getSearchResultsUsing(function (string $search) {
return UserModel::query()
->where(UserModel::ATTRIBUTE_NAME, ComparisonOperator::LIKE->value, "%$search%")
->get()
->mapWithKeys(fn (UserModel $model) => [$model->getKey() => Select::getSearchLabelWithBlade($model)])
->toArray();
}),
TextInput::make(ExternalProfileModel::ATTRIBUTE_NAME)
->label(__('filament.fields.external_profile.name.name'))
->helperText(__('filament.fields.external_profile.name.help'))
->required()
->maxLength(192)
->rules(['required', 'max:192']),
Select::make(ExternalProfileModel::ATTRIBUTE_SITE)
->label(__('filament.fields.external_profile.site.name'))
->helperText(__('filament.fields.external_profile.site.help'))
->options(ExternalProfileSite::asSelectArray())
->required()
->rules(['required', new Enum(ExternalProfileSite::class)]),
Select::make(ExternalProfileModel::ATTRIBUTE_VISIBILITY)
->label(__('filament.fields.external_profile.visibility.name'))
->helperText(__('filament.fields.external_profile.visibility.help'))
->options(ExternalProfileVisibility::asSelectArray())
->required()
->rules(['required', new Enum(ExternalProfileVisibility::class)]),
])
->columns(2);
}
/**
* 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(ExternalProfileModel::ATTRIBUTE_USER)
->label(__('filament.resources.singularLabel.user'))
->urlToRelated(User::class, ExternalProfileModel::RELATION_USER, true)
->sortable()
->toggleable()
->placeholder('-'),
TextColumn::make(ExternalProfileModel::ATTRIBUTE_NAME)
->label(__('filament.fields.external_profile.name.name'))
->sortable()
->toggleable(),
TextColumn::make(ExternalProfileModel::ATTRIBUTE_SITE)
->label(__('filament.fields.external_profile.site.name'))
->formatStateUsing(fn (ExternalProfileSite $state) => $state->localize())
->sortable()
->toggleable(),
TextColumn::make(ExternalProfileModel::ATTRIBUTE_VISIBILITY)
->label(__('filament.fields.external_profile.visibility.name'))
->formatStateUsing(fn (ExternalProfileVisibility $state) => $state->localize())
->sortable()
->toggleable(),
TextColumn::make(ExternalProfileModel::ATTRIBUTE_ID)
->label(__('filament.fields.base.id'))
->sortable()
->toggleable(),
])
->searchable();
}
/**
* 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(ExternalProfileModel::ATTRIBUTE_USER)
->label(__('filament.resources.singularLabel.user'))
->urlToRelated(User::class, ExternalProfileModel::RELATION_USER, true)
->placeholder('-'),
TextEntry::make(ExternalProfileModel::ATTRIBUTE_NAME)
->label(__('filament.fields.external_profile.name.name')),
TextEntry::make(ExternalProfileModel::ATTRIBUTE_SITE)
->label(__('filament.fields.external_profile.site.name'))
->formatStateUsing(fn (ExternalProfileSite $state) => $state->localize()),
TextEntry::make(ExternalProfileModel::ATTRIBUTE_VISIBILITY)
->label(__('filament.fields.external_profile.visibility.name'))
->formatStateUsing(fn (ExternalProfileVisibility $state) => $state->localize()),
TextEntry::make(ExternalProfileModel::ATTRIBUTE_ID)
->label(__('filament.fields.base.id')),
])
->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 [
RelationGroup::make(
static::getLabel(),
array_merge(
[
ExternalEntryExternalProfileRelationManager::class,
],
parent::getBaseRelations(),
)
),
];
}
/**
* 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 header actions available for the resource.
*
* @return array
*
* @noinspection PhpMissingParentCallCommonInspection
*/
public static function getHeaderActions(): array
{
return array_merge(
parent::getHeaderActions(),
[],
);
}
/**
* Get the pages available for the resource.
*
* @return array
*
* @noinspection PhpMissingParentCallCommonInspection
*/
public static function getPages(): array
{
return [
'index' => ListExternalProfiles::route('/'),
'create' => CreateExternalProfile::route('/create'),
'view' => ViewExternalProfile::route('/{record:profile_id}'),
'edit' => EditExternalProfile::route('/{record:profile_id}/edit'),
];
}
}
+10 -1
View File
@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace App\Filament\Resources\List;
use App\Enums\Http\Api\Filter\ComparisonOperator;
use App\Enums\Models\List\PlaylistVisibility;
use App\Filament\Actions\Models\AssignHashidsAction;
use App\Filament\Components\Columns\TextColumn;
@@ -129,7 +130,15 @@ class Playlist extends BaseResource
Select::make(PlaylistModel::ATTRIBUTE_USER)
->label(__('filament.resources.singularLabel.user'))
->relationship(PlaylistModel::RELATION_USER, UserModel::ATTRIBUTE_NAME)
->searchable(),
->allowHtml()
->searchable()
->getSearchResultsUsing(function (string $search) {
return UserModel::query()
->where(UserModel::ATTRIBUTE_NAME, ComparisonOperator::LIKE->value, "%$search%")
->get()
->mapWithKeys(fn (UserModel $model) => [$model->getKey() => Select::getSearchLabelWithBlade($model)])
->toArray();
}),
TextInput::make(PlaylistModel::ATTRIBUTE_NAME)
->label(__('filament.fields.playlist.name.name'))
+1 -1
View File
@@ -381,7 +381,7 @@ class Anime extends BaseResource
return array_merge(
[
NumberFilter::make(AnimeModel::ATTRIBUTE_YEAR)
->labels(__('filament.filters.anime.year_from'), __('filament.filters.anime.year_to')),
->label(__('filament.fields.anime.year.name')),
SelectFilter::make(AnimeModel::ATTRIBUTE_SEASON)
->label(__('filament.fields.anime.season.name'))
+1 -1
View File
@@ -502,7 +502,7 @@ class Theme extends BaseResource
->options(ThemeType::asSelectArray()),
NumberFilter::make(ThemeModel::ATTRIBUTE_SEQUENCE)
->labels(__('filament.filters.anime_theme.sequence_from'), __('filament.filters.anime_theme.sequence_to')),
->label(__('filament.fields.anime_theme.sequence.name')),
],
parent::getFilters(),
);
@@ -364,7 +364,7 @@ class Entry extends BaseResource
return array_merge(
[
NumberFilter::make(EntryModel::ATTRIBUTE_VERSION)
->labels(__('filament.filters.anime_theme_entry.version_from'), __('filament.filters.anime_theme_entry.version_to')),
->label(__('filament.fields.anime_theme_entry.version.name')),
TextFilter::make(EntryModel::ATTRIBUTE_EPISODES)
->label(__('filament.fields.anime_theme_entry.episodes.name')),
+1 -1
View File
@@ -241,7 +241,7 @@ class Audio extends BaseResource
return array_merge(
[
NumberFilter::make(AudioModel::ATTRIBUTE_SIZE)
->labels(__('filament.filters.audio.size_from'), __('filament.filters.audio.size_to')),
->label(__('filament.fields.audio.size.name')),
],
parent::getFilters(),
);
@@ -276,7 +276,7 @@ class ExternalResource extends BaseResource
->options(ResourceSite::asSelectArray()),
NumberFilter::make(ExternalResourceModel::ATTRIBUTE_EXTERNAL_ID)
->labels(__('filament.filters.external_resource.external_id_from'), __('filament.filters.external_resource.external_id_to')),
->label(__('filament.fields.external_resource.external_id.name')),
],
parent::getFilters(),
);
+2 -2
View File
@@ -326,7 +326,7 @@ class Video extends BaseResource
return array_merge(
[
NumberFilter::make(VideoModel::ATTRIBUTE_RESOLUTION)
->labels(__('filament.filters.video.resolution_from'), __('filament.filters.video.resolution_to')),
->label(__('filament.fields.video.resolution.name')),
Filter::make(VideoModel::ATTRIBUTE_NC)
->label(__('filament.fields.video.nc.name'))
@@ -353,7 +353,7 @@ class Video extends BaseResource
->options(VideoSource::asSelectArray()),
NumberFilter::make(VideoModel::ATTRIBUTE_SIZE)
->labels(__('filament.filters.video.size_from'), __('filament.filters.video.size_to')),
->label(__('filament.fields.video.size.name')),
],
parent::getFilters(),
);
@@ -0,0 +1,78 @@
<?php
declare(strict_types=1);
namespace App\Filament\Widgets\List;
use App\Filament\Widgets\BaseChartWidget;
use App\Models\List\ExternalProfile;
use Flowframe\Trend\TrendValue;
/**
* Class ExternalProfileChart.
*/
class ExternalProfileChart extends BaseChartWidget
{
/**
* Chart Id.
*
* @var string
*/
protected static string $chartId = 'externalProfileChart';
/**
* Get the displayed label of the widget.
*
* @return string
*/
protected function getHeading(): string
{
return __('filament.resources.label.external_profiles');
}
/**
* Chart options (series, labels, types, size, animations...)
* https://apexcharts.com/docs/options
*
* @return array
*/
protected function getOptions(): array
{
$data = $this->perMonth(ExternalProfile::class);
return [
'chart' => [
'type' => 'area',
'height' => 300,
],
'series' => [
[
'name' => $this->getHeading(),
'data' => $data->map(fn (TrendValue $value) => $value->aggregate),
],
],
'xaxis' => [
'categories' => $data->map(fn (TrendValue $value) => $this->translateDate($value->date)),
'labels' => [
'style' => [
'fontFamily' => 'inherit',
],
],
],
'yaxis' => [
'labels' => [
'style' => [
'fontFamily' => 'inherit',
],
],
],
'colors' => ['#71E9C5'],
'stroke' => [
'curve' => 'smooth',
],
'dataLabels' => [
'enabled' => false,
],
];
}
}
@@ -0,0 +1,53 @@
<?php
declare(strict_types=1);
namespace App\Http\Api\Field\List\ExternalProfile\ExternalEntry;
use App\Contracts\Http\Api\Field\CreatableField;
use App\Contracts\Http\Api\Field\SelectableField;
use App\Http\Api\Field\Field;
use App\Http\Api\Query\Query;
use App\Http\Api\Schema\Schema;
use App\Models\List\External\ExternalEntry;
use App\Models\Wiki\Anime;
use Illuminate\Http\Request;
use Illuminate\Validation\Rule;
/**
* Class ExternalEntryAnimeIdField.
*/
class ExternalEntryAnimeIdField extends Field implements CreatableField, SelectableField
{
/**
* Create a new field instance.
*
* @param Schema $schema
*/
public function __construct(Schema $schema)
{
parent::__construct($schema, ExternalEntry::ATTRIBUTE_ANIME);
}
/**
* Determine if the field should be included in the select clause of our query.
*
* @param Query $query
* @param Schema $schema
* @return bool
*/
public function shouldSelect(Query $query, Schema $schema): bool
{
// Needed to match playlist relation.
return true;
}
public function getCreationRules(Request $request): array
{
return [
'required',
'integer',
Rule::exists(Anime::class, Anime::ATTRIBUTE_ID),
];
}
}
@@ -0,0 +1,40 @@
<?php
declare(strict_types=1);
namespace App\Http\Api\Field\List\ExternalProfile\ExternalEntry;
use App\Contracts\Http\Api\Field\SelectableField;
use App\Http\Api\Field\Field;
use App\Http\Api\Query\Query;
use App\Http\Api\Schema\Schema;
use App\Models\List\External\ExternalEntry;
/**
* Class ExternalEntryExternalProfileIdField.
*/
class ExternalEntryExternalProfileIdField extends Field implements SelectableField
{
/**
* Create a new field instance.
*
* @param Schema $schema
*/
public function __construct(Schema $schema)
{
parent::__construct($schema, ExternalEntry::ATTRIBUTE_EXTERNAL_PROFILE);
}
/**
* Determine if the field should be included in the select clause of our query.
*
* @param Query $query
* @param Schema $schema
* @return bool
*/
public function shouldSelect(Query $query, Schema $schema): bool
{
// Needed to match playlist relation.
return true;
}
}
@@ -0,0 +1,50 @@
<?php
declare(strict_types=1);
namespace App\Http\Api\Field\List\ExternalProfile\ExternalEntry;
use App\Contracts\Http\Api\Field\SelectableField;
use App\Http\Api\Field\Field;
use App\Http\Api\Query\Query;
use App\Http\Api\Schema\Schema;
use App\Models\List\External\ExternalEntry;
/**
* Class ExternalEntryField.
*/
class ExternalEntryIdField extends Field implements SelectableField
{
/**
* Create a new field instance.
*
* @param Schema $schema
*/
public function __construct(Schema $schema)
{
parent::__construct($schema, ExternalEntry::ATTRIBUTE_ID);
}
/**
* Determine if the field should be included in the select clause of our query.
*
* @param Query $query
* @param Schema $schema
* @return bool
*/
public function shouldSelect(Query $query, Schema $schema): bool
{
// We can only exclude ID fields for top-level models that are not including related resources.
$includeCriteria = $query->getIncludeCriteria($this->schema->type());
if (
$this->schema->type() === $schema->type()
&& ($includeCriteria === null || $includeCriteria->getPaths()->isEmpty())
) {
$criteria = $query->getFieldCriteria($this->schema->type());
return $criteria === null || $criteria->isAllowedField($this->getKey());
}
return true;
}
}
@@ -0,0 +1,57 @@
<?php
declare(strict_types=1);
namespace App\Http\Api\Field\List\ExternalProfile\ExternalEntry;
use App\Contracts\Http\Api\Field\CreatableField;
use App\Contracts\Http\Api\Field\UpdatableField;
use App\Http\Api\Field\BooleanField;
use App\Http\Api\Schema\Schema;
use App\Models\List\External\ExternalEntry;
use Illuminate\Http\Request;
/**
* Class ExternalEntryIsFavoriteField.
*/
class ExternalEntryIsFavoriteField extends BooleanField implements CreatableField, UpdatableField
{
/**
* Create a new field instance.
*
* @param Schema $schema
*/
public function __construct(Schema $schema)
{
parent::__construct($schema, ExternalEntry::ATTRIBUTE_IS_FAVORITE);
}
/**
* Set the creation validation rules for the field.
*
* @param Request $request
* @return array
*/
public function getCreationRules(Request $request): array
{
return [
'required',
'boolean',
];
}
/**
* Set the update validation rules for the field.
*
* @param Request $request
* @return array
*/
public function getUpdateRules(Request $request): array
{
return [
'sometimes',
'required',
'boolean',
];
}
}
@@ -0,0 +1,57 @@
<?php
declare(strict_types=1);
namespace App\Http\Api\Field\List\ExternalProfile\ExternalEntry;
use App\Contracts\Http\Api\Field\CreatableField;
use App\Contracts\Http\Api\Field\UpdatableField;
use App\Http\Api\Field\FloatField;
use App\Http\Api\Schema\Schema;
use App\Models\List\External\ExternalEntry;
use App\Rules\ModerationRule;
use Illuminate\Http\Request;
/**
* Class ExternalEntryScoreField.
*/
class ExternalEntryScoreField extends FloatField implements CreatableField, UpdatableField
{
/**
* Create a new field instance.
*
* @param Schema $schema
*/
public function __construct(Schema $schema)
{
parent::__construct($schema, ExternalEntry::ATTRIBUTE_SCORE);
}
/**
* Set the creation validation rules for the field.
*
* @param Request $request
* @return array
*/
public function getCreationRules(Request $request): array
{
return [
'required',
'decimal:0,2',
];
}
/**
* Set the update validation rules for the field.
*
* @param Request $request
* @return array
*/
public function getUpdateRules(Request $request): array
{
return [
'sometimes',
'decimal:0,2',
];
}
}
@@ -0,0 +1,52 @@
<?php
declare(strict_types=1);
namespace App\Http\Api\Field\List\ExternalProfile\ExternalEntry;
use App\Contracts\Http\Api\Field\CreatableField;
use App\Contracts\Http\Api\Field\UpdatableField;
use App\Enums\Models\List\AnimeWatchStatus;
use App\Http\Api\Field\EnumField;
use App\Http\Api\Schema\Schema;
use App\Models\List\External\ExternalEntry;
use Illuminate\Http\Request;
use Illuminate\Validation\Rules\Enum;
/**
* Class ExternalEntryWatchStatusField.
*/
class ExternalEntryWatchStatusField extends EnumField implements CreatableField, UpdatableField
{
/**
* Create a new field instance.
*
* @param Schema $schema
*/
public function __construct(Schema $schema)
{
parent::__construct($schema, ExternalEntry::ATTRIBUTE_WATCH_STATUS, AnimeWatchStatus::class);
}
/**
* Set the creation validation rules for the field.
*
* @param Request $request
* @return array
*/
public function getCreationRules(Request $request): array
{
return [
'required',
new Enum(AnimeWatchStatus::class),
];
}
public function getUpdateRules(Request $request): array
{
return [
'sometimes',
new Enum(AnimeWatchStatus::class),
];
}
}
@@ -0,0 +1,50 @@
<?php
declare(strict_types=1);
namespace App\Http\Api\Field\List\ExternalProfile;
use App\Contracts\Http\Api\Field\SelectableField;
use App\Http\Api\Field\Field;
use App\Http\Api\Query\Query;
use App\Http\Api\Schema\Schema;
use App\Models\List\ExternalProfile;
/**
* Class ExternalProfileIdField.
*/
class ExternalProfileIdField extends Field implements SelectableField
{
/**
* Create a new field instance.
*
* @param Schema $schema
*/
public function __construct(Schema $schema)
{
parent::__construct($schema, ExternalProfile::ATTRIBUTE_ID);
}
/**
* Determine if the field should be included in the select clause of our query.
*
* @param Query $query
* @param Schema $schema
* @return bool
*/
public function shouldSelect(Query $query, Schema $schema): bool
{
// We can only exclude ID fields for top-level models that are not including related resources.
$includeCriteria = $query->getIncludeCriteria($this->schema->type());
if (
$this->schema->type() === $schema->type()
&& ($includeCriteria === null || $includeCriteria->getPaths()->isEmpty())
) {
$criteria = $query->getFieldCriteria($this->schema->type());
return $criteria === null || $criteria->isAllowedField($this->getKey());
}
return true;
}
}
@@ -0,0 +1,62 @@
<?php
declare(strict_types=1);
namespace App\Http\Api\Field\List\ExternalProfile;
use App\Contracts\Http\Api\Field\CreatableField;
use App\Contracts\Http\Api\Field\UpdatableField;
use App\Http\Api\Field\StringField;
use App\Http\Api\Schema\Schema;
use App\Models\List\ExternalProfile;
use App\Rules\ModerationRule;
use Illuminate\Http\Request;
/**
* Class ExternalProfileNameField.
*/
class ExternalProfileNameField extends StringField implements CreatableField, UpdatableField
{
/**
* Create a new field instance.
*
* @param Schema $schema
*/
public function __construct(Schema $schema)
{
parent::__construct($schema, ExternalProfile::ATTRIBUTE_NAME);
}
/**
* Set the creation validation rules for the field.
*
* @param Request $request
* @return array
*/
public function getCreationRules(Request $request): array
{
return [
'required',
'string',
'max:192',
new ModerationRule(),
];
}
/**
* Set the update validation rules for the field.
*
* @param Request $request
* @return array
*/
public function getUpdateRules(Request $request): array
{
return [
'sometimes',
'required',
'string',
'max:192',
new ModerationRule(),
];
}
}
@@ -0,0 +1,43 @@
<?php
declare(strict_types=1);
namespace App\Http\Api\Field\List\ExternalProfile;
use App\Contracts\Http\Api\Field\CreatableField;
use App\Enums\Models\List\ExternalProfileSite;
use App\Http\Api\Field\EnumField;
use App\Http\Api\Schema\Schema;
use App\Models\List\ExternalProfile;
use Illuminate\Http\Request;
use Illuminate\Validation\Rules\Enum;
/**
* Class ExternalProfileSiteField.
*/
class ExternalProfileSiteField extends EnumField implements CreatableField
{
/**
* Create a new field instance.
*
* @param Schema $schema
*/
public function __construct(Schema $schema)
{
parent::__construct($schema, ExternalProfile::ATTRIBUTE_SITE, ExternalProfileSite::class);
}
/**
* Set the creation validation rules for the field.
*
* @param Request $request
* @return array
*/
public function getCreationRules(Request $request): array
{
return [
'required',
new Enum(ExternalProfileSite::class),
];
}
}
@@ -0,0 +1,40 @@
<?php
declare(strict_types=1);
namespace App\Http\Api\Field\List\ExternalProfile;
use App\Contracts\Http\Api\Field\SelectableField;
use App\Http\Api\Field\Field;
use App\Http\Api\Query\Query;
use App\Http\Api\Schema\Schema;
use App\Models\List\ExternalProfile;
/**
* Class ExternalProfileUserIdField.
*/
class ExternalProfileUserIdField extends Field implements SelectableField
{
/**
* Create a new field instance.
*
* @param Schema $schema
*/
public function __construct(Schema $schema)
{
parent::__construct($schema, ExternalProfile::ATTRIBUTE_USER);
}
/**
* Determine if the field should be included in the select clause of our query.
*
* @param Query $query
* @param Schema $schema
* @return bool
*/
public function shouldSelect(Query $query, Schema $schema): bool
{
// Needed to match user relation.
return true;
}
}
@@ -0,0 +1,59 @@
<?php
declare(strict_types=1);
namespace App\Http\Api\Field\List\ExternalProfile;
use App\Contracts\Http\Api\Field\CreatableField;
use App\Contracts\Http\Api\Field\UpdatableField;
use App\Enums\Models\List\ExternalProfileVisibility;
use App\Http\Api\Field\EnumField;
use App\Http\Api\Schema\Schema;
use App\Models\List\ExternalProfile;
use Illuminate\Http\Request;
use Illuminate\Validation\Rules\Enum;
/**
* Class ExternalProfileVisibilityField.
*/
class ExternalProfileVisibilityField extends EnumField implements CreatableField, UpdatableField
{
/**
* Create a new field instance.
*
* @param Schema $schema
*/
public function __construct(Schema $schema)
{
parent::__construct($schema, ExternalProfile::ATTRIBUTE_VISIBILITY, ExternalProfileVisibility::class);
}
/**
* Set the creation validation rules for the field.
*
* @param Request $request
* @return array
*/
public function getCreationRules(Request $request): array
{
return [
'required',
new Enum(ExternalProfileVisibility::class),
];
}
/**
* Set the update validation rules for the field.
*
* @param Request $request
* @return array
*/
public function getUpdateRules(Request $request): array
{
return [
'sometimes',
'required',
new Enum(ExternalProfileVisibility::class),
];
}
}
@@ -14,6 +14,7 @@ use App\Http\Api\Include\AllowedInclude;
use App\Http\Api\Schema\Auth\PermissionSchema;
use App\Http\Api\Schema\Auth\RoleSchema;
use App\Http\Api\Schema\EloquentSchema;
use App\Http\Api\Schema\List\ExternalProfileSchema;
use App\Http\Api\Schema\List\PlaylistSchema;
use App\Http\Resources\Auth\User\Resource\MyResource;
use App\Models\Auth\User;
@@ -41,6 +42,7 @@ class MySchema extends EloquentSchema
public function allowedIncludes(): array
{
return [
new AllowedInclude(new ExternalProfileSchema(), User::RELATION_EXTERNAL_PROFILES),
new AllowedInclude(new PermissionSchema(), User::RELATION_PERMISSIONS),
new AllowedInclude(new PlaylistSchema(), User::RELATION_PLAYLISTS),
new AllowedInclude(new RoleSchema(), User::RELATION_ROLES),
@@ -0,0 +1,70 @@
<?php
declare(strict_types=1);
namespace App\Http\Api\Schema\List\External;
use App\Http\Api\Field\Field;
use App\Http\Api\Field\List\ExternalProfile\ExternalEntry\ExternalEntryAnimeIdField;
use App\Http\Api\Field\List\ExternalProfile\ExternalEntry\ExternalEntryExternalProfileIdField;
use App\Http\Api\Field\List\ExternalProfile\ExternalEntry\ExternalEntryWatchStatusField;
use App\Http\Api\Field\List\ExternalProfile\ExternalEntry\ExternalEntryIdField;
use App\Http\Api\Field\List\ExternalProfile\ExternalEntry\ExternalEntryIsFavoriteField;
use App\Http\Api\Field\List\ExternalProfile\ExternalEntry\ExternalEntryScoreField;
use App\Http\Api\Include\AllowedInclude;
use App\Http\Api\Schema\Auth\UserSchema;
use App\Http\Api\Schema\EloquentSchema;
use App\Http\Api\Schema\List\ExternalProfileSchema;
use App\Http\Api\Schema\Wiki\AnimeSchema;
use App\Http\Resources\List\External\Resource\ExternalEntryResource;
use App\Models\List\External\ExternalEntry;
/**
* Class ExternalEntrySchema.
*/
class ExternalEntrySchema extends EloquentSchema
{
/**
* Get the type of the resource.
*
* @return string
*/
public function type(): string
{
return ExternalEntryResource::$wrap;
}
/**
* Get the allowed includes.
*
* @return AllowedInclude[]
*/
public function allowedIncludes(): array
{
return [
new AllowedInclude(new AnimeSchema(), ExternalEntry::RELATION_ANIME),
new AllowedInclude(new ExternalProfileSchema(), ExternalEntry::RELATION_EXTERNAL_PROFILE),
new AllowedInclude(new UserSchema(), ExternalEntry::RELATION_USER),
];
}
/**
* Get the direct fields of the resource.
*
* @return Field[]
*/
public function fields(): array
{
return array_merge(
parent::fields(),
[
new ExternalEntryIdField($this),
new ExternalEntryScoreField($this),
new ExternalEntryIsFavoriteField($this),
new ExternalEntryWatchStatusField($this),
new ExternalEntryAnimeIdField($this),
new ExternalEntryExternalProfileIdField($this),
],
);
}
}
@@ -0,0 +1,69 @@
<?php
declare(strict_types=1);
namespace App\Http\Api\Schema\List;
use App\Contracts\Http\Api\Schema\SearchableSchema;
use App\Http\Api\Field\Field;
use App\Http\Api\Field\List\ExternalProfile\ExternalProfileNameField;
use App\Http\Api\Field\List\ExternalProfile\ExternalProfileIdField;
use App\Http\Api\Field\List\ExternalProfile\ExternalProfileVisibilityField;
use App\Http\Api\Field\List\ExternalProfile\ExternalProfileSiteField;
use App\Http\Api\Field\List\ExternalProfile\ExternalProfileUserIdField;
use App\Http\Api\Include\AllowedInclude;
use App\Http\Api\Schema\Auth\UserSchema;
use App\Http\Api\Schema\EloquentSchema;
use App\Http\Api\Schema\List\External\ExternalEntrySchema;
use App\Http\Api\Schema\Wiki\AnimeSchema;
use App\Http\Resources\List\Resource\ExternalProfileResource;
use App\Models\List\ExternalProfile;
/**
* Class ExternalProfileSchema.
*/
class ExternalProfileSchema extends EloquentSchema implements SearchableSchema
{
/**
* Get the type of the resource.
*
* @return string
*/
public function type(): string
{
return ExternalProfileResource::$wrap;
}
/**
* Get the allowed includes.
*
* @return AllowedInclude[]
*/
public function allowedIncludes(): array
{
return [
new AllowedInclude(new AnimeSchema(), ExternalProfile::RELATION_ANIMES),
new AllowedInclude(new ExternalEntrySchema(), ExternalProfile::RELATION_EXTERNAL_ENTRIES),
new AllowedInclude(new UserSchema(), ExternalProfile::RELATION_USER),
];
}
/**
* Get the direct fields of the resource.
*
* @return Field[]
*/
public function fields(): array
{
return array_merge(
parent::fields(),
[
new ExternalProfileIdField($this),
new ExternalProfileNameField($this),
new ExternalProfileSiteField($this),
new ExternalProfileVisibilityField($this),
new ExternalProfileUserIdField($this),
],
);
}
}
@@ -0,0 +1,65 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers\Api\Auth\User\Me\List;
use App\Actions\Http\Api\IndexAction;
use App\Http\Api\Query\Query;
use App\Http\Api\Schema\List\ExternalProfileSchema;
use App\Http\Api\Schema\Schema;
use App\Http\Controllers\Api\BaseController;
use App\Http\Middleware\Auth\Authenticate;
use App\Http\Requests\Api\IndexRequest;
use App\Http\Resources\List\Collection\ExternalProfileCollection;
use App\Models\Auth\User;
use App\Models\List\ExternalProfile;
use Illuminate\Support\Facades\Auth;
/**
* Class MyExternalProfileController.
*/
class MyExternalProfileController extends BaseController
{
/**
* Create a new controller instance.
*/
public function __construct()
{
$this->middleware(Authenticate::using('sanctum'));
parent::__construct(ExternalProfile::class, 'externalprofile');
}
/**
* Display a listing of the resource.
*
* @param IndexRequest $request
* @param IndexAction $action
* @return ExternalProfileCollection
*/
public function index(IndexRequest $request, IndexAction $action): ExternalProfileCollection
{
$query = new Query($request->validated());
/** @var User $user */
$user = Auth::user();
$builder = $user->externalprofiles()->getQuery();
$profiles = $action->index($builder, $query, $request->schema());
return new ExternalProfileCollection($profiles, $query);
}
/**
* Get the underlying schema.
*
* @return Schema
*
* @noinspection PhpMissingParentCallCommonInspection
*/
public function schema(): Schema
{
return new ExternalProfileSchema();
}
}
@@ -0,0 +1,172 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers\Api\List\External;
use App\Actions\Http\Api\DestroyAction;
use App\Actions\Http\Api\ForceDeleteAction;
use App\Actions\Http\Api\IndexAction;
use App\Actions\Http\Api\RestoreAction;
use App\Actions\Http\Api\ShowAction;
use App\Actions\Http\Api\StoreAction;
use App\Actions\Http\Api\UpdateAction;
use App\Features\AllowExternalProfileManagement;
use App\Http\Api\Query\Query;
use App\Http\Controllers\Api\BaseController;
use App\Http\Middleware\Api\EnabledOnlyOnLocalhost;
use App\Http\Middleware\Models\List\ExternalProfileExceedsEntryLimit;
use App\Http\Requests\Api\IndexRequest;
use App\Http\Requests\Api\ShowRequest;
use App\Http\Requests\Api\StoreRequest;
use App\Http\Requests\Api\UpdateRequest;
use App\Http\Resources\List\External\Collection\ExternalEntryCollection;
use App\Http\Resources\List\External\Resource\ExternalEntryResource;
use App\Models\List\External\ExternalEntry;
use App\Models\List\ExternalProfile;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Str;
use Laravel\Pennant\Middleware\EnsureFeaturesAreActive;
/**
* Class ExternalEntryController.
*/
class ExternalEntryController extends BaseController
{
/**
* Create a new controller instance.
*/
public function __construct()
{
parent::__construct(ExternalEntry::class, 'externalentry,externalprofile');
$isExternalProfileManagementAllowed = Str::of(EnsureFeaturesAreActive::class)
->append(':')
->append(AllowExternalProfileManagement::class)
->__toString();
$this->middleware(EnabledOnlyOnLocalhost::class);
$this->middleware($isExternalProfileManagementAllowed)->except(['index', 'show']);
$this->middleware(ExternalProfileExceedsEntryLimit::class)->only(['store', 'restore']);
}
/**
* Display a listing of the resource.
*
* @param IndexRequest $request
* @param ExternalProfile $externalprofile
* @param IndexAction $action
* @return ExternalEntryCollection
*/
public function index(IndexRequest $request, ExternalProfile $externalprofile, IndexAction $action): ExternalEntryCollection
{
$query = new Query($request->validated());
$builder = ExternalEntry::query()->where(ExternalEntry::ATTRIBUTE_EXTERNAL_PROFILE, $externalprofile->getKey());
$resources = $action->index($builder, $query, $request->schema());
return new ExternalEntryCollection($resources, $query);
}
/**
* Store a newly created resource.
*
* @param StoreRequest $request
* @param ExternalProfile $externalprofile
* @param StoreAction $action
* @return ExternalEntryResource
*/
public function store(StoreRequest $request, ExternalProfile $externalprofile, StoreAction $action): ExternalEntryResource
{
$validated = array_merge(
$request->validated(),
[ExternalEntry::ATTRIBUTE_EXTERNAL_PROFILE => $externalprofile->getKey()]
);
$externalentry = $action->store(ExternalEntry::query(), $validated);
return new ExternalEntryResource($externalentry, new Query());
}
/**
* Display the specified resource.
*
* @param ShowRequest $request
* @param ExternalProfile $externalprofile
* @param ExternalEntry $externalentry
* @param ShowAction $action
* @return ExternalEntryResource
*/
public function show(ShowRequest $request, ExternalProfile $externalprofile, ExternalEntry $externalentry, ShowAction $action): ExternalEntryResource
{
$query = new Query($request->validated());
$show = $action->show($externalentry, $query, $request->schema());
return new ExternalEntryResource($show, $query);
}
/**
* Update the specified resource.
*
* @param UpdateRequest $request
* @param ExternalProfile $externalprofile
* @param ExternalEntry $externalentry
* @param UpdateAction $action
* @return ExternalEntryResource
*/
public function update(UpdateRequest $request, ExternalProfile $externalprofile, ExternalEntry $externalentry, UpdateAction $action): ExternalEntryResource
{
$updated = $action->update($externalentry, $request->validated());
return new ExternalEntryResource($updated, new Query());
}
/**
* Remove the specified resource.
*
* @param ExternalProfile $externalprofile
* @param ExternalEntry $externalentry
* @param DestroyAction $action
* @return ExternalEntryResource
*/
public function destroy(ExternalProfile $externalprofile, ExternalEntry $externalentry, DestroyAction $action): ExternalEntryResource
{
$deleted = $action->destroy($externalentry);
return new ExternalEntryResource($deleted, new Query());
}
/**
* Restore the specified resource.
*
* @param ExternalProfile $externalprofile
* @param ExternalEntry $externalentry
* @param RestoreAction $action
* @return ExternalEntryResource
*/
public function restore(ExternalProfile $externalprofile, ExternalEntry $externalentry, RestoreAction $action): ExternalEntryResource
{
$restored = $action->restore($externalentry);
return new ExternalEntryResource($restored, new Query());
}
/**
* Hard-delete the specified resource.
*
* @param ExternalProfile $externalprofile
* @param ExternalEntry $externalentry
* @param ForceDeleteAction $action
* @return JsonResponse
*/
public function forceDelete(ExternalProfile $externalprofile, ExternalEntry $externalentry, ForceDeleteAction $action): JsonResponse
{
$message = $action->forceDelete($externalentry);
return new JsonResponse([
'message' => $message,
]);
}
}
@@ -0,0 +1,168 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers\Api\List;
use App\Actions\Http\Api\DestroyAction;
use App\Actions\Http\Api\ForceDeleteAction;
use App\Actions\Http\Api\IndexAction;
use App\Actions\Http\Api\RestoreAction;
use App\Actions\Http\Api\ShowAction;
use App\Actions\Http\Api\UpdateAction;
use App\Actions\Models\List\ExternalProfile\StoreExternalProfileAction;
use App\Enums\Models\List\ExternalProfileVisibility;
use App\Features\AllowExternalProfileManagement;
use App\Http\Api\Query\Query;
use App\Http\Controllers\Api\BaseController;
use App\Http\Middleware\Api\EnabledOnlyOnLocalhost;
use App\Http\Middleware\Models\List\UserExceedsExternalProfileLimit;
use App\Http\Requests\Api\IndexRequest;
use App\Http\Requests\Api\ShowRequest;
use App\Http\Requests\Api\StoreRequest;
use App\Http\Requests\Api\UpdateRequest;
use App\Http\Resources\List\Collection\ExternalProfileCollection;
use App\Http\Resources\List\Resource\ExternalProfileResource;
use App\Models\List\ExternalProfile;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Str;
use Laravel\Pennant\Middleware\EnsureFeaturesAreActive;
/**
* Class ExternalProfileController.
*/
class ExternalProfileController extends BaseController
{
/**
* Create a new controller instance.
*/
public function __construct()
{
parent::__construct(ExternalProfile::class, 'externalprofile');
$isExternalProfileManagementAllowed = Str::of(EnsureFeaturesAreActive::class)
->append(':')
->append(AllowExternalProfileManagement::class)
->__toString();
$this->middleware(EnabledOnlyOnLocalhost::class);
$this->middleware($isExternalProfileManagementAllowed)->except(['index', 'show']);
$this->middleware(UserExceedsExternalProfileLimit::class)->only(['store', 'restore']);
}
/**
* Display a listing of the resource.
*
* @param IndexRequest $request
* @param IndexAction $action
* @return ExternalProfileCollection
*/
public function index(IndexRequest $request, IndexAction $action): ExternalProfileCollection
{
$query = new Query($request->validated());
$builder = ExternalProfile::query()->where(ExternalProfile::ATTRIBUTE_VISIBILITY, ExternalProfileVisibility::PUBLIC->value);
$externalprofiles = $query->hasSearchCriteria()
? $action->search($query, $request->schema())
: $action->index($builder, $query, $request->schema());
return new ExternalProfileCollection($externalprofiles, $query);
}
/**
* Store a newly created resource.
*
* @param StoreRequest $request
* @param StoreExternalProfileAction $action
* @return ExternalProfileResource
*/
public function store(StoreRequest $request, StoreExternalProfileAction $action): ExternalProfileResource
{
$validated = array_merge(
$request->validated(),
[ExternalProfile::ATTRIBUTE_USER => Auth::id()]
);
$externalprofile = $action->store(ExternalProfile::query(), $validated);
return new ExternalProfileResource($externalprofile, new Query());
}
/**
* Display the specified resource.
*
* @param ShowRequest $request
* @param ExternalProfile $externalprofile
* @param ShowAction $action
* @return ExternalProfileResource
*/
public function show(ShowRequest $request, ExternalProfile $externalprofile, ShowAction $action): ExternalProfileResource
{
$query = new Query($request->validated());
$show = $action->show($externalprofile, $query, $request->schema());
return new ExternalProfileResource($show, $query);
}
/**
* Update the specified resource.
*
* @param UpdateRequest $request
* @param ExternalProfile $externalprofile
* @param UpdateAction $action
* @return ExternalProfileResource
*/
public function update(UpdateRequest $request, ExternalProfile $externalprofile, UpdateAction $action): ExternalProfileResource
{
$updated = $action->update($externalprofile, $request->validated());
return new ExternalProfileResource($updated, new Query());
}
/**
* Remove the specified resource.
*
* @param ExternalProfile $externalprofile
* @param DestroyAction $action
* @return ExternalProfileResource
*/
public function destroy(ExternalProfile $externalprofile, DestroyAction $action): ExternalProfileResource
{
$deleted = $action->destroy($externalprofile);
return new ExternalProfileResource($deleted, new Query());
}
/**
* Restore the specified resource.
*
* @param ExternalProfile $externalprofile
* @param RestoreAction $action
* @return ExternalProfileResource
*/
public function restore(ExternalProfile $externalprofile, RestoreAction $action): ExternalProfileResource
{
$restored = $action->restore($externalprofile);
return new ExternalProfileResource($restored, new Query());
}
/**
* Hard-delete the specified resource.
*
* @param ExternalProfile $externalprofile
* @param ForceDeleteAction $action
* @return JsonResponse
*/
public function forceDelete(ExternalProfile $externalprofile, ForceDeleteAction $action): JsonResponse
{
$message = $action->forceDelete($externalprofile);
return new JsonResponse([
'message' => $message,
]);
}
}
@@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
namespace App\Http\Middleware\Api;
use Closure;
use Illuminate\Http\Request;
/**
* Class EnabledOnlyOnLocalhost.
*/
class EnabledOnlyOnLocalhost
{
/**
* Handle an incoming request.
*
* @param Request $request
* @param Closure(Request): mixed $next
* @return mixed
*/
public function handle(Request $request, Closure $next): mixed
{
$ip = $request->ip();
if ($ip !== '127.0.0.1') {
abort(403, "Route only available for localhost");
}
return $next($request);
}
}
@@ -0,0 +1,46 @@
<?php
declare(strict_types=1);
namespace App\Http\Middleware\Models\List;
use App\Constants\Config\ExternalProfileConstants;
use App\Enums\Auth\SpecialPermission;
use App\Models\Auth\User;
use App\Models\List\ExternalProfile;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Config;
/**
* Class ExternalProfileExceedsEntryLimit.
*/
class ExternalProfileExceedsEntryLimit
{
/**
* Handle an incoming request.
*
* @param Request $request
* @param Closure(Request): mixed $next
* @return mixed
*/
public function handle(Request $request, Closure $next): mixed
{
$entryLimit = intval(Config::get(ExternalProfileConstants::MAX_ENTRIES_QUALIFIED));
/** @var ExternalProfile|null $profile */
$profile = $request->route('externalprofile');
/** @var User|null $user */
$user = $request->user('sanctum');
if (
intval($profile?->externalentries()?->count()) >= $entryLimit
&& empty($user?->can(SpecialPermission::BYPASS_FEATURE_FLAGS->value))
) {
abort(403, "External profiles cannot contain more than '$entryLimit' entries.");
}
return $next($request);
}
}
@@ -0,0 +1,42 @@
<?php
declare(strict_types=1);
namespace App\Http\Middleware\Models\List;
use App\Constants\Config\ExternalProfileConstants;
use App\Enums\Auth\SpecialPermission;
use App\Models\Auth\User;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Config;
/**
* Class UserExceedsExternalProfileLimit.
*/
class UserExceedsExternalProfileLimit
{
/**
* Handle an incoming request.
*
* @param Request $request
* @param Closure(Request): mixed $next
* @return mixed
*/
public function handle(Request $request, Closure $next): mixed
{
$profileLimit = intval(Config::get(ExternalProfileConstants::MAX_PROFILES_QUALIFIED));
/** @var User|null $user */
$user = $request->user('sanctum');
if (
intval($user?->externalprofiles()?->count()) >= $profileLimit
&& empty($user?->can(SpecialPermission::BYPASS_FEATURE_FLAGS->value))
) {
abort(403, "User cannot have more than '$profileLimit' external profiles.");
}
return $next($request);
}
}
@@ -0,0 +1,36 @@
<?php
declare(strict_types=1);
namespace App\Http\Resources\List\Collection;
use App\Http\Resources\BaseCollection;
use App\Http\Resources\List\Resource\ExternalProfileResource;
use App\Models\List\ExternalProfile;
use Illuminate\Http\Request;
/**
* Class ExternalProfileCollection.
*/
class ExternalProfileCollection extends BaseCollection
{
/**
* The "data" wrapper that should be applied.
*
* @var string|null
*/
public static $wrap = 'externalprofiles';
/**
* Transform the resource into a JSON array.
*
* @param Request $request
* @return array
*
* @noinspection PhpMissingParentCallCommonInspection
*/
public function toArray(Request $request): array
{
return $this->collection->map(fn (ExternalProfile $profile) => new ExternalProfileResource($profile, $this->query))->all();
}
}
@@ -0,0 +1,36 @@
<?php
declare(strict_types=1);
namespace App\Http\Resources\List\External\Collection;
use App\Http\Resources\BaseCollection;
use App\Http\Resources\List\External\Resource\ExternalEntryResource;
use App\Models\List\External\ExternalEntry;
use Illuminate\Http\Request;
/**
* Class ExternalEntryCollection.
*/
class ExternalEntryCollection extends BaseCollection
{
/**
* The "data" wrapper that should be applied.
*
* @var string|null
*/
public static $wrap = 'externalentries';
/**
* Transform the resource into a JSON array.
*
* @param Request $request
* @return array
*
* @noinspection PhpMissingParentCallCommonInspection
*/
public function toArray(Request $request): array
{
return $this->collection->map(fn (ExternalEntry $entry) => new ExternalEntryResource($entry, $this->query))->all();
}
}
@@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
namespace App\Http\Resources\List\External\Resource;
use App\Http\Api\Schema\List\External\ExternalEntrySchema;
use App\Http\Api\Schema\Schema;
use App\Http\Resources\BaseResource;
/**
* Class ExternalEntryResource.
*/
class ExternalEntryResource extends BaseResource
{
/**
* The "data" wrapper that should be applied.
*
* @var string|null
*/
public static $wrap = 'externalentry';
/**
* Get the resource schema.
*
* @return Schema
*/
protected function schema(): Schema
{
return new ExternalEntrySchema();
}
}
@@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
namespace App\Http\Resources\List\Resource;
use App\Http\Api\Schema\List\ExternalProfileSchema;
use App\Http\Api\Schema\Schema;
use App\Http\Resources\BaseResource;
/**
* Class ExternalProfileResource.
*/
class ExternalProfileResource extends BaseResource
{
/**
* The "data" wrapper that should be applied.
*
* @var string|null
*/
public static $wrap = 'externalprofile';
/**
* Get the resource schema.
*
* @return Schema
*/
protected function schema(): Schema
{
return new ExternalProfileSchema();
}
}
+13
View File
@@ -13,6 +13,7 @@ use App\Events\Auth\User\UserRestored;
use App\Events\Auth\User\UserUpdated;
use App\Models\Admin\ActionLog;
use App\Models\List\Playlist;
use App\Models\List\ExternalProfile;
use Database\Factories\Auth\UserFactory;
use Filament\Models\Contracts\FilamentUser;
use Filament\Models\Contracts\HasAvatar;
@@ -39,6 +40,7 @@ use Spatie\Permission\Traits\HasRoles;
* @property Carbon $deleted_at
* @property string $email
* @property Carbon|null $email_verified_at
* @property Collection<int, ExternalProfile> $externalprofiles
* @property int $id
* @property string $name
* @property string $password
@@ -76,6 +78,7 @@ class User extends Authenticatable implements MustVerifyEmail, Nameable, HasSubt
final public const ATTRIBUTE_TWO_FACTOR_RECOVERY_CODES = 'two_factor_recovery_codes';
final public const ATTRIBUTE_TWO_FACTOR_SECRET = 'two_factor_secret';
final public const RELATION_EXTERNAL_PROFILES = 'externalprofiles';
final public const RELATION_PERMISSIONS = 'permissions';
final public const RELATION_PLAYLISTS = 'playlists';
final public const RELATION_ROLES = 'roles';
@@ -234,4 +237,14 @@ class User extends Authenticatable implements MustVerifyEmail, Nameable, HasSubt
{
return $this->hasMany(ActionLog::class, ActionLog::ATTRIBUTE_USER);
}
/**
* Get the playlists that belong to the user.
*
* @return HasMany
*/
public function externalprofiles(): HasMany
{
return $this->hasMany(ExternalProfile::class, ExternalProfile::ATTRIBUTE_USER);
}
}
+152
View File
@@ -0,0 +1,152 @@
<?php
declare(strict_types=1);
namespace App\Models\List\External;
use App\Enums\Models\List\AnimeWatchStatus;
use App\Events\List\ExternalProfile\ExternalEntry\ExternalEntryCreated;
use App\Events\List\ExternalProfile\ExternalEntry\ExternalEntryDeleted;
use App\Events\List\ExternalProfile\ExternalEntry\ExternalEntryRestored;
use App\Events\List\ExternalProfile\ExternalEntry\ExternalEntryUpdated;
use App\Models\BaseModel;
use App\Models\List\ExternalProfile;
use App\Models\Wiki\Anime;
use Database\Factories\List\External\ExternalEntryFactory;
use Elastic\ScoutDriverPlus\Searchable;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
/**
* Class ExternalEntry.
*
* @property int $entry_id
* @property int $anime_id
* @property Anime $anime
* @property int $external_profile_id
* @property ExternalProfile $externalprofile
* @property bool $is_favorite
* @property float|null $score
* @property AnimeWatchStatus|null $watch_status
*
* @method static ExternalEntryFactory factory(...$parameters)
*/
class ExternalEntry extends BaseModel
{
use Searchable;
final public const TABLE = 'external_entries';
final public const ATTRIBUTE_ID = 'entry_id';
final public const ATTRIBUTE_ANIME = 'anime_id';
final public const ATTRIBUTE_EXTERNAL_PROFILE = 'external_profile_id';
final public const ATTRIBUTE_IS_FAVORITE = 'is_favorite';
final public const ATTRIBUTE_SCORE = 'score';
final public const ATTRIBUTE_WATCH_STATUS = 'watch_status';
final public const RELATION_ANIME = 'anime';
final public const RELATION_EXTERNAL_PROFILE = 'externalprofile';
final public const RELATION_USER = 'externalprofile.user';
/**
* The attributes that are mass assignable.
*
* @var array<int, string>
*/
protected $fillable = [
ExternalEntry::ATTRIBUTE_ANIME,
ExternalEntry::ATTRIBUTE_EXTERNAL_PROFILE,
ExternalEntry::ATTRIBUTE_IS_FAVORITE,
ExternalEntry::ATTRIBUTE_SCORE,
ExternalEntry::ATTRIBUTE_WATCH_STATUS,
];
/**
* The event map for the model.
*
* Allows for object-based events for native Eloquent events.
*
* @var array
*/
protected $dispatchesEvents = [
'created' => ExternalEntryCreated::class,
'deleted' => ExternalEntryDeleted::class,
'restored' => ExternalEntryRestored::class,
'updated' => ExternalEntryUpdated::class,
];
/**
* The table associated with the model.
*
* @var string
*/
protected $table = ExternalEntry::TABLE;
/**
* The primary key associated with the table.
*
* @var string
*/
protected $primaryKey = ExternalEntry::ATTRIBUTE_ID;
/**
* The attributes that should be cast.
*
* @var array<string, string>
*/
protected $casts = [
ExternalEntry::ATTRIBUTE_IS_FAVORITE => 'bool',
ExternalEntry::ATTRIBUTE_WATCH_STATUS => AnimeWatchStatus::class,
];
/**
* Get name.
*
* @return string
*/
public function getName(): string
{
return strval($this->getKey());
}
/**
* Get subtitle.
*
* @return string
*/
public function getSubtitle(): string
{
return $this->anime->getName();
}
/**
* Get the route key for the model.
*
* @return string
*
* @noinspection PhpMissingParentCallCommonInspection
*/
public function getRouteKeyName(): string
{
return ExternalEntry::ATTRIBUTE_ID;
}
/**
* Get the anime that owns the user external entry.
*
* @return BelongsTo
*/
public function anime(): BelongsTo
{
return $this->belongsTo(Anime::class, ExternalEntry::ATTRIBUTE_ANIME);
}
/**
* Get the user profile that owns the user profile.
*
* @return BelongsTo
*/
public function externalprofile(): BelongsTo
{
return $this->belongsTo(ExternalProfile::class, ExternalEntry::ATTRIBUTE_EXTERNAL_PROFILE);
}
}
+172
View File
@@ -0,0 +1,172 @@
<?php
declare(strict_types=1);
namespace App\Models\List;
use App\Enums\Models\List\ExternalProfileSite;
use App\Enums\Models\List\ExternalProfileVisibility;
use App\Events\List\ExternalProfile\ExternalProfileCreated;
use App\Events\List\ExternalProfile\ExternalProfileDeleted;
use App\Events\List\ExternalProfile\ExternalProfileRestored;
use App\Events\List\ExternalProfile\ExternalProfileUpdated;
use App\Models\Auth\User;
use App\Models\BaseModel;
use App\Models\List\External\ExternalEntry;
use Database\Factories\List\ExternalProfileFactory;
use Elastic\ScoutDriverPlus\Searchable;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Support\Collection;
/**
* Class ExternalProfile.
*
* @property int $profile_id
* @property Collection<int, ExternalEntry> $externalentries
* @property string $name
* @property ExternalProfileSite $site
* @property int|null $user_id
* @property User|null $user
* @property ExternalProfileVisibility $visibility
*
* @method static ExternalProfileFactory factory(...$parameters)
*/
class ExternalProfile extends BaseModel
{
use Searchable;
final public const TABLE = 'external_profiles';
final public const ATTRIBUTE_ID = 'profile_id';
final public const ATTRIBUTE_NAME = 'name';
final public const ATTRIBUTE_SITE = 'site';
final public const ATTRIBUTE_VISIBILITY = 'visibility';
final public const ATTRIBUTE_USER = 'user_id';
final public const RELATION_ANIMES = 'externalentries.anime';
final public const RELATION_EXTERNAL_ENTRIES = 'externalentries';
final public const RELATION_USER = 'user';
/**
* The attributes that are mass assignable.
*
* @var array<int, string>
*/
protected $fillable = [
ExternalProfile::ATTRIBUTE_NAME,
ExternalProfile::ATTRIBUTE_SITE,
ExternalProfile::ATTRIBUTE_VISIBILITY,
ExternalProfile::ATTRIBUTE_USER,
];
/**
* The event map for the model.
*
* Allows for object-based events for native Eloquent events.
*
* @var array
*/
protected $dispatchesEvents = [
'created' => ExternalProfileCreated::class,
'deleted' => ExternalProfileDeleted::class,
'restored' => ExternalProfileRestored::class,
'updated' => ExternalProfileUpdated::class,
];
/**
* The table associated with the model.
*
* @var string
*/
protected $table = ExternalProfile::TABLE;
/**
* The primary key associated with the table.
*
* @var string
*/
protected $primaryKey = ExternalProfile::ATTRIBUTE_ID;
/**
* The attributes that should be cast.
*
* @var array<string, string>
*/
protected $casts = [
ExternalProfile::ATTRIBUTE_SITE => ExternalProfileSite::class,
ExternalProfile::ATTRIBUTE_VISIBILITY => ExternalProfileVisibility::class,
];
/**
* Get name.
*
* @return string
*/
public function getName(): string
{
return $this->name;
}
/**
* Get subtitle.
*
* @return string
*/
public function getSubtitle(): string
{
return $this->user === null ? $this->getName() : $this->user->getName();
}
/**
* Get the route key for the model.
*
* @return string
*
* @noinspection PhpMissingParentCallCommonInspection
*/
public function getRouteKeyName(): string
{
return ExternalProfile::ATTRIBUTE_ID;
}
/**
* Determine if the model should be searchable.
*
* @return bool
*/
public function shouldBeSearchable(): bool
{
return ExternalProfileVisibility::PUBLIC === $this->visibility;
}
/**
* Get the entries for the profile.
*
* @return HasMany
*/
public function externalentries(): HasMany
{
return $this->hasMany(ExternalEntry::class, ExternalEntry::ATTRIBUTE_EXTERNAL_PROFILE);
}
/**
* Get the user that owns the profile.
*
* @return BelongsTo
*/
public function user(): BelongsTo
{
return $this->belongsTo(User::class, ExternalProfile::ATTRIBUTE_USER);
}
/**
* Only get the attributes as an array to prevent recursive toArray() calls.
*
* @return array
*/
public function toSearchableArray(): array
{
return $this->attributesToArray();
}
}
+14
View File
@@ -17,6 +17,7 @@ use App\Http\Resources\Pivot\Wiki\Resource\AnimeSeriesResource;
use App\Http\Resources\Pivot\Wiki\Resource\AnimeStudioResource;
use App\Models\BaseModel;
use App\Models\Discord\DiscordThread;
use App\Models\List\External\ExternalEntry;
use App\Models\Wiki\Anime\AnimeSynonym;
use App\Models\Wiki\Anime\AnimeTheme;
use App\Pivots\Wiki\AnimeImage;
@@ -39,6 +40,7 @@ use Laravel\Nova\Actions\Actionable;
* @property Collection<int, AnimeSynonym> $animesynonyms
* @property Collection<int, AnimeTheme> $animethemes
* @property DiscordThread|null $discordthread
* @property Collection<int, ExternalEntry> $externalentries
* @property Collection<int, Image> $images
* @property AnimeMediaFormat $media_format
* @property string $name
@@ -70,6 +72,8 @@ class Anime extends BaseModel
final public const RELATION_ARTISTS = 'animethemes.song.artists';
final public const RELATION_AUDIO = 'animethemes.animethemeentries.videos.audio';
final public const RELATION_ENTRIES = 'animethemes.animethemeentries';
final public const RELATION_EXTERNAL_ENTRIES = 'externalentries';
final public const RELATION_EXTERNAL_PROFILE = 'externalentries.externalprofile';
final public const RELATION_GROUPS = 'animethemes.group';
final public const RELATION_IMAGES = 'images';
final public const RELATION_RESOURCES = 'resources';
@@ -273,4 +277,14 @@ class Anime extends BaseModel
->as(AnimeStudioResource::$wrap)
->withTimestamps();
}
/**
* Get the entries for the anime.
*
* @return HasMany
*/
public function externalentries(): HasMany
{
return $this->hasMany(ExternalEntry::class, ExternalEntry::ATTRIBUTE_ANIME);
}
}
+3
View File
@@ -11,6 +11,7 @@ use App\Nova\Actions\Models\Auth\User\RevokePermissionAction;
use App\Nova\Actions\Models\Auth\User\RevokeRoleAction;
use App\Nova\Resources\BaseResource;
use App\Nova\Resources\List\Playlist;
use App\Nova\Resources\List\ExternalProfile;
use Exception;
use Illuminate\Validation\Rule;
use Laravel\Nova\Fields\BelongsToMany;
@@ -174,6 +175,8 @@ class User extends BaseResource
HasMany::make(__('nova.resources.label.playlists'), UserModel::RELATION_PLAYLISTS, Playlist::class),
HasMany::make(__('nova.resources.label.externalprofiles'), UserModel::RELATION_EXTERNAL_PROFILES, ExternalProfile::class),
Panel::make(__('nova.fields.base.timestamps'), $this->timestamps())
->collapsable(),
];
+192
View File
@@ -0,0 +1,192 @@
<?php
declare(strict_types=1);
namespace App\Nova\Resources\List\External;
use App\Enums\Models\List\AnimeWatchStatus;
use App\Models\List\External\ExternalEntry as ExternalEntryModel;
use App\Nova\Resources\BaseResource;
use App\Nova\Resources\List\ExternalProfile;
use App\Nova\Resources\Wiki\Anime;
use Illuminate\Validation\Rules\Enum;
use Laravel\Nova\Fields\BelongsTo;
use Laravel\Nova\Fields\Boolean;
use Laravel\Nova\Fields\ID;
use Laravel\Nova\Fields\Number;
use Laravel\Nova\Fields\Select;
use Laravel\Nova\Http\Requests\NovaRequest;
use Laravel\Nova\Panel;
/**
* Class ExternalEntry.
*/
class ExternalEntry extends BaseResource
{
/**
* The model the resource corresponds to.
*
* @var string
*/
public static string $model = ExternalEntryModel::class;
/**
* The single value that should be used to represent the resource when being displayed.
*
* @var string
*/
public static $title = ExternalEntryModel::ATTRIBUTE_ANIME;
/**
* Get the value that should be displayed to represent the resource.
*
* @return string
*/
public function title(): string
{
$entry = $this->model();
if ($entry instanceof ExternalEntryModel) {
return $entry->getName();
}
return parent::title();
}
/**
* Get the search result subtitle for the resource.
*
* @return string|null
*
* @noinspection PhpMissingParentCallCommonInspection
*/
public function subtitle(): ?string
{
$entry = $this->model();
if ($entry instanceof ExternalEntryModel) {
return $entry->getName();
}
return null;
}
/**
* Get the displayable label of the resource.
*
* @return string
*
* @noinspection PhpMissingParentCallCommonInspection
*/
public static function label(): string
{
return __('nova.resources.label.externalentries');
}
/**
* Get the displayable singular label of the resource.
*
* @return string
*
* @noinspection PhpMissingParentCallCommonInspection
*/
public static function singularLabel(): string
{
return __('nova.resources.singularLabel.externalentries');
}
/**
* Get the URI key for the resource.
*
* @return string
*
* @noinspection PhpMissingParentCallCommonInspection
*/
public static function uriKey(): string
{
return 'user-external-list-entry';
}
/**
* The logical group associated with the resource.
*
* @return string
*
* @noinspection PhpMissingParentCallCommonInspection
*/
public static function group(): string
{
return __('nova.resources.group.list');
}
/**
* Indicates if the resource should be globally searchable.
*
* @var bool
*/
public static $globallySearchable = false;
/**
* Get the fields displayed by the resource.
*
* @param NovaRequest $request
* @return array
*/
public function fields(NovaRequest $request): array
{
return [
ID::make(__('nova.fields.base.id'), ExternalEntryModel::ATTRIBUTE_ID)
->sortable()
->showOnPreview()
->showWhenPeeking(),
BelongsTo::make(__('nova.resources.singularLabel.anime'), ExternalEntryModel::RELATION_ANIME, Anime::class)
->sortable()
->filterable()
->searchable()
->required()
->rules(['required'])
->withSubtitles()
->showOnPreview(),
BelongsTo::make(__('nova.resources.singularLabel.externalprofiles'), ExternalEntryModel::RELATION_EXTERNAL_PROFILE, ExternalProfile::class)
->sortable()
->filterable()
->searchable()
->required()
->rules(['required'])
->withSubtitles()
->showCreateRelationButton()
->showOnPreview(),
Boolean::make(__('nova.fields.externalentry.is_favorite.name'), ExternalEntryModel::ATTRIBUTE_IS_FAVORITE)
->sortable()
->filterable()
->rules(['required'])
->help(__('nova.fields.externalentry.is_favorite.help'))
->showWhenPeeking()
->showOnPreview(),
Number::make(__('nova.fields.externalentry.score.name'), ExternalEntryModel::ATTRIBUTE_SCORE)
->sortable()
->nullable()
->rules(['nullable'])
->step(0.01)
->help(__('nova.fields.externalentry.score.help'))
->showOnPreview()
->filterable()
->showWhenPeeking(),
Select::make(__('nova.fields.externalentry.watch_status.name'), ExternalEntryModel::ATTRIBUTE_WATCH_STATUS)
->options(AnimeWatchStatus::asSelectArray())
->displayUsing(fn (?int $enumValue) => AnimeWatchStatus::tryFrom($enumValue)?->localize())
->sortable()
->rules(['required', new Enum(AnimeWatchStatus::class)])
->help(__('nova.fields.externalentry.watch_status.help'))
->showOnPreview()
->filterable()
->showWhenPeeking(),
Panel::make(__('nova.fields.base.timestamps'), $this->timestamps())
->collapsable(),
];
}
}
+202
View File
@@ -0,0 +1,202 @@
<?php
declare(strict_types=1);
namespace App\Nova\Resources\List;
use App\Enums\Models\List\ExternalProfileSite;
use App\Enums\Models\List\ExternalProfileVisibility;
use App\Models\List\ExternalProfile as ExternalProfileModel;
use App\Nova\Resources\Auth\User;
use App\Nova\Resources\BaseResource;
use App\Nova\Resources\List\External\ExternalEntry;
use Illuminate\Validation\Rules\Enum;
use Laravel\Nova\Fields\BelongsTo;
use Laravel\Nova\Fields\HasMany;
use Laravel\Nova\Fields\ID;
use Laravel\Nova\Fields\Select;
use Laravel\Nova\Fields\Text;
use Laravel\Nova\Http\Requests\NovaRequest;
use Laravel\Nova\Panel;
use Laravel\Nova\Query\Search\Column;
/**
* Class ExternalProfile.
*/
class ExternalProfile extends BaseResource
{
/**
* The model the resource corresponds to.
*
* @var string
*/
public static string $model = ExternalProfileModel::class;
/**
* The single value that should be used to represent the resource when being displayed.
*
* @var string
*/
public static $title = ExternalProfileModel::ATTRIBUTE_NAME;
/**
* Get the value that should be displayed to represent the resource.
*
* @return string
*/
public function title(): string
{
$profile = $this->model();
if ($profile instanceof ExternalProfileModel) {
return $profile->getName();
}
return parent::title();
}
/**
* Get the search result subtitle for the resource.
*
* @return string|null
*
* @noinspection PhpMissingParentCallCommonInspection
*/
public function subtitle(): ?string
{
$profile = $this->model();
if ($profile instanceof ExternalProfileModel) {
return $profile->getName();
}
return null;
}
/**
* Get the displayable label of the resource.
*
* @return string
*
* @noinspection PhpMissingParentCallCommonInspection
*/
public static function label(): string
{
return __('nova.resources.label.externalprofiles');
}
/**
* Get the displayable singular label of the resource.
*
* @return string
*
* @noinspection PhpMissingParentCallCommonInspection
*/
public static function singularLabel(): string
{
return __('nova.resources.singularLabel.externalprofiles');
}
/**
* Get the URI key for the resource.
*
* @return string
*
* @noinspection PhpMissingParentCallCommonInspection
*/
public static function uriKey(): string
{
return 'user-external-profile';
}
/**
* The logical group associated with the resource.
*
* @return string
*
* @noinspection PhpMissingParentCallCommonInspection
*/
public static function group(): string
{
return __('nova.resources.group.list');
}
/**
* Get the searchable columns for the resource.
*
* @return array
*
* @noinspection PhpMissingParentCallCommonInspection
*/
public static function searchableColumns(): array
{
return [
new Column(ExternalProfileModel::ATTRIBUTE_NAME),
];
}
/**
* Indicates if the resource should be globally searchable.
*
* @var bool
*/
public static $globallySearchable = false;
/**
* Get the fields displayed by the resource.
*
* @param NovaRequest $request
* @return array
*/
public function fields(NovaRequest $request): array
{
return [
ID::make(__('nova.fields.base.id'), ExternalProfileModel::ATTRIBUTE_ID)
->sortable()
->showOnPreview()
->showWhenPeeking(),
Text::make(__('nova.fields.externalprofile.name.name'), ExternalProfileModel::ATTRIBUTE_NAME)
->sortable()
->copyable()
->rules(['required', 'max:192'])
->help(__('nova.fields.externalprofile.name.help'))
->showOnPreview()
->filterable()
->maxlength(192)
->enforceMaxlength()
->showWhenPeeking(),
Select::make(__('nova.fields.externalprofile.site.name'), ExternalProfileModel::ATTRIBUTE_SITE)
->options(ExternalProfileSite::asSelectArray())
->displayUsing(fn (?int $enumValue) => ExternalProfileSite::tryFrom($enumValue)?->localize())
->sortable()
->rules(['required', new Enum(ExternalProfileSite::class)])
->help(__('nova.fields.externalprofile.site.help'))
->showOnPreview()
->filterable()
->showWhenPeeking(),
Select::make(__('nova.fields.externalprofile.visibility.name'), ExternalProfileModel::ATTRIBUTE_VISIBILITY)
->options(ExternalProfileVisibility::asSelectArray())
->displayUsing(fn (?int $enumValue) => ExternalProfileVisibility::tryFrom($enumValue)?->localize())
->sortable()
->rules(['required', new Enum(ExternalProfileVisibility::class)])
->help(__('nova.fields.externalprofile.visibility.help'))
->showOnPreview()
->filterable()
->showWhenPeeking(),
BelongsTo::make(__('nova.resources.singularLabel.user'), ExternalProfileModel::RELATION_USER, User::class)
->sortable()
->filterable()
->searchable()
->nullable()
->withSubtitles()
->showOnPreview(),
HasMany::make(__('nova.resources.label.externalentries'), ExternalProfileModel::RELATION_EXTERNAL_ENTRIES, ExternalEntry::class),
Panel::make(__('nova.fields.base.timestamps'), $this->timestamps())
->collapsable(),
];
}
}
+189
View File
@@ -0,0 +1,189 @@
<?php
declare(strict_types=1);
namespace App\Policies\List\External;
use App\Enums\Auth\CrudPermission;
use App\Enums\Auth\ExtendedCrudPermission;
use App\Enums\Models\List\ExternalProfileVisibility;
use App\Models\Auth\User;
use App\Models\List\ExternalProfile;
use App\Models\List\External\ExternalEntry;
use Filament\Facades\Filament;
use Illuminate\Auth\Access\HandlesAuthorization;
use Illuminate\Http\Request;
use Laravel\Nova\Nova;
/**
* Class ExternalEntryPolicy.
*/
class ExternalEntryPolicy
{
use HandlesAuthorization;
/**
* Determine whether the user can view any models.
*
* @param User|null $user
* @return bool
*/
public function viewAny(?User $user): bool
{
if (Filament::isServing()) {
return $user !== null && $user->hasRole('Admin');
}
return Nova::whenServing(
fn (): bool => $user !== null && $user->hasRole('Admin'),
function (Request $request) use ($user): bool {
/** @var ExternalProfile|null $profile */
$profile = $request->route('externalprofile');
return $user !== null
? ($user->getKey() === $profile?->user_id || ExternalProfileVisibility::PRIVATE !== $profile?->visibility) && $user->can(CrudPermission::VIEW->format(ExternalEntry::class))
: ExternalProfileVisibility::PRIVATE !== $profile?->visibility;
}
);
}
/**
* Determine whether the user can view the model.
*
* @param User|null $user
* @param ExternalEntry $entry
* @return bool
*
* @noinspection PhpUnusedParameterInspection
*/
public function view(?User $user, ExternalEntry $entry): bool
{
if (Filament::isServing()) {
return $user !== null && $user->hasRole('Admin');
}
return Nova::whenServing(
fn (): bool => $user !== null && $user->hasRole('Admin'),
function (Request $request) use ($user): bool {
/** @var ExternalProfile|null $profile */
$profile = $request->route('externalprofile');
return $user !== null
? ($user->getKey() === $profile?->user_id || ExternalProfileVisibility::PRIVATE !== $profile?->visibility) && $user->can(CrudPermission::VIEW->format(ExternalEntry::class))
: ExternalProfileVisibility::PRIVATE !== $profile?->visibility;
}
);
}
/**
* Determine whether the user can create models.
*
* @param User $user
* @return bool
*/
public function create(User $user): bool
{
if (Filament::isServing()) {
return $user !== null && $user->hasRole('Admin');
}
return Nova::whenServing(
fn (): bool => $user->hasRole('Admin'),
function (Request $request) use ($user): bool {
/** @var ExternalProfile|null $profile */
$profile = $request->route('externalprofile');
return $user->getKey() === $profile?->user_id;
}
);
}
/**
* Determine whether the user can update the model.
*
* @param User $user
* @param ExternalEntry $entry
* @return bool
*
* @noinspection PhpUnusedParameterInspection
*/
public function update(User $user, ExternalEntry $entry): bool
{
if (Filament::isServing()) {
return $user !== null && $user->hasRole('Admin');
}
return Nova::whenServing(
fn (): bool => $user->hasRole('Admin'),
function (Request $request) use ($user, $entry): bool {
/** @var ExternalProfile|null $profile */
$profile = $request->route('externalprofile');
return ! $entry->trashed() && $user->getKey() === $profile?->user_id && $user->can(CrudPermission::UPDATE->format(ExternalEntry::class));
}
);
}
/**
* Determine whether the user can delete the model.
*
* @param User $user
* @param ExternalEntry $entry
* @return bool
*
* @noinspection PhpUnusedParameterInspection
*/
public function delete(User $user, ExternalEntry $entry): bool
{
if (Filament::isServing()) {
return $user !== null && $user->hasRole('Admin');
}
return Nova::whenServing(
fn (): bool => $user->hasRole('Admin'),
function (Request $request) use ($user, $entry): bool {
/** @var ExternalProfile|null $profile */
$profile = $request->route('externalprofile');
return ! $entry->trashed() && $user->getKey() === $profile?->user_id && $user->can(CrudPermission::DELETE->format(ExternalEntry::class));
}
);
}
/**
* Determine whether the user can restore the model.
*
* @param User $user
* @param ExternalEntry $entry
* @return bool
*
* @noinspection PhpUnusedParameterInspection
*/
public function restore(User $user, ExternalEntry $entry): bool
{
if (Filament::isServing()) {
return $user !== null && $user->hasRole('Admin');
}
return Nova::whenServing(
fn (): bool => $user->hasRole('Admin'),
function (Request $request) use ($user, $entry): bool {
/** @var ExternalProfile|null $profile */
$profile = $request->route('externalprofile');
return $entry->trashed() && $user->getKey() === $profile?->user_id && $user->can(ExtendedCrudPermission::RESTORE->format(ExternalEntry::class));
}
);
}
/**
* Determine whether the user can permanently delete the model.
*
* @param User $user
* @return bool
*/
public function forceDelete(User $user): bool
{
return $user->can(ExtendedCrudPermission::FORCE_DELETE->format(ExternalEntry::class));
}
}
+158
View File
@@ -0,0 +1,158 @@
<?php
declare(strict_types=1);
namespace App\Policies\List;
use App\Enums\Auth\CrudPermission;
use App\Enums\Auth\ExtendedCrudPermission;
use App\Enums\Models\List\ExternalProfileVisibility;
use App\Models\Auth\User;
use App\Models\List\ExternalProfile;
use Filament\Facades\Filament;
use Illuminate\Auth\Access\HandlesAuthorization;
use Laravel\Nova\Nova;
/**
* Class ExternalProfilePolicy.
*/
class ExternalProfilePolicy
{
use HandlesAuthorization;
/**
* Determine whether the user can view any models.
*
* @param User|null $user
* @return bool
*/
public function viewAny(?User $user): bool
{
if (Filament::isServing()) {
return $user !== null && $user->hasRole('Admin');
}
return Nova::whenServing(
fn (): bool => $user !== null && $user->hasRole('Admin'),
fn (): bool => $user === null || $user->can(CrudPermission::VIEW->format(ExternalProfile::class))
);
}
/**
* Determine whether the user can view the model.
*
* @param User|null $user
* @param ExternalProfile $profile
* @return bool
*/
public function view(?User $user, ExternalProfile $profile): bool
{
if (Filament::isServing()) {
return $user !== null && $user->hasRole('Admin');
}
return Nova::whenServing(
fn (): bool => $user !== null && $user->hasRole('Admin'),
fn (): bool => $user !== null
? ($user->getKey() === $profile->user_id || ExternalProfileVisibility::PRIVATE !== $profile->visibility) && $user->can(CrudPermission::VIEW->format(ExternalProfile::class))
: ExternalProfileVisibility::PRIVATE !== $profile->visibility
);
}
/**
* Determine whether the user can create models.
*
* @param User $user
* @return bool
*/
public function create(User $user): bool
{
if (Filament::isServing()) {
return $user !== null && $user->hasRole('Admin');
}
return Nova::whenServing(
fn (): bool => $user->hasRole('Admin'),
fn (): bool => $user->can(CrudPermission::CREATE->format(ExternalProfile::class))
);
}
/**
* Determine whether the user can update the model.
*
* @param User $user
* @param ExternalProfile $profile
* @return bool
*/
public function update(User $user, ExternalProfile $profile): bool
{
if (Filament::isServing()) {
return $user !== null && $user->hasRole('Admin');
}
return Nova::whenServing(
fn (): bool => $user->hasRole('Admin'),
fn (): bool => ! $profile->trashed() && $user->getKey() === $profile->user_id && $user->can(CrudPermission::UPDATE->format(ExternalProfile::class))
);
}
/**
* Determine whether the user can delete the model.
*
* @param User $user
* @param ExternalProfile $profile
* @return bool
*/
public function delete(User $user, ExternalProfile $profile): bool
{
if (Filament::isServing()) {
return $user !== null && $user->hasRole('Admin');
}
return Nova::whenServing(
fn (): bool => $user->hasRole('Admin'),
fn (): bool => ! $profile->trashed() && $user->getKey() === $profile->user_id && $user->can(CrudPermission::DELETE->format(ExternalProfile::class))
);
}
/**
* Determine whether the user can restore the model.
*
* @param User $user
* @param ExternalProfile $profile
* @return bool
*/
public function restore(User $user, ExternalProfile $profile): bool
{
if (Filament::isServing()) {
return $user !== null && $user->hasRole('Admin');
}
return Nova::whenServing(
fn (): bool => $user->hasRole('Admin'),
fn (): bool => $profile->trashed() && $user->getKey() === $profile->user_id && $user->can(ExtendedCrudPermission::RESTORE->format(ExternalProfile::class))
);
}
/**
* Determine whether the user can permanently delete the model.
*
* @param User $user
* @return bool
*/
public function forceDelete(User $user): bool
{
return $user->can(ExtendedCrudPermission::FORCE_DELETE->format(ExternalProfile::class));
}
/**
* Determine whether the user can add a entry to the profile.
*
* @param User $user
* @return bool
*/
public function addEntry(User $user): bool
{
return $user->hasRole('Admin');
}
}
+11
View File
@@ -273,4 +273,15 @@ class AnimePolicy
{
return $user->can(CrudPermission::UPDATE->format(Anime::class));
}
/**
* Determine whether the user can add a entry to the anime.
*
* @param User $user
* @return bool
*/
public function addEntry(User $user): bool
{
return $user->hasRole('Admin');
}
}
@@ -0,0 +1,39 @@
<?php
declare(strict_types=1);
namespace App\Rules\Admin;
use Closure;
use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Support\Carbon;
use Illuminate\Translation\PotentiallyTranslatedString;
use RuntimeException;
/**
* Class StartDateBeforeEndDateRule.
*/
class StartDateBeforeEndDateRule implements ValidationRule
{
/**
* Run the validation rule.
*
* @param string $attribute
* @param mixed $value
* @param Closure(string): PotentiallyTranslatedString $fail
* @return void
*
* @throws RuntimeException
*/
public function validate(string $attribute, mixed $value, Closure $fail): void
{
$dates = explode(' - ', $value);
$startAt = Carbon::createFromFormat('m/d/Y', $dates[0]);
$endAt = Carbon::createFromFormat('m/d/Y', $dates[1]);
if ($endAt->lessThan($startAt)) {
$fail(__('validation.date', ['attribute' => $attribute]));
}
}
}
+1
View File
@@ -45,6 +45,7 @@
"laravel/tinker": "^2.8",
"league/flysystem-aws-s3-v3": "^3.0",
"leandrocfe/filament-apex-charts": "3.1",
"malzariey/filament-daterangepicker-filter": "^2.7",
"propaganistas/laravel-disposable-email": "^2.2",
"spatie/db-dumper": "^3.1.1",
"spatie/laravel-permission": "^5.8",
Generated
+68 -1
View File
@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "ceb52a17efa10f4ca74b636ab69ea8ec",
"content-hash": "f7ceaa1076b7ab1d72ea6d6aa211ea76",
"packages": [
{
"name": "anourvalar/eloquent-serialize",
@@ -5474,6 +5474,73 @@
],
"time": "2024-04-02T14:22:50+00:00"
},
{
"name": "malzariey/filament-daterangepicker-filter",
"version": "2.7.3",
"source": {
"type": "git",
"url": "https://github.com/malzariey/filament-daterangepicker-filter.git",
"reference": "37dca49a04593be4df207107fe62131a02954d8a"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/malzariey/filament-daterangepicker-filter/zipball/37dca49a04593be4df207107fe62131a02954d8a",
"reference": "37dca49a04593be4df207107fe62131a02954d8a",
"shasum": ""
},
"require": {
"filament/filament": "^2.0|^3.0",
"illuminate/contracts": "^8.0|^9.0|^10.0|^11.0",
"php": "^8.0",
"spatie/laravel-package-tools": "^1.16.4"
},
"type": "library",
"extra": {
"laravel": {
"providers": [
"Malzariey\\FilamentDaterangepickerFilter\\FilamentDaterangepickerFilterServiceProvider"
],
"aliases": {
"FilamentDaterangepickerFilter": "Malzariey\\FilamentDaterangepickerFilter\\Facades\\FilamentDaterangepickerFilter"
}
}
},
"autoload": {
"psr-4": {
"Malzariey\\FilamentDaterangepickerFilter\\": "src",
"Malzariey\\FilamentDaterangepickerFilter\\Database\\Factories\\": "database/factories"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Majid Al-Zariey",
"email": "malzariey@gmail.com",
"role": "Developer"
}
],
"description": "This package uses daterangepciker library to filter date by a range or predefined date ranges (Today , Yesterday ...etc)",
"homepage": "https://github.com/malzariey/filament-daterangepicker-filter",
"keywords": [
"Malzariey",
"filament-daterangepicker-filter",
"laravel"
],
"support": {
"issues": "https://github.com/malzariey/filament-daterangepicker-filter/issues",
"source": "https://github.com/malzariey/filament-daterangepicker-filter/tree/2.7.3"
},
"funding": [
{
"url": "https://github.com/Malzariey",
"type": "github"
}
],
"time": "2024-06-07T06:30:43+00:00"
},
{
"name": "masterminds/html5",
"version": "2.9.0",
+21
View File
@@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
return [
/*
|--------------------------------------------------------------------------
| Limits
|--------------------------------------------------------------------------
|
| These values represent caps on external profiles to prevent spam. By default,
| an individual external profile is permitted 1000 entries, and a user
| is permitted 5 external profiles.
|
*/
'profile_max_entries' => (int) env('PROFILE_MAX_ENTRIES', 1000),
'user_max_profiles' => (int) env('USER_MAX_PROFILES', 5),
];
@@ -0,0 +1,38 @@
<?php
declare(strict_types=1);
namespace Database\Factories\List\External;
use App\Models\List\External\ExternalEntry;
use Illuminate\Database\Eloquent\Factories\Factory;
/**
* Class ExternalEntryFactory.
*
* @method ExternalEntry createOne($attributes = [])
* @method ExternalEntry makeOne($attributes = [])
*
* @extends Factory<ExternalEntry>
*/
class ExternalEntryFactory extends Factory
{
/**
* The name of the factory's corresponding model.
*
* @var class-string<ExternalEntry>
*/
protected $model = ExternalEntry::class;
/**
* Define the model's default state.
*
* @return array
*/
public function definition(): array
{
return [
//
];
}
}
@@ -0,0 +1,99 @@
<?php
declare(strict_types=1);
namespace Database\Factories\List;
use App\Enums\Models\List\AnimeWatchStatus;
use App\Enums\Models\List\ExternalProfileSite;
use App\Enums\Models\List\ExternalProfileVisibility;
use App\Models\List\ExternalProfile;
use App\Models\List\External\ExternalEntry;
use App\Models\Wiki\Anime;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\Arr;
/**
* Class ExternalProfileFactory.
*
* @method ExternalProfile createOne($attributes = [])
* @method ExternalProfile makeOne($attributes = [])
*
* @extends Factory<ExternalProfile>
*/
class ExternalProfileFactory extends Factory
{
/**
* The name of the factory's corresponding model.
*
* @var class-string<ExternalProfile>
*/
protected $model = ExternalProfile::class;
/**
* Define the model's default state.
*
* @return array
*/
public function definition(): array
{
$site = Arr::random(ExternalProfileSite::cases());
$visibility = Arr::random(ExternalProfileVisibility::cases());
return [
ExternalProfile::ATTRIBUTE_NAME => fake()->words(3, true),
ExternalProfile::ATTRIBUTE_SITE => $site->value,
ExternalProfile::ATTRIBUTE_VISIBILITY => $visibility->value,
];
}
/**
* Define the model's entry listing.
*
* @param int $count
* @return static
*/
public function entries(int $count): static
{
return $this->afterCreating(
function (ExternalProfile $profile) use ($count) {
foreach (range(1, $count) as $index) {
$entry = ExternalEntry::factory()
->for($profile)
->for(Anime::factory())
->createOne();
$entry->is_favorite = fake()->boolean();
$entry->score = fake()->numberBetween(1, 10);
$entry->watch_status = Arr::random(AnimeWatchStatus::cases())->value;
$entry->save();
}
}
);
}
/**
* Define the model's entry listing.
*
* @param array $animeIds
* @return static
*/
public function entriesForIds(array $animeIds): static
{
return $this->afterCreating(
function (ExternalProfile $profile) use ($animeIds) {
foreach ($animeIds as $animeId) {
$entry = ExternalEntry::factory()
->for($profile)
->for(Anime::query()->find($animeId))
->createOne();
$entry->is_favorite = fake()->boolean();
$entry->score = fake()->numberBetween(1, 10);
$entry->watch_status = Arr::random(AnimeWatchStatus::cases())->value;
$entry->save();
}
}
);
}
}
@@ -0,0 +1,42 @@
<?php
declare(strict_types=1);
use App\Enums\Models\List\ExternalProfileVisibility;
use App\Models\Auth\User;
use App\Models\BaseModel;
use App\Models\List\ExternalProfile;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
if (! Schema::hasTable(ExternalProfile::TABLE)) {
Schema::create(ExternalProfile::TABLE, function (Blueprint $table) {
$table->timestamps(6);
$table->softDeletes(BaseModel::ATTRIBUTE_DELETED_AT, 6);
$table->id(ExternalProfile::ATTRIBUTE_ID);
$table->string(ExternalProfile::ATTRIBUTE_NAME);
$table->integer(ExternalProfile::ATTRIBUTE_SITE)->nullable();
$table->integer(ExternalProfile::ATTRIBUTE_VISIBILITY)->default(ExternalProfileVisibility::PRIVATE->value);
$table->unsignedBigInteger(ExternalProfile::ATTRIBUTE_USER)->nullable();
$table->foreign(ExternalProfile::ATTRIBUTE_USER)->references(User::ATTRIBUTE_ID)->on(User::TABLE)->cascadeOnDelete();
});
}
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists(ExternalProfile::TABLE);
}
};
@@ -0,0 +1,45 @@
<?php
declare(strict_types=1);
use App\Models\BaseModel;
use App\Models\List\External\ExternalEntry;
use App\Models\List\ExternalProfile;
use App\Models\Wiki\Anime;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
if (! Schema::hasTable(ExternalEntry::TABLE)) {
Schema::create(ExternalEntry::TABLE, function (Blueprint $table) {
$table->timestamps(6);
$table->softDeletes(BaseModel::ATTRIBUTE_DELETED_AT, 6);
$table->id(ExternalEntry::ATTRIBUTE_ID);
$table->float(ExternalEntry::ATTRIBUTE_SCORE)->nullable();
$table->integer(ExternalEntry::ATTRIBUTE_WATCH_STATUS)->nullable();
$table->boolean(ExternalEntry::ATTRIBUTE_IS_FAVORITE)->default(false);
$table->unsignedBigInteger(ExternalEntry::ATTRIBUTE_ANIME)->nullable();
$table->foreign(ExternalEntry::ATTRIBUTE_ANIME)->references(Anime::ATTRIBUTE_ID)->on(Anime::TABLE)->nullOnDelete();
$table->unsignedBigInteger(ExternalEntry::ATTRIBUTE_EXTERNAL_PROFILE)->nullable();
$table->foreign(ExternalEntry::ATTRIBUTE_EXTERNAL_PROFILE)->references(ExternalProfile::ATTRIBUTE_ID)->on(ExternalProfile::TABLE)->cascadeOnDelete();
});
}
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists(ExternalEntry::TABLE);
}
};
@@ -7,6 +7,7 @@ namespace Database\Seeders\Admin\Feature;
use App\Constants\FeatureConstants;
use App\Features\AllowAudioStreams;
use App\Features\AllowDumpDownloading;
use App\Features\AllowExternalProfileManagement;
use App\Features\AllowPlaylistManagement;
use App\Features\AllowScriptDownloading;
use App\Features\AllowVideoStreams;
@@ -27,6 +28,7 @@ class FeatureSeeder extends Seeder
{
Feature::deactivate(AllowAudioStreams::class);
Feature::deactivate(AllowDumpDownloading::class);
Feature::deactivate(AllowExternalProfileManagement::class);
Feature::deactivate(AllowPlaylistManagement::class);
Feature::deactivate(AllowScriptDownloading::class);
Feature::deactivate(AllowVideoStreams::class);
@@ -16,6 +16,8 @@ use App\Models\Auth\Role;
use App\Models\Auth\User;
use App\Models\Discord\DiscordThread;
use App\Models\Document\Page;
use App\Models\List\External\ExternalEntry;
use App\Models\List\ExternalProfile;
use App\Models\List\Playlist;
use App\Models\List\Playlist\PlaylistTrack;
use App\Models\Wiki\Anime;
@@ -66,6 +68,8 @@ class PermissionSeeder extends Seeder
$this->registerResource(DiscordThread::class, CrudPermission::cases());
// List Resources
$this->registerResource(ExternalEntry::class, $extendedCrudPermissions);
$this->registerResource(ExternalProfile::class, $extendedCrudPermissions);
$this->registerResource(Playlist::class, $extendedCrudPermissions);
$this->registerResource(PlaylistTrack::class, $extendedCrudPermissions);
@@ -17,6 +17,8 @@ use App\Models\Auth\Role;
use App\Models\Auth\User;
use App\Models\Discord\DiscordThread;
use App\Models\Document\Page;
use App\Models\List\External\ExternalEntry;
use App\Models\List\ExternalProfile;
use App\Models\List\Playlist;
use App\Models\List\Playlist\PlaylistTrack;
use App\Models\Wiki\Anime;
@@ -73,6 +75,8 @@ class AdminSeeder extends RoleSeeder
$this->configureResource($role, DiscordThread::class, CrudPermission::cases());
// List Resources
$this->configureResource($role, ExternalEntry::class, $extendedCrudPermissions);
$this->configureResource($role, ExternalProfile::class, $extendedCrudPermissions);
$this->configureResource($role, Playlist::class, $extendedCrudPermissions);
$this->configureResource($role, PlaylistTrack::class, $extendedCrudPermissions);
Loaded 100 of 179 files, more files were not shown because too many files have changed in this diff. Show more