diff --git a/.env.example b/.env.example index 6966ecb4b..dead28979 100644 --- a/.env.example +++ b/.env.example @@ -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= diff --git a/app/Actions/Models/List/ExternalProfile/StoreExternalProfileAction.php b/app/Actions/Models/List/ExternalProfile/StoreExternalProfileAction.php new file mode 100644 index 000000000..8b43293dd --- /dev/null +++ b/app/Actions/Models/List/ExternalProfile/StoreExternalProfileAction.php @@ -0,0 +1,106 @@ +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; + } + } +} diff --git a/app/Constants/Config/ExternalProfileConstants.php b/app/Constants/Config/ExternalProfileConstants.php new file mode 100644 index 000000000..d9104ae08 --- /dev/null +++ b/app/Constants/Config/ExternalProfileConstants.php @@ -0,0 +1,15 @@ +value => ResourceSite::MAL, + static::ANILIST->value => ResourceSite::ANILIST, + static::KITSU->value => ResourceSite::KITSU, + default => null, + }; + } +} diff --git a/app/Enums/Models/List/ExternalProfileVisibility.php b/app/Enums/Models/List/ExternalProfileVisibility.php new file mode 100644 index 000000000..e53d16310 --- /dev/null +++ b/app/Enums/Models/List/ExternalProfileVisibility.php @@ -0,0 +1,18 @@ + + */ +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; + } +} diff --git a/app/Events/Base/List/ListDeletedEvent.php b/app/Events/Base/List/ListDeletedEvent.php new file mode 100644 index 000000000..3abcf69cb --- /dev/null +++ b/app/Events/Base/List/ListDeletedEvent.php @@ -0,0 +1,41 @@ + + */ +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; + } +} diff --git a/app/Events/Base/List/ListRestoredEvent.php b/app/Events/Base/List/ListRestoredEvent.php new file mode 100644 index 000000000..d50ec72ba --- /dev/null +++ b/app/Events/Base/List/ListRestoredEvent.php @@ -0,0 +1,41 @@ + + */ +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; + } +} diff --git a/app/Events/Base/List/ListUpdatedEvent.php b/app/Events/Base/List/ListUpdatedEvent.php new file mode 100644 index 000000000..71dd8163e --- /dev/null +++ b/app/Events/Base/List/ListUpdatedEvent.php @@ -0,0 +1,41 @@ + + */ +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; + } +} diff --git a/app/Events/List/ExternalProfile/ExternalEntry/ExternalEntryCreated.php b/app/Events/List/ExternalProfile/ExternalEntry/ExternalEntryCreated.php new file mode 100644 index 000000000..e81d52179 --- /dev/null +++ b/app/Events/List/ExternalProfile/ExternalEntry/ExternalEntryCreated.php @@ -0,0 +1,67 @@ + + */ +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()}**'."; + } +} diff --git a/app/Events/List/ExternalProfile/ExternalEntry/ExternalEntryDeleted.php b/app/Events/List/ExternalProfile/ExternalEntry/ExternalEntryDeleted.php new file mode 100644 index 000000000..db8af9606 --- /dev/null +++ b/app/Events/List/ExternalProfile/ExternalEntry/ExternalEntryDeleted.php @@ -0,0 +1,67 @@ + + */ +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()}**'."; + } +} diff --git a/app/Events/List/ExternalProfile/ExternalEntry/ExternalEntryRestored.php b/app/Events/List/ExternalProfile/ExternalEntry/ExternalEntryRestored.php new file mode 100644 index 000000000..9170f31f8 --- /dev/null +++ b/app/Events/List/ExternalProfile/ExternalEntry/ExternalEntryRestored.php @@ -0,0 +1,67 @@ + + */ +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()}**'."; + } +} diff --git a/app/Events/List/ExternalProfile/ExternalEntry/ExternalEntryUpdated.php b/app/Events/List/ExternalProfile/ExternalEntry/ExternalEntryUpdated.php new file mode 100644 index 000000000..41f07374c --- /dev/null +++ b/app/Events/List/ExternalProfile/ExternalEntry/ExternalEntryUpdated.php @@ -0,0 +1,68 @@ + + */ +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()}**'."; + } +} diff --git a/app/Events/List/ExternalProfile/ExternalProfileCreated.php b/app/Events/List/ExternalProfile/ExternalProfileCreated.php new file mode 100644 index 000000000..a3bb46eab --- /dev/null +++ b/app/Events/List/ExternalProfile/ExternalProfileCreated.php @@ -0,0 +1,46 @@ + + */ +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."; + } +} diff --git a/app/Events/List/ExternalProfile/ExternalProfileDeleted.php b/app/Events/List/ExternalProfile/ExternalProfileDeleted.php new file mode 100644 index 000000000..314489956 --- /dev/null +++ b/app/Events/List/ExternalProfile/ExternalProfileDeleted.php @@ -0,0 +1,58 @@ + + */ +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."; + } +} diff --git a/app/Events/List/ExternalProfile/ExternalProfileRestored.php b/app/Events/List/ExternalProfile/ExternalProfileRestored.php new file mode 100644 index 000000000..85ed1d0cd --- /dev/null +++ b/app/Events/List/ExternalProfile/ExternalProfileRestored.php @@ -0,0 +1,58 @@ + + */ +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."; + } +} diff --git a/app/Events/List/ExternalProfile/ExternalProfileUpdated.php b/app/Events/List/ExternalProfile/ExternalProfileUpdated.php new file mode 100644 index 000000000..77a00cc5c --- /dev/null +++ b/app/Events/List/ExternalProfile/ExternalProfileUpdated.php @@ -0,0 +1,59 @@ + + */ +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."; + } +} diff --git a/app/Events/List/Playlist/PlaylistCreated.php b/app/Events/List/Playlist/PlaylistCreated.php index ae1ece819..65b83f585 100644 --- a/app/Events/List/Playlist/PlaylistCreated.php +++ b/app/Events/List/Playlist/PlaylistCreated.php @@ -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 + * @extends ListCreatedEvent */ -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. * diff --git a/app/Events/List/Playlist/PlaylistDeleted.php b/app/Events/List/Playlist/PlaylistDeleted.php index df5ae25b8..b1903668e 100644 --- a/app/Events/List/Playlist/PlaylistDeleted.php +++ b/app/Events/List/Playlist/PlaylistDeleted.php @@ -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 + * @extends ListDeletedEvent */ -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. * diff --git a/app/Events/List/Playlist/PlaylistRestored.php b/app/Events/List/Playlist/PlaylistRestored.php index 2ae765e30..78db42d12 100644 --- a/app/Events/List/Playlist/PlaylistRestored.php +++ b/app/Events/List/Playlist/PlaylistRestored.php @@ -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 + * @extends ListRestoredEvent */ -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. * diff --git a/app/Events/List/Playlist/PlaylistUpdated.php b/app/Events/List/Playlist/PlaylistUpdated.php index a3fd284da..9a4f4d7a9 100644 --- a/app/Events/List/Playlist/PlaylistUpdated.php +++ b/app/Events/List/Playlist/PlaylistUpdated.php @@ -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 + * @extends ListUpdatedEvent */ -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. * diff --git a/app/Events/List/Playlist/Track/TrackCreated.php b/app/Events/List/Playlist/Track/TrackCreated.php index f7c6ff451..50dd2485a 100644 --- a/app/Events/List/Playlist/Track/TrackCreated.php +++ b/app/Events/List/Playlist/Track/TrackCreated.php @@ -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 + * @extends ListCreatedEvent */ -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. * diff --git a/app/Events/List/Playlist/Track/TrackDeleted.php b/app/Events/List/Playlist/Track/TrackDeleted.php index 3aa659576..ea9ec1f3b 100644 --- a/app/Events/List/Playlist/Track/TrackDeleted.php +++ b/app/Events/List/Playlist/Track/TrackDeleted.php @@ -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 + * @extends ListDeletedEvent */ -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. * diff --git a/app/Events/List/Playlist/Track/TrackRestored.php b/app/Events/List/Playlist/Track/TrackRestored.php index dae350034..440e9291e 100644 --- a/app/Events/List/Playlist/Track/TrackRestored.php +++ b/app/Events/List/Playlist/Track/TrackRestored.php @@ -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 + * @extends ListRestoredEvent */ -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. * diff --git a/app/Events/List/Playlist/Track/TrackUpdated.php b/app/Events/List/Playlist/Track/TrackUpdated.php index 30e4d03dc..1b863a8a8 100644 --- a/app/Events/List/Playlist/Track/TrackUpdated.php +++ b/app/Events/List/Playlist/Track/TrackUpdated.php @@ -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 + * @extends ListUpdatedEvent */ -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. * diff --git a/app/Features/AllowExternalProfileManagement.php b/app/Features/AllowExternalProfileManagement.php new file mode 100644 index 000000000..696672439 --- /dev/null +++ b/app/Features/AllowExternalProfileManagement.php @@ -0,0 +1,30 @@ +can(SpecialPermission::BYPASS_FEATURE_FLAGS->value))) { + return true; + } + + return Feature::for(null)->value(static::class); + } +} diff --git a/app/Filament/Components/Filters/DateFilter.php b/app/Filament/Components/Filters/DateFilter.php deleted file mode 100644 index 87c4a2e85..000000000 --- a/app/Filament/Components/Filters/DateFilter.php +++ /dev/null @@ -1,71 +0,0 @@ -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), - ); - } -} \ No newline at end of file diff --git a/app/Filament/Components/Filters/NumberFilter.php b/app/Filament/Components/Filters/NumberFilter.php index 96b705c9b..875e9e6c6 100644 --- a/app/Filament/Components/Filters/NumberFilter.php +++ b/app/Filament/Components/Filters/NumberFilter.php @@ -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), ); } diff --git a/app/Filament/Dashboards/AdminDashboard.php b/app/Filament/Dashboards/AdminDashboard.php index 34e75da51..4f27bab62 100644 --- a/app/Filament/Dashboards/AdminDashboard.php +++ b/app/Filament/Dashboards/AdminDashboard.php @@ -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, ]; diff --git a/app/Filament/RelationManagers/List/External/ExternalEntryRelationManager.php b/app/Filament/RelationManagers/List/External/ExternalEntryRelationManager.php new file mode 100644 index 000000000..00d871c7f --- /dev/null +++ b/app/Filament/RelationManagers/List/External/ExternalEntryRelationManager.php @@ -0,0 +1,110 @@ +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(), + ); + } +} diff --git a/app/Filament/RelationManagers/List/ExternalProfileRelationManager.php b/app/Filament/RelationManagers/List/ExternalProfileRelationManager.php new file mode 100644 index 000000000..a3d848dab --- /dev/null +++ b/app/Filament/RelationManagers/List/ExternalProfileRelationManager.php @@ -0,0 +1,110 @@ +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(), + ); + } +} diff --git a/app/Filament/Resources/Admin/ActionLog.php b/app/Filament/Resources/Admin/ActionLog.php index f8ee1294f..0e34978da 100644 --- a/app/Filament/Resources/Admin/ActionLog.php +++ b/app/Filament/Resources/Admin/ActionLog.php @@ -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')), ]; } diff --git a/app/Filament/Resources/Admin/FeaturedTheme.php b/app/Filament/Resources/Admin/FeaturedTheme.php index 832db11c8..4bcc9869d 100644 --- a/app/Filament/Resources/Admin/FeaturedTheme.php +++ b/app/Filament/Resources/Admin/FeaturedTheme.php @@ -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) diff --git a/app/Filament/Resources/Auth/Role.php b/app/Filament/Resources/Auth/Role.php index a44122f24..3383ca7a8 100644 --- a/app/Filament/Resources/Auth/Role.php +++ b/app/Filament/Resources/Auth/Role.php @@ -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')), ]; } diff --git a/app/Filament/Resources/BaseResource.php b/app/Filament/Resources/BaseResource.php index 2baede0c2..bdd0d9c50 100644 --- a/app/Filament/Resources/BaseResource.php +++ b/app/Filament/Resources/BaseResource.php @@ -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')), ]; } diff --git a/app/Filament/Resources/List/External/ExternalEntry.php b/app/Filament/Resources/List/External/ExternalEntry.php new file mode 100644 index 000000000..a02b09d83 --- /dev/null +++ b/app/Filament/Resources/List/External/ExternalEntry.php @@ -0,0 +1,349 @@ +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'), + ]; + } +} diff --git a/app/Filament/Resources/List/External/ExternalEntry/Pages/CreateExternalEntry.php b/app/Filament/Resources/List/External/ExternalEntry/Pages/CreateExternalEntry.php new file mode 100644 index 000000000..4aa4b273e --- /dev/null +++ b/app/Filament/Resources/List/External/ExternalEntry/Pages/CreateExternalEntry.php @@ -0,0 +1,16 @@ +applyColumnSearchesToTableQuery($query); + + if (filled($search = $this->getTableSearch())) { + $query->whereIn(ExternalProfileModel::ATTRIBUTE_ID, ExternalProfileModel::search($search)->take(25)->keys()); + } + + return $query; + } +} diff --git a/app/Filament/Resources/List/External/Pages/ViewExternalProfile.php b/app/Filament/Resources/List/External/Pages/ViewExternalProfile.php new file mode 100644 index 000000000..a5db31dc5 --- /dev/null +++ b/app/Filament/Resources/List/External/Pages/ViewExternalProfile.php @@ -0,0 +1,31 @@ +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(), + [], + ); + } +} diff --git a/app/Filament/Resources/List/ExternalProfile.php b/app/Filament/Resources/List/ExternalProfile.php new file mode 100644 index 000000000..9340e35f7 --- /dev/null +++ b/app/Filament/Resources/List/ExternalProfile.php @@ -0,0 +1,346 @@ +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'), + ]; + } +} diff --git a/app/Filament/Resources/List/Playlist.php b/app/Filament/Resources/List/Playlist.php index c42b5fb36..daeb475d3 100644 --- a/app/Filament/Resources/List/Playlist.php +++ b/app/Filament/Resources/List/Playlist.php @@ -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')) diff --git a/app/Filament/Resources/Wiki/Anime.php b/app/Filament/Resources/Wiki/Anime.php index 6be8f8c3f..4d4fecb6e 100644 --- a/app/Filament/Resources/Wiki/Anime.php +++ b/app/Filament/Resources/Wiki/Anime.php @@ -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')) diff --git a/app/Filament/Resources/Wiki/Anime/Theme.php b/app/Filament/Resources/Wiki/Anime/Theme.php index 2d7d56e42..a7b163002 100644 --- a/app/Filament/Resources/Wiki/Anime/Theme.php +++ b/app/Filament/Resources/Wiki/Anime/Theme.php @@ -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(), ); diff --git a/app/Filament/Resources/Wiki/Anime/Theme/Entry.php b/app/Filament/Resources/Wiki/Anime/Theme/Entry.php index 7d65a3641..7416340c8 100644 --- a/app/Filament/Resources/Wiki/Anime/Theme/Entry.php +++ b/app/Filament/Resources/Wiki/Anime/Theme/Entry.php @@ -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')), diff --git a/app/Filament/Resources/Wiki/Audio.php b/app/Filament/Resources/Wiki/Audio.php index c34f9e934..e526c477f 100644 --- a/app/Filament/Resources/Wiki/Audio.php +++ b/app/Filament/Resources/Wiki/Audio.php @@ -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(), ); diff --git a/app/Filament/Resources/Wiki/ExternalResource.php b/app/Filament/Resources/Wiki/ExternalResource.php index e571b7c09..78e0f842a 100644 --- a/app/Filament/Resources/Wiki/ExternalResource.php +++ b/app/Filament/Resources/Wiki/ExternalResource.php @@ -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(), ); diff --git a/app/Filament/Resources/Wiki/Video.php b/app/Filament/Resources/Wiki/Video.php index efdd7bbea..069a58549 100644 --- a/app/Filament/Resources/Wiki/Video.php +++ b/app/Filament/Resources/Wiki/Video.php @@ -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(), ); diff --git a/app/Filament/Widgets/List/ExternalProfileChart.php b/app/Filament/Widgets/List/ExternalProfileChart.php new file mode 100644 index 000000000..6f1d00aa0 --- /dev/null +++ b/app/Filament/Widgets/List/ExternalProfileChart.php @@ -0,0 +1,78 @@ +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, + ], + ]; + } +} diff --git a/app/Http/Api/Field/List/ExternalProfile/ExternalEntry/ExternalEntryAnimeIdField.php b/app/Http/Api/Field/List/ExternalProfile/ExternalEntry/ExternalEntryAnimeIdField.php new file mode 100644 index 000000000..e5d25d65d --- /dev/null +++ b/app/Http/Api/Field/List/ExternalProfile/ExternalEntry/ExternalEntryAnimeIdField.php @@ -0,0 +1,53 @@ +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; + } +} \ No newline at end of file diff --git a/app/Http/Api/Field/List/ExternalProfile/ExternalEntry/ExternalEntryIsFavoriteField.php b/app/Http/Api/Field/List/ExternalProfile/ExternalEntry/ExternalEntryIsFavoriteField.php new file mode 100644 index 000000000..1086037a3 --- /dev/null +++ b/app/Http/Api/Field/List/ExternalProfile/ExternalEntry/ExternalEntryIsFavoriteField.php @@ -0,0 +1,57 @@ +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; + } +} \ No newline at end of file diff --git a/app/Http/Api/Field/List/ExternalProfile/ExternalProfileNameField.php b/app/Http/Api/Field/List/ExternalProfile/ExternalProfileNameField.php new file mode 100644 index 000000000..ebba94cd7 --- /dev/null +++ b/app/Http/Api/Field/List/ExternalProfile/ExternalProfileNameField.php @@ -0,0 +1,62 @@ +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(); + } +} diff --git a/app/Http/Controllers/Api/List/External/ExternalEntryController.php b/app/Http/Controllers/Api/List/External/ExternalEntryController.php new file mode 100644 index 000000000..a70f916f0 --- /dev/null +++ b/app/Http/Controllers/Api/List/External/ExternalEntryController.php @@ -0,0 +1,172 @@ +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, + ]); + } +} \ No newline at end of file diff --git a/app/Http/Controllers/Api/List/ExternalProfileController.php b/app/Http/Controllers/Api/List/ExternalProfileController.php new file mode 100644 index 000000000..534ec1ebd --- /dev/null +++ b/app/Http/Controllers/Api/List/ExternalProfileController.php @@ -0,0 +1,168 @@ +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, + ]); + } +} \ No newline at end of file diff --git a/app/Http/Middleware/Api/EnabledOnlyOnLocalhost.php b/app/Http/Middleware/Api/EnabledOnlyOnLocalhost.php new file mode 100644 index 000000000..8a97b3587 --- /dev/null +++ b/app/Http/Middleware/Api/EnabledOnlyOnLocalhost.php @@ -0,0 +1,32 @@ +ip(); + + if ($ip !== '127.0.0.1') { + abort(403, "Route only available for localhost"); + } + + return $next($request); + } +} diff --git a/app/Http/Middleware/Models/List/ExternalProfileExceedsEntryLimit.php b/app/Http/Middleware/Models/List/ExternalProfileExceedsEntryLimit.php new file mode 100644 index 000000000..5c8b7c680 --- /dev/null +++ b/app/Http/Middleware/Models/List/ExternalProfileExceedsEntryLimit.php @@ -0,0 +1,46 @@ +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); + } +} diff --git a/app/Http/Middleware/Models/List/UserExceedsExternalProfileLimit.php b/app/Http/Middleware/Models/List/UserExceedsExternalProfileLimit.php new file mode 100644 index 000000000..e4b111494 --- /dev/null +++ b/app/Http/Middleware/Models/List/UserExceedsExternalProfileLimit.php @@ -0,0 +1,42 @@ +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); + } +} diff --git a/app/Http/Resources/List/Collection/ExternalProfileCollection.php b/app/Http/Resources/List/Collection/ExternalProfileCollection.php new file mode 100644 index 000000000..58a508073 --- /dev/null +++ b/app/Http/Resources/List/Collection/ExternalProfileCollection.php @@ -0,0 +1,36 @@ +collection->map(fn (ExternalProfile $profile) => new ExternalProfileResource($profile, $this->query))->all(); + } +} diff --git a/app/Http/Resources/List/External/Collection/ExternalEntryCollection.php b/app/Http/Resources/List/External/Collection/ExternalEntryCollection.php new file mode 100644 index 000000000..bbc4677e9 --- /dev/null +++ b/app/Http/Resources/List/External/Collection/ExternalEntryCollection.php @@ -0,0 +1,36 @@ +collection->map(fn (ExternalEntry $entry) => new ExternalEntryResource($entry, $this->query))->all(); + } +} diff --git a/app/Http/Resources/List/External/Resource/ExternalEntryResource.php b/app/Http/Resources/List/External/Resource/ExternalEntryResource.php new file mode 100644 index 000000000..6361b93b4 --- /dev/null +++ b/app/Http/Resources/List/External/Resource/ExternalEntryResource.php @@ -0,0 +1,32 @@ + $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); + } } diff --git a/app/Models/List/External/ExternalEntry.php b/app/Models/List/External/ExternalEntry.php new file mode 100644 index 000000000..74d5f53a4 --- /dev/null +++ b/app/Models/List/External/ExternalEntry.php @@ -0,0 +1,152 @@ + + */ + 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 + */ + 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); + } +} diff --git a/app/Models/List/ExternalProfile.php b/app/Models/List/ExternalProfile.php new file mode 100644 index 000000000..8f13b4f0c --- /dev/null +++ b/app/Models/List/ExternalProfile.php @@ -0,0 +1,172 @@ + $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 + */ + 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 + */ + 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(); + } +} diff --git a/app/Models/Wiki/Anime.php b/app/Models/Wiki/Anime.php index 3767e6235..df14700a6 100644 --- a/app/Models/Wiki/Anime.php +++ b/app/Models/Wiki/Anime.php @@ -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 $animesynonyms * @property Collection $animethemes * @property DiscordThread|null $discordthread + * @property Collection $externalentries * @property Collection $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); + } } diff --git a/app/Nova/Resources/Auth/User.php b/app/Nova/Resources/Auth/User.php index 0a1e4759c..b4f45af4e 100644 --- a/app/Nova/Resources/Auth/User.php +++ b/app/Nova/Resources/Auth/User.php @@ -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(), ]; diff --git a/app/Nova/Resources/List/External/ExternalEntry.php b/app/Nova/Resources/List/External/ExternalEntry.php new file mode 100644 index 000000000..c9a39820a --- /dev/null +++ b/app/Nova/Resources/List/External/ExternalEntry.php @@ -0,0 +1,192 @@ +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(), + ]; + } +} diff --git a/app/Nova/Resources/List/ExternalProfile.php b/app/Nova/Resources/List/ExternalProfile.php new file mode 100644 index 000000000..c75f15018 --- /dev/null +++ b/app/Nova/Resources/List/ExternalProfile.php @@ -0,0 +1,202 @@ +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(), + ]; + } +} diff --git a/app/Policies/List/External/ExternalEntryPolicy.php b/app/Policies/List/External/ExternalEntryPolicy.php new file mode 100644 index 000000000..b260364c6 --- /dev/null +++ b/app/Policies/List/External/ExternalEntryPolicy.php @@ -0,0 +1,189 @@ +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)); + } +} diff --git a/app/Policies/List/ExternalProfilePolicy.php b/app/Policies/List/ExternalProfilePolicy.php new file mode 100644 index 000000000..46df762b0 --- /dev/null +++ b/app/Policies/List/ExternalProfilePolicy.php @@ -0,0 +1,158 @@ +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'); + } +} diff --git a/app/Policies/Wiki/AnimePolicy.php b/app/Policies/Wiki/AnimePolicy.php index 1944be220..1ce91077d 100644 --- a/app/Policies/Wiki/AnimePolicy.php +++ b/app/Policies/Wiki/AnimePolicy.php @@ -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'); + } } diff --git a/app/Rules/Admin/StartDateBeforeEndDateRule.php b/app/Rules/Admin/StartDateBeforeEndDateRule.php new file mode 100644 index 000000000..9d3bb7624 --- /dev/null +++ b/app/Rules/Admin/StartDateBeforeEndDateRule.php @@ -0,0 +1,39 @@ +lessThan($startAt)) { + $fail(__('validation.date', ['attribute' => $attribute])); + } + } +} diff --git a/composer.json b/composer.json index 1e0a1aaf9..670536b29 100644 --- a/composer.json +++ b/composer.json @@ -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", diff --git a/composer.lock b/composer.lock index fba63c09c..91f3d0938 100644 --- a/composer.lock +++ b/composer.lock @@ -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", diff --git a/config/externalprofile.php b/config/externalprofile.php new file mode 100644 index 000000000..5d60615ca --- /dev/null +++ b/config/externalprofile.php @@ -0,0 +1,21 @@ + (int) env('PROFILE_MAX_ENTRIES', 1000), + + 'user_max_profiles' => (int) env('USER_MAX_PROFILES', 5), +]; diff --git a/database/factories/List/External/ExternalEntryFactory.php b/database/factories/List/External/ExternalEntryFactory.php new file mode 100644 index 000000000..e4449acd1 --- /dev/null +++ b/database/factories/List/External/ExternalEntryFactory.php @@ -0,0 +1,38 @@ + + */ +class ExternalEntryFactory extends Factory +{ + /** + * The name of the factory's corresponding model. + * + * @var class-string + */ + protected $model = ExternalEntry::class; + + /** + * Define the model's default state. + * + * @return array + */ + public function definition(): array + { + return [ + // + ]; + } +} diff --git a/database/factories/List/ExternalProfileFactory.php b/database/factories/List/ExternalProfileFactory.php new file mode 100644 index 000000000..66ae7692d --- /dev/null +++ b/database/factories/List/ExternalProfileFactory.php @@ -0,0 +1,99 @@ + + */ +class ExternalProfileFactory extends Factory +{ + /** + * The name of the factory's corresponding model. + * + * @var class-string + */ + 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(); + } + } + ); + } +} diff --git a/database/migrations/2024_03_10_181149_create_external_profile_table.php b/database/migrations/2024_03_10_181149_create_external_profile_table.php new file mode 100644 index 000000000..ebc257d52 --- /dev/null +++ b/database/migrations/2024_03_10_181149_create_external_profile_table.php @@ -0,0 +1,42 @@ +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); + } +}; diff --git a/database/migrations/2024_03_10_181158_create_external_entry_table.php b/database/migrations/2024_03_10_181158_create_external_entry_table.php new file mode 100644 index 000000000..c0284d60d --- /dev/null +++ b/database/migrations/2024_03_10_181158_create_external_entry_table.php @@ -0,0 +1,45 @@ +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); + } +}; diff --git a/database/seeders/Admin/Feature/FeatureSeeder.php b/database/seeders/Admin/Feature/FeatureSeeder.php index 43faeccd3..8f81438c2 100644 --- a/database/seeders/Admin/Feature/FeatureSeeder.php +++ b/database/seeders/Admin/Feature/FeatureSeeder.php @@ -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); diff --git a/database/seeders/Auth/Permission/PermissionSeeder.php b/database/seeders/Auth/Permission/PermissionSeeder.php index aa70f9897..7a7b3da6b 100644 --- a/database/seeders/Auth/Permission/PermissionSeeder.php +++ b/database/seeders/Auth/Permission/PermissionSeeder.php @@ -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); diff --git a/database/seeders/Auth/Role/AdminSeeder.php b/database/seeders/Auth/Role/AdminSeeder.php index 0822d0f49..e873f1b00 100644 --- a/database/seeders/Auth/Role/AdminSeeder.php +++ b/database/seeders/Auth/Role/AdminSeeder.php @@ -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); diff --git a/database/seeders/Auth/Role/DeveloperRoleSeeder.php b/database/seeders/Auth/Role/DeveloperRoleSeeder.php index d594c5bce..6b75627f8 100644 --- a/database/seeders/Auth/Role/DeveloperRoleSeeder.php +++ b/database/seeders/Auth/Role/DeveloperRoleSeeder.php @@ -11,6 +11,8 @@ use App\Enums\Auth\SpecialPermission; use App\Models\Auth\Role; 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; @@ -56,6 +58,8 @@ class DeveloperRoleSeeder extends RoleSeeder $this->configureResource($role, DiscordThread::class, [CrudPermission::VIEW]); // 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); diff --git a/database/seeders/Auth/Role/EncoderRoleSeeder.php b/database/seeders/Auth/Role/EncoderRoleSeeder.php index 26c596d4a..770eb6b35 100644 --- a/database/seeders/Auth/Role/EncoderRoleSeeder.php +++ b/database/seeders/Auth/Role/EncoderRoleSeeder.php @@ -11,6 +11,8 @@ use App\Enums\Auth\SpecialPermission; use App\Models\Auth\Role; 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; @@ -56,6 +58,8 @@ class EncoderRoleSeeder extends RoleSeeder $this->configureResource($role, DiscordThread::class, [CrudPermission::CREATE, CrudPermission::UPDATE, CrudPermission::VIEW]); // 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); diff --git a/database/seeders/Auth/Role/PatronRoleSeeder.php b/database/seeders/Auth/Role/PatronRoleSeeder.php index e11350311..fa93be080 100644 --- a/database/seeders/Auth/Role/PatronRoleSeeder.php +++ b/database/seeders/Auth/Role/PatronRoleSeeder.php @@ -11,6 +11,8 @@ use App\Enums\Auth\SpecialPermission; use App\Models\Auth\Role; 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; @@ -56,6 +58,8 @@ class PatronRoleSeeder extends RoleSeeder $this->configureResource($role, DiscordThread::class, [CrudPermission::VIEW]); // 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); diff --git a/database/seeders/Auth/Role/PlaylistUserRoleSeeder.php b/database/seeders/Auth/Role/PlaylistUserRoleSeeder.php index 68dfbc00d..d633cd6c5 100644 --- a/database/seeders/Auth/Role/PlaylistUserRoleSeeder.php +++ b/database/seeders/Auth/Role/PlaylistUserRoleSeeder.php @@ -8,6 +8,8 @@ use App\Enums\Auth\CrudPermission; use App\Enums\Auth\ExtendedCrudPermission; use App\Enums\Auth\Role as RoleEnum; use App\Models\Auth\Role; +use App\Models\List\External\ExternalEntry; +use App\Models\List\ExternalProfile; use App\Models\List\Playlist; use App\Models\List\Playlist\PlaylistTrack; @@ -36,6 +38,8 @@ class PlaylistUserRoleSeeder extends RoleSeeder ); // 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); diff --git a/database/seeders/Auth/Role/WikiEditorRoleSeeder.php b/database/seeders/Auth/Role/WikiEditorRoleSeeder.php index 9edf6cb49..662706c51 100644 --- a/database/seeders/Auth/Role/WikiEditorRoleSeeder.php +++ b/database/seeders/Auth/Role/WikiEditorRoleSeeder.php @@ -11,6 +11,8 @@ use App\Enums\Auth\SpecialPermission; use App\Models\Auth\Role; 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; @@ -56,6 +58,8 @@ class WikiEditorRoleSeeder extends RoleSeeder $this->configureResource($role, DiscordThread::class, [CrudPermission::VIEW]); // 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); diff --git a/database/seeders/Auth/Role/WikiViewerRoleSeeder.php b/database/seeders/Auth/Role/WikiViewerRoleSeeder.php index e06a94d6b..c0e263f7f 100644 --- a/database/seeders/Auth/Role/WikiViewerRoleSeeder.php +++ b/database/seeders/Auth/Role/WikiViewerRoleSeeder.php @@ -11,6 +11,8 @@ use App\Enums\Auth\SpecialPermission; use App\Models\Auth\Role; 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; @@ -56,6 +58,8 @@ class WikiViewerRoleSeeder extends RoleSeeder $this->configureResource($role, DiscordThread::class, [CrudPermission::VIEW]); // 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); diff --git a/lang/en/enums.php b/lang/en/enums.php index bd9be3812..e334de3a6 100644 --- a/lang/en/enums.php +++ b/lang/en/enums.php @@ -3,7 +3,10 @@ declare(strict_types=1); use App\Enums\Actions\ActionLogStatus; +use App\Enums\Models\List\AnimeWatchStatus; +use App\Enums\Models\List\ExternalProfileSite; use App\Enums\Models\List\PlaylistVisibility; +use App\Enums\Models\List\ExternalProfileVisibility; use App\Enums\Models\Wiki\AnimeMediaFormat; use App\Enums\Models\Wiki\AnimeSeason; use App\Enums\Models\Wiki\AnimeSynonymType; @@ -40,6 +43,22 @@ return [ ActionLogStatus::FAILED->name => 'Failed', ActionLogStatus::FINISHED->name => 'Finished', ], + AnimeWatchStatus::class => [ + AnimeWatchStatus::WATCHING->name => 'Watching', + AnimeWatchStatus::COMPLETED->name => 'Completed', + AnimeWatchStatus::PAUSED->name => 'Paused', + AnimeWatchStatus::DROPPED->name => 'Dropped', + AnimeWatchStatus::PLAN_TO_WATCH->name => 'Plan to Watch', + ], + ExternalProfileSite::class => [ + ExternalProfileSite::MAL->name => 'MyAnimeList', + ExternalProfileSite::ANILIST->name => 'AniList', + ExternalProfileSite::KITSU->name => 'Kitsu', + ], + ExternalProfileVisibility::class => [ + ExternalProfileVisibility::PUBLIC->name => 'Public', + ExternalProfileVisibility::PRIVATE->name => 'Private', + ], ImageFacet::class => [ ImageFacet::COVER_SMALL->name => 'Small Cover', ImageFacet::COVER_LARGE->name => 'Large Cover', diff --git a/lang/en/filament.php b/lang/en/filament.php index 5f9376f39..0b91a4f57 100644 --- a/lang/en/filament.php +++ b/lang/en/filament.php @@ -608,6 +608,34 @@ return [ 'dump' => [ 'path' => 'Path', ], + 'external_entry' => [ + 'is_favorite' => [ + 'help' => 'If the user has the entry favorited.', + 'name' => 'Is Favorite', + ], + 'score' => [ + 'help' => 'The anime score of the user.', + 'name' => 'Score', + ], + 'watch_status' => [ + 'help' => 'The watch status of the anime.', + 'name' => 'Watch Status', + ], + ], + 'external_profile' => [ + 'name' => [ + 'help' => 'The name of the user of the external site.', + 'name' => 'Name', + ], + 'site' => [ + 'help' => 'The site that owns the external list.', + 'name' => 'Site', + ], + 'visibility' => [ + 'help' => 'The visibility of the user profile.', + 'name' => 'Visibility', + ], + ], 'external_resource' => [ 'external_id' => [ 'help' => 'The identifier used by the external site.', @@ -633,14 +661,12 @@ return [ ], ], 'featured_theme' => [ - 'end_at' => [ - 'help' => 'The datetime that the featured theme should stop being featured.', - 'name' => 'End At', - ], - 'start_at' => [ - 'help' => 'The datetime that the featured theme should start being featured.', - 'name' => 'Start At', + 'date' => [ + 'help' => 'The range of days that the featured theme should remain.', + 'name' => 'Date Range', ], + 'end_at' => 'End At', + 'start_at' => 'Start At', ], 'group' => [ 'name' => [ @@ -833,49 +859,9 @@ return [ ], ], 'filters' => [ - 'action_log' => [ - 'finished_at_from' => 'Finished At - From', - 'finished_at_to' => 'Finished At - To', - 'happened_at_from' => 'Happened At - From', - 'happened_at_to' => 'Happened At - To', - ], - 'anime' => [ - 'year_from' => 'Year - From', - 'year_to' => 'Year - To', - ], - 'anime_theme' => [ - 'sequence_from' => 'Sequence - From', - 'sequence_to' => 'Sequence - To', - ], - 'anime_theme_entry' => [ - 'version_from' => 'Version - From', - 'version_to' => 'Version - To', - ], - 'audio' => [ - 'size_from' => 'Size - From', - 'size_to' => 'Size - To' - ], 'base' => [ - 'created_at_from' => 'Created At - From', - 'created_at_to' => 'Created At - To', - 'updated_at_from' => 'Updated At - From', - 'updated_at_to' => 'Updated At - To', - 'deleted_at_from' => 'Deleted At - From', - 'deleted_at_to' => 'Deleted At - To', - ], - 'external_resource' => [ - 'external_id_from' => 'External ID - From', - 'external_id_to' => 'External ID - To', - ], - 'role' => [ - 'priority_from' => 'Priority - From', - 'priority_to' => 'Priority - To', - ], - 'video' => [ - 'resolution_from' => 'Resolution - From', - 'resolution_to' => 'Resolution - To', - 'size_from' => 'Size - From', - 'size_to' => 'Size - To' + 'from' => 'From', + 'to' => 'To', ], ], 'resources' => [ @@ -898,6 +884,8 @@ return [ 'audios' => 'heroicon-o-speaker-wave', 'discord_thread' => 'heroicon-o-chat-bubble-left-right', 'dumps' => 'heroicon-o-circle-stack', + 'external_entries' => 'heroicon-o-list-bullet', + 'external_profiles' => 'heroicon-o-list-bullet', 'external_resources' => 'heroicon-o-arrow-top-right-on-square', 'features' => 'heroicon-o-cog-6-tooth', 'featured_themes' => 'heroicon-o-calendar-days', @@ -927,6 +915,8 @@ return [ 'audios' => 'Audios', 'discord_threads' => 'Threads', 'dumps' => 'Dumps', + 'external_entries' => 'External Entries', + 'external_profiles' => 'External Profiles', 'external_resources' => 'External Resources', 'features' => 'Features', 'featured_themes' => 'Featured Themes', @@ -956,6 +946,8 @@ return [ 'audio' => 'Audio', 'discord_thread' => 'Thread', 'dump' => 'Dump', + 'external_entry' => 'External Entry', + 'external_profile' => 'External Profile', 'external_resource' => 'External Resource', 'feature' => 'Feature', 'featured_theme' => 'Featured Theme', diff --git a/lang/en/nova.php b/lang/en/nova.php index 08f909b36..245d45e45 100644 --- a/lang/en/nova.php +++ b/lang/en/nova.php @@ -537,6 +537,34 @@ return [ 'dump' => [ 'path' => 'Path', ], + 'externalentry' => [ + 'is_favorite' => [ + 'help' => 'If the user has the entry favorited.', + 'name' => 'Is Favorite', + ], + 'score' => [ + 'help' => 'The anime score of the user.', + 'name' => 'Score', + ], + 'watch_status' => [ + 'help' => 'The watch status of the anime.', + 'name' => 'Watch Status', + ], + ], + 'externalprofile' => [ + 'name' => [ + 'help' => 'The name of the user of the external site.', + 'name' => 'Name', + ], + 'site' => [ + 'help' => 'The site that owns the external list.', + 'name' => 'Site', + ], + 'visibility' => [ + 'help' => 'The visibility of the user profile.', + 'name' => 'Visibility', + ], + ], 'external_resource' => [ 'external_id' => [ 'help' => 'The identifier used by the external site.', @@ -877,6 +905,8 @@ return [ 'artists' => 'Artists', 'audios' => 'Audios', 'dumps' => 'Dumps', + 'externalentries' => 'External Entries', + 'externalprofiles' => 'External Profiles', 'external_resources' => 'External Resources', 'features' => 'Features', 'featured_themes' => 'Featured Themes', @@ -905,6 +935,8 @@ return [ 'artist' => 'Artist', 'audio' => 'Audio', 'dump' => 'Dump', + 'externalentries' => 'External Entry', + 'externalprofiles' => 'External Profile', 'external_resource' => 'External Resource', 'feature' => 'Feature', 'featured_theme' => 'Featured Theme', diff --git a/public/css/filament-daterangepicker-filter/filament-daterangepicker-filter2.7.3.0.css b/public/css/filament-daterangepicker-filter/filament-daterangepicker-filter2.7.3.0.css new file mode 100644 index 000000000..46580b71a --- /dev/null +++ b/public/css/filament-daterangepicker-filter/filament-daterangepicker-filter2.7.3.0.css @@ -0,0 +1 @@ +.relative{position:relative}.inline-block{display:inline-block}.w-full{width:100%}.overflow-hidden{overflow:hidden}.bg-\[\#ebf4f8\]{--tw-bg-opacity:1;background-color:rgb(235 244 248/var(--tw-bg-opacity))}.bg-white{--tw-bg-opacity:1;background-color:rgb(255 255 255/var(--tw-bg-opacity))}.text-gray-600{--tw-text-opacity:1;color:rgb(75 85 99/var(--tw-text-opacity))}.container{width:100%}@media (min-width:640px){.container{max-width:640px}}@media (min-width:768px){.container{max-width:768px}}@media (min-width:1024px){.container{max-width:1024px}}@media (min-width:1280px){.container{max-width:1280px}}@media (min-width:1536px){.container{max-width:1536px}}.daterangepicker{color:inherit;position:absolute}.daterangepicker:is(.dark *){--tw-bg-opacity:1;--tw-border-opacity:1;background-color:rgb(31 41 55/var(--tw-bg-opacity));border-color:rgb(55 65 81/var(--tw-border-opacity))}.daterangepicker{--tw-bg-opacity:1;background-color:rgb(255 255 255/var(--tw-bg-opacity));border:1px solid #ddd;border-radius:4px;display:none;font-family:inherit;font-size:15px;left:20px;line-height:1em;margin-top:7px;max-width:none;padding:0;top:100px;width:278px;z-index:3001}.daterangepicker .applyBtn{--tw-text-opacity:1;color:rgb(37 99 235/var(--tw-text-opacity))}.daterangepicker .applyBtn:hover{--tw-text-opacity:1;color:rgb(96 165 250/var(--tw-text-opacity))}.daterangepicker:after,.daterangepicker:before{border-bottom-color:rgba(0,0,0,.2);content:"";display:inline-block;position:absolute}.daterangepicker .ampmselect:is(.dark *),.daterangepicker .hourselect:is(.dark *),.daterangepicker .minuteselect:is(.dark *),.daterangepicker .secondselect:is(.dark *){--tw-border-opacity:1;background-color:transparent;border-color:rgb(55 65 81/var(--tw-border-opacity))}.daterangepicker .ampmselect,.daterangepicker .hourselect,.daterangepicker .minuteselect,.daterangepicker .secondselect{--tw-bg-opacity:1;background-color:rgb(255 255 255/var(--tw-bg-opacity))}.daterangepicker .ampmselect>option:is(.dark *),.daterangepicker .hourselect>option:is(.dark *),.daterangepicker .minuteselect>option:is(.dark *),.daterangepicker .secondselect>option:is(.dark *){--tw-bg-opacity:1;--tw-border-opacity:1;background-color:rgb(31 41 55/var(--tw-bg-opacity));border-color:rgb(55 65 81/var(--tw-border-opacity))}.daterangepicker .ampmselect>option,.daterangepicker .hourselect>option,.daterangepicker .minuteselect>option,.daterangepicker .secondselect>option{--tw-bg-opacity:1;background-color:rgb(255 255 255/var(--tw-bg-opacity))}.daterangepicker:before{border-bottom:7px solid #ccc;border-left:7px solid transparent;border-right:7px solid transparent;top:-7px}.daterangepicker:after{border-bottom:6px solid #fff;border-left:6px solid transparent;border-right:6px solid transparent;top:-6px}.daterangepicker.opensleft:before{right:9px}.daterangepicker.opensleft:after{right:10px}.daterangepicker.openscenter:after,.daterangepicker.openscenter:before{left:0;margin-left:auto;margin-right:auto;right:0;width:0}.daterangepicker.opensright:before{left:9px}.daterangepicker.opensright:after{left:10px}.daterangepicker.drop-up{margin-top:-7px}.daterangepicker.drop-up:before{border-bottom:initial;border-top:7px solid #ccc;bottom:-7px;top:auto}.daterangepicker.drop-up:after{border-bottom:initial;border-top:6px solid #fff;bottom:-6px;top:auto}.daterangepicker.single .daterangepicker .ranges,.daterangepicker.single .drp-calendar{float:none}.daterangepicker.single .drp-selected{display:none}.daterangepicker.show-calendar .drp-buttons,.daterangepicker.show-calendar .drp-calendar{display:block}.daterangepicker.auto-apply .drp-buttons{display:none}.daterangepicker .drp-calendar{display:none;max-width:270px}.daterangepicker .drp-calendar.left{padding:8px 0 8px 8px}.daterangepicker .drp-calendar.right{padding:8px}.daterangepicker .drp-calendar.single .calendar-table{border:none}.daterangepicker .calendar-table .next span,.daterangepicker .calendar-table .prev span{border:solid #000;border-radius:0;border-width:0 2px 2px 0;color:#fff;display:inline-block;padding:3px}.daterangepicker .calendar-table .next span{transform:rotate(-45deg);-webkit-transform:rotate(-45deg)}.daterangepicker .calendar-table .prev span{transform:rotate(135deg);-webkit-transform:rotate(135deg)}.daterangepicker .calendar-table td,.daterangepicker .calendar-table th{border:1px solid transparent;border-radius:4px;cursor:pointer;font-size:12px;height:24px;line-height:24px;min-width:32px;text-align:center;vertical-align:middle;white-space:nowrap;width:32px}.daterangepicker .calendar-table{background-color:#fff;border:1px solid #fff;border-radius:4px}.daterangepicker .calendar-table:is(.dark *){--tw-bg-opacity:1;--tw-border-opacity:1;background-color:rgb(31 41 55/var(--tw-bg-opacity));border-color:rgb(31 41 55/var(--tw-border-opacity))}.daterangepicker .calendar-table table{border-collapse:collapse;border-spacing:0;margin:0;width:100%}.daterangepicker td.available:hover,.daterangepicker th.available:hover{background-color:#eee}.daterangepicker td.available:hover:is(.dark *),.daterangepicker th.available:hover:is(.dark *){--tw-bg-opacity:1;background-color:rgb(107 114 128/var(--tw-bg-opacity))}.daterangepicker td.available:hover,.daterangepicker th.available:hover{border-color:transparent;color:inherit}.next.available span:is(.dark *),.prev.available span:is(.dark *){--tw-border-opacity:1!important;border-color:rgb(255 255 255/var(--tw-border-opacity))!important}.daterangepicker td.week,.daterangepicker th.week{color:#ccc;font-size:80%}.daterangepicker td.off.end-date:is(.dark *),.daterangepicker td.off.start-date:is(.dark *){--tw-bg-opacity:1;background-color:rgb(31 41 55/var(--tw-bg-opacity))}.daterangepicker td.off.end-date,.daterangepicker td.off.start-date{border-color:transparent;color:#fff}.daterangepicker td.in-range{border-color:transparent;border-radius:0;color:#000}.daterangepicker td.in-range:hover{color:#fff}.daterangepicker td.start-date{border-radius:4px 0 0 4px}.daterangepicker td.start-date.active{--tw-bg-opacity:1;--tw-text-opacity:1;background-color:rgb(59 130 246/var(--tw-bg-opacity));color:rgb(255 255 255/var(--tw-text-opacity))}.daterangepicker td.in-range{--tw-bg-opacity:1;background-color:rgb(235 244 248/var(--tw-bg-opacity))}.daterangepicker td.in-range:is(.dark *){--tw-bg-opacity:1;background-color:rgb(255 255 255/var(--tw-bg-opacity))}.daterangepicker td.end-date.active{--tw-bg-opacity:1;background-color:rgb(59 130 246/var(--tw-bg-opacity))}.daterangepicker td.end-date.active.off,.daterangepicker td.off,.daterangepicker td.off.in-range,.daterangepicker td.start-date.active.off{--tw-text-opacity:1;background-color:transparent;color:rgb(153 153 153/var(--tw-text-opacity))}.daterangepicker td.end-date.active.off:is(.dark *),.daterangepicker td.off.in-range:is(.dark *),.daterangepicker td.off:is(.dark *),.daterangepicker td.start-date.active.off:is(.dark *){--tw-bg-opacity:1;background-color:rgb(31 41 55/var(--tw-bg-opacity))}.daterangepicker td.end-date{border-radius:0 4px 4px 0;color:#fff}.daterangepicker td.start-date.end-date{border-radius:4px}.daterangepicker td.active,.daterangepicker td.active:hover{border-color:transparent}.daterangepicker th.month{width:auto}.daterangepicker option.disabled,.daterangepicker td.disabled{color:#999;cursor:not-allowed;text-decoration:line-through}.daterangepicker select.monthselect,.daterangepicker select.yearselect{cursor:default;font-size:12px;height:auto;margin:0;padding:1px}.daterangepicker select.monthselect{margin-right:2%;width:56%}.daterangepicker select.yearselect{width:40%}.daterangepicker select.ampmselect,.daterangepicker select.hourselect,.daterangepicker select.minuteselect,.daterangepicker select.secondselect{background:#eee;border:1px solid #eee;font-size:12px;margin:0 auto;outline:0;padding:2px;width:50px}.daterangepicker .calendar-time{line-height:30px;margin:4px auto 0;position:relative;text-align:center}.daterangepicker .calendar-time select.disabled{color:#ccc;cursor:not-allowed}.daterangepicker .drp-buttons{border-top:1px solid #ddd;clear:both;padding:8px;text-align:right}.daterangepicker .drp-buttons:is(.dark *){--tw-border-opacity:1;border-color:rgb(55 65 81/var(--tw-border-opacity))}.daterangepicker .drp-buttons{display:none;line-height:12px;vertical-align:middle}.daterangepicker .drp-selected{display:inline-block;font-size:12px;padding-right:8px}.daterangepicker .drp-buttons .btn{font-size:12px;font-weight:700;margin-left:8px;padding:4px 8px}.daterangepicker.show-ranges.single.rtl .drp-calendar.left{border-right:1px solid #ddd}.daterangepicker.show-ranges.single.rtl .drp-calendar.left:is(.dark *){--tw-border-opacity:1;border-color:rgb(55 65 81/var(--tw-border-opacity))}.daterangepicker.show-ranges.single.ltr .drp-calendar.left{border-left:1px solid #ddd}.daterangepicker.show-ranges.single.ltr .drp-calendar.left:is(.dark *){--tw-border-opacity:1;border-color:rgb(55 65 81/var(--tw-border-opacity))}.daterangepicker.show-ranges.rtl .drp-calendar.right{border-right:1px solid #ddd}.daterangepicker.show-ranges.rtl .drp-calendar.right:is(.dark *){--tw-border-opacity:1;border-color:rgb(55 65 81/var(--tw-border-opacity))}.daterangepicker.show-ranges.ltr .drp-calendar.left{border-left:1px solid #ddd}.daterangepicker.show-ranges.ltr .drp-calendar.left:is(.dark *){--tw-border-opacity:1;border-color:rgb(55 65 81/var(--tw-border-opacity))}.daterangepicker .ranges{float:none;margin:0;text-align:left}.daterangepicker.show-calendar .ranges{margin-top:8px}.daterangepicker .ranges ul{list-style:none;margin:0 auto;padding:0;width:100%}.daterangepicker .ranges li{cursor:pointer;font-size:12px;padding:8px 12px}.daterangepicker .ranges li.active{color:#fff}@media (min-width:564px){.daterangepicker{width:auto}.daterangepicker .ranges ul{width:140px}.daterangepicker.single .ranges ul{width:100%}.daterangepicker.single .drp-calendar.left{clear:none}.daterangepicker.single .drp-calendar,.daterangepicker.single .ranges{float:left}.daterangepicker{direction:ltr;text-align:left}.daterangepicker .drp-calendar.left{clear:left;margin-right:0}.daterangepicker .drp-calendar.left .calendar-table{border-bottom-right-radius:0;border-right:none;border-top-right-radius:0}.daterangepicker .drp-calendar.right{margin-left:0}.daterangepicker .drp-calendar.right .calendar-table{border-bottom-left-radius:0;border-left:none;border-top-left-radius:0}.daterangepicker .drp-calendar.left .calendar-table{padding-right:8px}.daterangepicker .drp-calendar,.daterangepicker .ranges{float:left}}@media (min-width:730px){.daterangepicker .ranges{float:left;width:auto}.daterangepicker.rtl .ranges{float:right}.daterangepicker .drp-calendar.left{clear:none!important}}.hover\:text-gray-400:hover{--tw-text-opacity:1;color:rgb(156 163 175/var(--tw-text-opacity))}.dark\:bg-white:is(.dark *){--tw-bg-opacity:1;background-color:rgb(255 255 255/var(--tw-bg-opacity))}.dark\:bg-white\/5:is(.dark *){background-color:hsla(0,0%,100%,.05)}.dark\:text-white:is(.dark *){--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity))}.dark\:hover\:text-gray-200:hover:is(.dark *){--tw-text-opacity:1;color:rgb(229 231 235/var(--tw-text-opacity))} diff --git a/public/js/filament-daterangepicker-filter/filament-daterangepicker-filter2.7.3.0.js b/public/js/filament-daterangepicker-filter/filament-daterangepicker-filter2.7.3.0.js new file mode 100644 index 000000000..0eef7b614 --- /dev/null +++ b/public/js/filament-daterangepicker-filter/filament-daterangepicker-filter2.7.3.0.js @@ -0,0 +1,2 @@ +/*! For license information please see filament-daterangepicker.js.LICENSE.txt */ +(()=>{var e,t={9316:(e,t,n)=>{"use strict";var a=n(4692),s=n.n(a),r=n(5093),i=n.n(r);n(6348);function o(e,t){return function(e){if(Array.isArray(e))return e}(e)||function(e,t){var n=null==e?null:"undefined"!=typeof Symbol&&e[Symbol.iterator]||e["@@iterator"];if(null!=n){var a,s,r,i,o=[],d=!0,u=!1;try{if(r=(n=n.call(e)).next,0===t){if(Object(n)!==n)return;d=!1}else for(;!(d=(a=r.call(n)).done)&&(o.push(a.value),o.length!==t);d=!0);}catch(e){u=!0,s=e}finally{try{if(!d&&null!=n.return&&(i=n.return(),Object(i)!==i))return}finally{if(u)throw s}}return o}}(e,t)||function(e,t){if(!e)return;if("string"==typeof e)return d(e,t);var n=Object.prototype.toString.call(e).slice(8,-1);"Object"===n&&e.constructor&&(n=e.constructor.name);if("Map"===n||"Set"===n)return Array.from(e);if("Arguments"===n||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n))return d(e,t)}(e,t)||function(){throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}()}function d(e,t){(null==t||t>e.length)&&(t=e.length);for(var n=0,a=new Array(t);n0&&(e=w.map((function(e){return i()(e)}))),s()(this.$refs.daterange).daterangepicker({name:t,alwaysShowCalendars:a,autoApply:r,linkedCalendars:d,singleDatePicker:u,autoUpdateInput:!1,drops:T,opens:b,startDate:null!=l?i()(l):i()(),endDate:null!=_?i()(_):i()(),maxDate:null!=m?i()(m):null,minDate:null!=c?i()(c):null,timePicker:h,timePicker24Hour:f,timePickerSeconds:p,timePickerIncrement:M,showCustomRangeLabel:!v,locale:{format:y,separator:Z,applyLabel:L,cancelLabel:Y,fromLabel:g,toLabel:k,customRangeLabel:D,weekLabel:"W",daysOfWeek:[S,H,x,j,P,O,E],monthNames:[W,A,C,F,N,z,R,I,J,q,U,B],firstDay:G},ranges:K?void 0:ee,maxSpan:$,isInvalidDate:function(t){return e.length>0&&e.some((function(e){return e.isSame(t,"day")}))}},(function(e,n){Q(u?e.format(y):e.format(y)+Z+n.format(y),t)})),this.dateRangePicker=s()(this.$refs.daterange).data("daterangepicker"),null!=this.state){var n=this.state.split(Z);2==n.length&&null!=this.dateRangePicker&&(this.dateRangePicker.setStartDate(n[0]),this.dateRangePicker.setEndDate(n[1]))}s()(this.$refs.daterange).val(this.getRangeLabel(this.state));var o=this;this.$watch("state",(function(e){null==e&&(e="",o.dateRangePicker.setStartDate(i()()),o.dateRangePicker.setEndDate(i()())),s()(o.$refs.daterange).val(o.getRangeLabel(e))}))}}}))};document.addEventListener("alpine:init",(function(){window.Alpine.plugin(u)}))},6348:function(e,t,n){var a,s;function r(e){return r="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e},r(e)}a=[n(5093),n(4692)],void 0===(s=function(e,t){return t.fn||(t.fn={}),"function"!=typeof e&&e.hasOwnProperty("default")&&(e=e.default),function(e,t){var n=function(n,a,s){if(this.parentEl="body",this.element=t(n),this.startDate=e().startOf("day"),this.endDate=e().endOf("day"),this.minDate=!1,this.maxDate=!1,this.maxSpan=!1,this.autoApply=!1,this.singleDatePicker=!1,this.showDropdowns=!1,this.minYear=e().subtract(100,"year").format("YYYY"),this.maxYear=e().add(100,"year").format("YYYY"),this.showWeekNumbers=!1,this.showISOWeekNumbers=!1,this.showCustomRangeLabel=!0,this.timePicker=!1,this.timePicker24Hour=!1,this.timePickerIncrement=1,this.timePickerSeconds=!1,this.linkedCalendars=!0,this.autoUpdateInput=!0,this.alwaysShowCalendars=!1,this.isVisible=!1,this.ranges={},this.name=null,this.opens="right",this.element.hasClass("pull-right")&&(this.opens="left"),this.drops="down",this.element.hasClass("dropup")&&(this.drops="up"),this.buttonClasses="btn btn-sm",this.applyButtonClasses="filament-link inline-flex items-center justify-center gap-0.5 font-medium outline-none hover:underline focus:underline text-sm",this.cancelButtonClasses="filament-link inline-flex items-center justify-center gap-0.5 font-medium outline-none hover:underline focus:underline text-sm text-gray-600 hover:text-gray-400 dark:text-white dark:hover:text-gray-400",this.locale={direction:"ltr",format:e.localeData().longDateFormat("L"),separator:" - ",applyLabel:"Apply",cancelLabel:"Cancel",weekLabel:"W",customRangeLabel:"Custom Range",daysOfWeek:e.weekdaysMin(),monthNames:e.monthsShort(),firstDay:e.localeData().firstDayOfWeek()},this.callback=function(){},this.isShowing=!1,this.leftCalendar={},this.rightCalendar={},"object"===r(a)&&null!==a||(a={}),"string"==typeof(a=t.extend(this.element.data(),a)).template||a.template instanceof t||(a.template='
'),this.parentEl=a.parentEl&&t(a.parentEl).length?t(a.parentEl):t(this.parentEl),this.container=t(a.template).appendTo(this.parentEl),this.name=a.name,"object"===r(a.locale)&&("string"==typeof a.locale.direction&&(this.locale.direction=a.locale.direction),"string"==typeof a.locale.format&&(this.locale.format=a.locale.format),"string"==typeof a.locale.separator&&(this.locale.separator=a.locale.separator),"object"===r(a.locale.daysOfWeek)&&(this.locale.daysOfWeek=a.locale.daysOfWeek.slice()),"object"===r(a.locale.monthNames)&&(this.locale.monthNames=a.locale.monthNames.slice()),"number"==typeof a.locale.firstDay&&(this.locale.firstDay=a.locale.firstDay),"string"==typeof a.locale.applyLabel&&(this.locale.applyLabel=a.locale.applyLabel),"string"==typeof a.locale.cancelLabel&&(this.locale.cancelLabel=a.locale.cancelLabel),"string"==typeof a.locale.weekLabel&&(this.locale.weekLabel=a.locale.weekLabel),"string"==typeof a.locale.customRangeLabel)){(c=document.createElement("textarea")).innerHTML=a.locale.customRangeLabel;var i=c.value;this.locale.customRangeLabel=i}if(this.container.addClass(this.locale.direction),"string"==typeof a.startDate&&(this.startDate=e(a.startDate,this.locale.format)),"string"==typeof a.endDate&&(this.endDate=e(a.endDate,this.locale.format)),"string"==typeof a.minDate&&(this.minDate=e(a.minDate,this.locale.format)),"string"==typeof a.maxDate&&(this.maxDate=e(a.maxDate,this.locale.format)),"object"===r(a.startDate)&&(this.startDate=e(a.startDate)),"object"===r(a.endDate)&&(this.endDate=e(a.endDate)),"object"===r(a.minDate)&&(this.minDate=e(a.minDate)),"object"===r(a.maxDate)&&(this.maxDate=e(a.maxDate)),this.minDate&&this.startDate.isBefore(this.minDate)&&(this.startDate=this.minDate.clone()),this.maxDate&&this.endDate.isAfter(this.maxDate)&&(this.endDate=this.maxDate.clone()),"string"==typeof a.applyButtonClasses&&(this.applyButtonClasses=a.applyButtonClasses),"string"==typeof a.applyClass&&(this.applyButtonClasses=a.applyClass),"string"==typeof a.cancelButtonClasses&&(this.cancelButtonClasses=a.cancelButtonClasses),"string"==typeof a.cancelClass&&(this.cancelButtonClasses=a.cancelClass),"object"===r(a.maxSpan)&&(this.maxSpan=a.maxSpan),"object"===r(a.dateLimit)&&(this.maxSpan=a.dateLimit),"string"==typeof a.opens&&(this.opens=a.opens),"string"==typeof a.drops&&(this.drops=a.drops),"boolean"==typeof a.showWeekNumbers&&(this.showWeekNumbers=a.showWeekNumbers),"boolean"==typeof a.showISOWeekNumbers&&(this.showISOWeekNumbers=a.showISOWeekNumbers),"string"==typeof a.buttonClasses&&(this.buttonClasses=a.buttonClasses),"object"===r(a.buttonClasses)&&(this.buttonClasses=a.buttonClasses.join(" ")),"boolean"==typeof a.showDropdowns&&(this.showDropdowns=a.showDropdowns),"number"==typeof a.minYear&&(this.minYear=a.minYear),"number"==typeof a.maxYear&&(this.maxYear=a.maxYear),"boolean"==typeof a.showCustomRangeLabel&&(this.showCustomRangeLabel=a.showCustomRangeLabel),"boolean"==typeof a.singleDatePicker&&(this.singleDatePicker=a.singleDatePicker,this.singleDatePicker&&(this.endDate=this.startDate.clone())),"boolean"==typeof a.timePicker&&(this.timePicker=a.timePicker),"boolean"==typeof a.timePickerSeconds&&(this.timePickerSeconds=a.timePickerSeconds),"number"==typeof a.timePickerIncrement&&(this.timePickerIncrement=a.timePickerIncrement),"boolean"==typeof a.timePicker24Hour&&(this.timePicker24Hour=a.timePicker24Hour),"boolean"==typeof a.autoApply&&(this.autoApply=a.autoApply),"boolean"==typeof a.autoUpdateInput&&(this.autoUpdateInput=a.autoUpdateInput),"boolean"==typeof a.linkedCalendars&&(this.linkedCalendars=a.linkedCalendars),"function"==typeof a.isInvalidDate&&(this.isInvalidDate=a.isInvalidDate),"function"==typeof a.isCustomDate&&(this.isCustomDate=a.isCustomDate),"boolean"==typeof a.alwaysShowCalendars&&(this.alwaysShowCalendars=a.alwaysShowCalendars),0!=this.locale.firstDay)for(var o=this.locale.firstDay;o>0;)this.locale.daysOfWeek.push(this.locale.daysOfWeek.shift()),o--;var d,u,l;if(void 0===a.startDate&&void 0===a.endDate&&t(this.element).is(":text")){var _=t(this.element).val(),m=_.split(this.locale.separator);d=u=null,2==m.length?(d=e(m[0],this.locale.format),u=e(m[1],this.locale.format)):this.singleDatePicker&&""!==_&&(d=e(_,this.locale.format),u=e(_,this.locale.format)),null!==d&&null!==u&&(this.setStartDate(d),this.setEndDate(u))}if("object"===r(a.ranges)){for(l in a.ranges){d="string"==typeof a.ranges[l][0]?e(a.ranges[l][0],this.locale.format):e(a.ranges[l][0]),u="string"==typeof a.ranges[l][1]?e(a.ranges[l][1],this.locale.format):e(a.ranges[l][1]),this.minDate&&d.isBefore(this.minDate)&&(d=this.minDate.clone());var c,h=this.maxDate;if(this.maxSpan&&h&&d.clone().add(this.maxSpan).isAfter(h)&&(h=d.clone().add(this.maxSpan)),h&&u.isAfter(h)&&(u=h.clone()),!(this.minDate&&u.isBefore(this.minDate,this.timepicker?"minute":"day")||h&&d.isAfter(h,this.timepicker?"minute":"day")))(c=document.createElement("textarea")).innerHTML=l,i=c.value,this.ranges[i]=[d,u]}var f="
    ";for(l in this.ranges)f+='
  • '+l+"
  • ";this.showCustomRangeLabel&&(f+='
  • '+this.locale.customRangeLabel+"
  • "),f+="
",this.container.find(".ranges").prepend(f)}"function"==typeof s&&(this.callback=s),this.timePicker||(this.startDate=this.startDate.startOf("day"),this.endDate=this.endDate.endOf("day"),this.container.find(".calendar-time").hide()),this.timePicker&&this.autoApply&&(this.autoApply=!1),this.autoApply&&this.container.addClass("auto-apply"),"object"===r(a.ranges)&&this.container.addClass("show-ranges"),this.singleDatePicker&&(this.container.addClass("single"),this.container.find(".drp-calendar.left").addClass("single"),this.container.find(".drp-calendar.left").show(),this.container.find(".drp-calendar.right").hide(),!this.timePicker&&this.autoApply&&this.container.addClass("auto-apply")),(void 0===a.ranges&&!this.singleDatePicker||this.alwaysShowCalendars)&&this.container.addClass("show-calendar"),this.container.addClass("opens"+this.opens),this.container.find(".applyBtn, .cancelBtn").addClass(this.buttonClasses),this.applyButtonClasses.length&&this.container.find(".applyBtn").addClass(this.applyButtonClasses),this.cancelButtonClasses.length&&this.container.find(".cancelBtn").addClass(this.cancelButtonClasses),this.container.find(".applyBtn").html(this.locale.applyLabel),this.container.find(".cancelBtn").html(this.locale.cancelLabel),this.container.find(".drp-calendar").on("click.daterangepicker",".prev",t.proxy(this.clickPrev,this)).on("click.daterangepicker",".next",t.proxy(this.clickNext,this)).on("mousedown.daterangepicker","td.available",t.proxy(this.clickDate,this)).on("mouseenter.daterangepicker","td.available",t.proxy(this.hoverDate,this)).on("change.daterangepicker","select.yearselect",t.proxy(this.monthOrYearChanged,this)).on("change.daterangepicker","select.monthselect",t.proxy(this.monthOrYearChanged,this)).on("change.daterangepicker","select.hourselect,select.minuteselect,select.secondselect,select.ampmselect",t.proxy(this.timeChanged,this)),this.container.find(".ranges").on("click.daterangepicker","li",t.proxy(this.clickRange,this)),this.container.find(".drp-buttons").on("click.daterangepicker","button.applyBtn",t.proxy(this.clickApply,this)).on("click.daterangepicker","button.cancelBtn",t.proxy(this.clickCancel,this)),this.element.is("input")||this.element.is("button")?this.element.on({"click.daterangepicker":t.proxy(this.show,this),"focus.daterangepicker":t.proxy(this.show,this),"keyup.daterangepicker":t.proxy(this.elementChanged,this),"keydown.daterangepicker":t.proxy(this.keydown,this)}):(this.element.on("click.daterangepicker",t.proxy(this.toggle,this)),this.element.on("keydown.daterangepicker",t.proxy(this.toggle,this))),this.updateElement()};return n.prototype={constructor:n,setStartDate:function(t){"string"==typeof t&&(this.startDate=e(t,this.locale.format)),"object"===r(t)&&(this.startDate=e(t)),this.timePicker||(this.startDate=this.startDate.startOf("day")),this.timePicker&&this.timePickerIncrement&&this.startDate.minute(Math.round(this.startDate.minute()/this.timePickerIncrement)*this.timePickerIncrement),this.minDate&&this.startDate.isBefore(this.minDate)&&(this.startDate=this.minDate.clone(),this.timePicker&&this.timePickerIncrement&&this.startDate.minute(Math.round(this.startDate.minute()/this.timePickerIncrement)*this.timePickerIncrement)),this.maxDate&&this.startDate.isAfter(this.maxDate)&&(this.startDate=this.maxDate.clone(),this.timePicker&&this.timePickerIncrement&&this.startDate.minute(Math.floor(this.startDate.minute()/this.timePickerIncrement)*this.timePickerIncrement)),this.isShowing||this.updateElement(),this.updateMonthsInView()},setEndDate:function(t){"string"==typeof t&&(this.endDate=e(t,this.locale.format)),"object"===r(t)&&(this.endDate=e(t)),this.timePicker||(this.endDate=this.endDate.endOf("day")),this.timePicker&&this.timePickerIncrement&&this.endDate.minute(Math.round(this.endDate.minute()/this.timePickerIncrement)*this.timePickerIncrement),this.endDate.isBefore(this.startDate)&&(this.endDate=this.startDate.clone()),this.maxDate&&this.endDate.isAfter(this.maxDate)&&(this.endDate=this.maxDate.clone()),this.maxSpan&&this.startDate.clone().add(this.maxSpan).isBefore(this.endDate)&&(this.endDate=this.startDate.clone().add(this.maxSpan)),this.previousRightTime=this.endDate.clone(),this.container.find(".drp-selected").html(this.startDate.format(this.locale.format)+this.locale.separator+this.endDate.format(this.locale.format)),this.isShowing||this.updateElement(),this.updateMonthsInView()},isInvalidDate:function(){return!1},isCustomDate:function(){return!1},updateView:function(){this.timePicker&&(this.renderTimePicker("left"),this.renderTimePicker("right"),this.endDate?this.container.find(".right .calendar-time select").prop("disabled",!1).removeClass("disabled"):this.container.find(".right .calendar-time select").prop("disabled",!0).addClass("disabled")),this.endDate&&this.container.find(".drp-selected").html(this.startDate.format(this.locale.format)+this.locale.separator+this.endDate.format(this.locale.format)),this.updateMonthsInView(),this.updateCalendars(),this.updateFormInputs()},updateMonthsInView:function(){if(this.endDate){if(!this.singleDatePicker&&this.leftCalendar.month&&this.rightCalendar.month&&(this.startDate.format("YYYY-MM")==this.leftCalendar.month.format("YYYY-MM")||this.startDate.format("YYYY-MM")==this.rightCalendar.month.format("YYYY-MM"))&&(this.endDate.format("YYYY-MM")==this.leftCalendar.month.format("YYYY-MM")||this.endDate.format("YYYY-MM")==this.rightCalendar.month.format("YYYY-MM")))return;this.leftCalendar.month=this.startDate.clone().date(2),this.linkedCalendars||this.endDate.month()==this.startDate.month()&&this.endDate.year()==this.startDate.year()?this.rightCalendar.month=this.startDate.clone().date(2).add(1,"month"):this.rightCalendar.month=this.endDate.clone().date(2)}else this.leftCalendar.month.format("YYYY-MM")!=this.startDate.format("YYYY-MM")&&this.rightCalendar.month.format("YYYY-MM")!=this.startDate.format("YYYY-MM")&&(this.leftCalendar.month=this.startDate.clone().date(2),this.rightCalendar.month=this.startDate.clone().date(2).add(1,"month"));this.maxDate&&this.linkedCalendars&&!this.singleDatePicker&&this.rightCalendar.month>this.maxDate&&(this.rightCalendar.month=this.maxDate.clone().date(2),this.leftCalendar.month=this.maxDate.clone().date(2).subtract(1,"month"))},updateCalendars:function(){var e,t,n,a;this.timePicker&&(this.endDate?(e=parseInt(this.container.find(".left .hourselect").val(),10),t=parseInt(this.container.find(".left .minuteselect").val(),10),isNaN(t)&&(t=parseInt(this.container.find(".left .minuteselect option:last").val(),10)),n=this.timePickerSeconds?parseInt(this.container.find(".left .secondselect").val(),10):0,this.timePicker24Hour||("PM"===(a=this.container.find(".left .ampmselect").val())&&e<12&&(e+=12),"AM"===a&&12===e&&(e=0))):(e=parseInt(this.container.find(".right .hourselect").val(),10),t=parseInt(this.container.find(".right .minuteselect").val(),10),isNaN(t)&&(t=parseInt(this.container.find(".right .minuteselect option:last").val(),10)),n=this.timePickerSeconds?parseInt(this.container.find(".right .secondselect").val(),10):0,this.timePicker24Hour||("PM"===(a=this.container.find(".right .ampmselect").val())&&e<12&&(e+=12),"AM"===a&&12===e&&(e=0))),this.leftCalendar.month.hour(e).minute(t).second(n),this.rightCalendar.month.hour(e).minute(t).second(n));this.renderCalendar("left"),this.renderCalendar("right"),this.container.find(".ranges li").removeClass("active"),this.container.find(".ranges li").removeClass("bg-primary-500"),this.container.find(".ranges li").removeClass("dark:bg-primary-500"),this.container.find(".ranges li").removeClass("text-white"),this.container.find(".ranges li").removeClass("dark:text-white"),null!=this.endDate&&this.calculateChosenLabel()},renderCalendar:function(n){var a,s=(a="left"==n?this.leftCalendar:this.rightCalendar).month.month(),r=a.month.year(),i=a.month.hour(),o=a.month.minute(),d=a.month.second(),u=e([r,s]).daysInMonth(),l=e([r,s,1]),_=e([r,s,u]),m=e(l).subtract(1,"month").month(),c=e(l).subtract(1,"month").year(),h=e([c,m]).daysInMonth(),f=l.day();(a=[]).firstDay=l,a.lastDay=_;for(var p=0;p<6;p++)a[p]=[];var M=h-f+this.locale.firstDay+1;M>h&&(M-=7),f==this.locale.firstDay&&(M=h-6);for(var y=e([c,m,M,12,o,d]),L=(p=0,0),Y=0;p<42;p++,L++,y=e(y).add(24,"hour"))p>0&&L%7==0&&(L=0,Y++),a[Y][L]=y.clone().hour(i).minute(o).second(d),y.hour(12),this.minDate&&a[Y][L].format("YYYY-MM-DD")==this.minDate.format("YYYY-MM-DD")&&a[Y][L].isBefore(this.minDate)&&"left"==n&&(a[Y][L]=this.minDate.clone()),this.maxDate&&a[Y][L].format("YYYY-MM-DD")==this.maxDate.format("YYYY-MM-DD")&&a[Y][L].isAfter(this.maxDate)&&"right"==n&&(a[Y][L]=this.maxDate.clone());"left"==n?this.leftCalendar.calendar=a:this.rightCalendar.calendar=a;var g="left"==n?this.minDate:this.startDate,k=this.maxDate,D=("left"==n?this.startDate:this.endDate,this.locale.direction,'');D+="",D+="",(this.showWeekNumbers||this.showISOWeekNumbers)&&(D+=""),g&&g.isValid()&&!g.isBefore(a.firstDay)||this.linkedCalendars&&"left"!=n?D+="":D+='';var v=this.locale.monthNames[a[1][1].month()]+a[1][1].format(" YYYY");if(this.showDropdowns){for(var w=a[1][1].month(),T=a[1][1].year(),b=k&&k.year()||this.maxYear,S=g&&g.year()||this.minYear,H=T==S,x=T==b,j='";for(var O='")}if(D+='",k&&k.isValid()&&!k.isAfter(a.lastDay)||this.linkedCalendars&&"right"!=n&&!this.singleDatePicker?D+="":D+='',D+="",D+="",(this.showWeekNumbers||this.showISOWeekNumbers)&&(D+='"),t.each(this.locale.daysOfWeek,(function(e,t){D+=""})),D+="",D+="",D+="",null==this.endDate&&this.maxSpan){var W=this.startDate.clone().add(this.maxSpan).endOf("day");k&&!W.isBefore(k)||(k=W)}for(Y=0;Y<6;Y++){for(D+="",this.showWeekNumbers?D+='":this.showISOWeekNumbers&&(D+='"),L=0;L<7;L++){var A=[];a[Y][L].isSame(new Date,"day")&&A.push("today"),a[Y][L].isoWeekday()>5&&A.push("weekend"),a[Y][L].month()!=a[1][1].month()&&A.push("off","ends"),this.minDate&&a[Y][L].isBefore(this.minDate,"day")&&A.push("off","disabled"),k&&a[Y][L].isAfter(k,"day")&&A.push("off","disabled"),this.isInvalidDate(a[Y][L])&&A.push("off","disabled"),a[Y][L].format("YYYY-MM-DD")==this.startDate.format("YYYY-MM-DD")&&A.push("active","start-date"),null!=this.endDate&&a[Y][L].format("YYYY-MM-DD")==this.endDate.format("YYYY-MM-DD")&&A.push("active","end-date"),null!=this.endDate&&a[Y][L]>this.startDate&&a[Y][L]'+a[Y][L].date()+""}D+=""}D+="",D+="
'+v+"
'+this.locale.weekLabel+""+t+"
'+a[Y][0].week()+"'+a[Y][0].isoWeek()+"
",this.container.find(".drp-calendar."+n+" .calendar-table").html(D)},renderTimePicker:function(e){if("right"!=e||this.endDate){var t,n,a,s=this.maxDate;if(!this.maxSpan||this.maxDate&&!this.startDate.clone().add(this.maxSpan).isBefore(this.maxDate)||(s=this.startDate.clone().add(this.maxSpan)),"left"==e)n=this.startDate.clone(),a=this.minDate;else if("right"==e){n=this.endDate.clone(),a=this.startDate;var r=this.container.find(".drp-calendar.right .calendar-time");if(""!=r.html()&&(n.hour(isNaN(n.hour())?r.find(".hourselect option:selected").val():n.hour()),n.minute(isNaN(n.minute())?r.find(".minuteselect option:selected").val():n.minute()),n.second(isNaN(n.second())?r.find(".secondselect option:selected").val():n.second()),!this.timePicker24Hour)){var i=r.find(".ampmselect option:selected").val();"PM"===i&&n.hour()<12&&n.hour(n.hour()+12),"AM"===i&&12===n.hour()&&n.hour(0)}n.isBefore(this.startDate)&&(n=this.startDate.clone()),s&&n.isAfter(s)&&(n=s.clone())}t=' ",t+=': ",this.timePickerSeconds){for(t+=': "}if(!this.timePicker24Hour){t+='"}this.container.find(".drp-calendar."+e+" .calendar-time").html(t)}},updateFormInputs:function(){this.singleDatePicker||this.endDate&&(this.startDate.isBefore(this.endDate)||this.startDate.isSame(this.endDate))?this.container.find("button.applyBtn").prop("disabled",!1):this.container.find("button.applyBtn").prop("disabled",!0)},move:function(){var e,n={top:0,left:0},a=this.drops,s=t(window).width();switch(this.parentEl.is("body")||(n={top:this.parentEl.offset().top-this.parentEl.scrollTop(),left:this.parentEl.offset().left-this.parentEl.scrollLeft()},s=this.parentEl[0].clientWidth+this.parentEl.offset().left),a){case"auto":(e=this.element.offset().top+this.element.outerHeight()-n.top)+this.container.outerHeight()>=this.parentEl[0].scrollHeight&&(e=this.element.offset().top-this.container.outerHeight()-n.top,a="up");break;case"up":e=this.element.offset().top-this.container.outerHeight()-n.top;break;default:e=this.element.offset().top+this.element.outerHeight()-n.top}this.container.css({top:0,left:0,right:"auto"});var r=this.container.outerWidth();if(this.container.toggleClass("drop-up","up"==a),"left"==this.opens){var i=s-this.element.offset().left-this.element.outerWidth();r+i>t(window).width()?this.container.css({top:e,right:"auto",left:9}):this.container.css({top:e,right:i,left:"auto"})}else if("center"==this.opens)(o=this.element.offset().left-n.left+this.element.outerWidth()/2-r/2)<0?this.container.css({top:e,right:"auto",left:9}):o+r>t(window).width()?this.container.css({top:e,left:"auto",right:0}):this.container.css({top:e,left:o,right:"auto"});else{var o;(o=this.element.offset().left-n.left)+r>t(window).width()?this.container.css({top:e,left:"auto",right:0}):this.container.css({top:e,left:o,right:"auto"})}},show:function(e){this.isShowing||(this._outsideClickProxy=t.proxy((function(e){this.outsideClick(e)}),this),t(document).on("mousedown.daterangepicker",this._outsideClickProxy).on("touchend.daterangepicker",this._outsideClickProxy).on("click.daterangepicker","[data-toggle=dropdown]",this._outsideClickProxy).on("focusin.daterangepicker",this._outsideClickProxy),t(window).on("resize.daterangepicker",t.proxy((function(e){this.move(e)}),this)),this.oldStartDate=this.startDate.clone(),this.oldEndDate=this.endDate.clone(),this.previousRightTime=this.endDate.clone(),this.updateView(),this.container.show(),this.move(),this.element.trigger("show.daterangepicker",this),this.isShowing=!0)},hide:function(e){this.isShowing&&(this.endDate||(this.startDate=this.oldStartDate.clone(),this.endDate=this.oldEndDate.clone()),this.startDate.isSame(this.oldStartDate)&&this.endDate.isSame(this.oldEndDate)||this.callback(this.startDate.clone(),this.endDate.clone(),this.chosenLabel),this.updateElement(),t(document).off(".daterangepicker"),t(window).off(".daterangepicker"),this.container.hide(),this.element.trigger("hide.daterangepicker",this),this.isShowing=!1)},toggle:function(e){this.isShowing?this.hide():this.show()},outsideClick:function(e){var n=t(e.target);"focusin"==e.type||n.closest(this.element).length||n.closest(this.container).length||n.closest(".calendar-table").length||(this.hide(),this.element.trigger("outsideClick.daterangepicker",this))},showCalendars:function(){this.isVisible=!0,this.container.addClass("show-calendar"),this.move(),this.element.trigger("showCalendar.daterangepicker",this)},hideCalendars:function(){this.isVisible=!1,this.container.removeClass("show-calendar"),this.element.trigger("hideCalendar.daterangepicker",this)},clickRange:function(e){var t=e.target.getAttribute("data-range-key");if(this.chosenLabel=t,t==this.locale.customRangeLabel)this.alwaysShowCalendars||this.isVisible||this.showCalendars();else{var n=this.ranges[t];this.startDate=n[0],this.endDate=n[1],this.timePicker||(this.startDate.startOf("day"),this.endDate.endOf("day")),this.alwaysShowCalendars||this.hideCalendars(),this.clickApply()}},clickPrev:function(e){t(e.target).parents(".drp-calendar").hasClass("left")?(this.leftCalendar.month.subtract(1,"month"),this.linkedCalendars&&this.rightCalendar.month.subtract(1,"month")):this.rightCalendar.month.subtract(1,"month"),this.updateCalendars()},clickNext:function(e){t(e.target).parents(".drp-calendar").hasClass("left")?this.leftCalendar.month.add(1,"month"):(this.rightCalendar.month.add(1,"month"),this.linkedCalendars&&this.leftCalendar.month.add(1,"month")),this.updateCalendars()},hoverDate:function(e){if(t(e.target).hasClass("available")){var n=t(e.target).attr("data-title"),a=n.substr(1,1),s=n.substr(3,1),r=t(e.target).parents(".drp-calendar").hasClass("left")?this.leftCalendar.calendar[a][s]:this.rightCalendar.calendar[a][s],i=this.leftCalendar,o=this.rightCalendar,d=this.startDate;this.endDate||this.container.find(".drp-calendar tbody td").each((function(e,n){if(!t(n).hasClass("week")){var a=t(n).attr("data-title"),s=a.substr(1,1),u=a.substr(3,1),l=t(n).parents(".drp-calendar").hasClass("left")?i.calendar[s][u]:o.calendar[s][u];l.isAfter(d)&&l.isBefore(r)||l.isSame(r,"day")?t(n).addClass("in-range bg-[#ebf4f8] dark:bg-white dark:text-black"):t(n).removeClass("in-range bg-[#ebf4f8] dark:bg-white dark:text-black")}}))}},clickDate:function(e){if(t(e.target).hasClass("available")){var n=t(e.target).attr("data-title"),a=n.substr(1,1),s=n.substr(3,1),r=t(e.target).parents(".drp-calendar").hasClass("left")?this.leftCalendar.calendar[a][s]:this.rightCalendar.calendar[a][s];if(this.endDate||r.isBefore(this.startDate,"day")){if(this.timePicker){var i=parseInt(this.container.find(".left .hourselect").val(),10);this.timePicker24Hour||("PM"===(u=this.container.find(".left .ampmselect").val())&&i<12&&(i+=12),"AM"===u&&12===i&&(i=0));var o=parseInt(this.container.find(".left .minuteselect").val(),10);isNaN(o)&&(o=parseInt(this.container.find(".left .minuteselect option:last").val(),10));var d=this.timePickerSeconds?parseInt(this.container.find(".left .secondselect").val(),10):0;r=r.clone().hour(i).minute(o).second(d)}this.endDate=null,this.setStartDate(r.clone())}else if(!this.endDate&&r.isBefore(this.startDate))this.setEndDate(this.startDate.clone());else{var u;if(this.timePicker)i=parseInt(this.container.find(".right .hourselect").val(),10),this.timePicker24Hour||("PM"===(u=this.container.find(".right .ampmselect").val())&&i<12&&(i+=12),"AM"===u&&12===i&&(i=0)),o=parseInt(this.container.find(".right .minuteselect").val(),10),isNaN(o)&&(o=parseInt(this.container.find(".right .minuteselect option:last").val(),10)),d=this.timePickerSeconds?parseInt(this.container.find(".right .secondselect").val(),10):0,r=r.clone().hour(i).minute(o).second(d);this.setEndDate(r.clone()),this.autoApply&&(this.calculateChosenLabel(),this.clickApply())}this.singleDatePicker&&(this.setEndDate(this.startDate),!this.timePicker&&this.autoApply&&this.clickApply()),this.updateView(),e.stopPropagation()}},calculateChosenLabel:function(){var e=!0,t=0;for(var n in this.ranges){if(this.timePicker){var a=this.timePickerSeconds?"YYYY-MM-DD HH:mm:ss":"YYYY-MM-DD HH:mm";if(this.startDate.format(a)==this.ranges[n][0].format(a)&&this.endDate.format(a)==this.ranges[n][1].format(a)){e=!1,this.chosenLabel=this.container.find(".ranges li:eq("+t+")").addClass("active bg-primary-500 text-white").attr("data-range-key");break}}else if(this.startDate.format("YYYY-MM-DD")==this.ranges[n][0].format("YYYY-MM-DD")&&this.endDate.format("YYYY-MM-DD")==this.ranges[n][1].format("YYYY-MM-DD")){e=!1,this.chosenLabel=this.container.find(".ranges li:eq("+t+")").addClass("active bg-primary-500 text-white").attr("data-range-key");break}t++}e&&(this.showCustomRangeLabel?this.chosenLabel=this.container.find(".ranges li:last").addClass("active bg-primary-500 text-white").attr("data-range-key"):this.chosenLabel=null,this.alwaysShowCalendars||this.isVisible||this.showCalendars())},clickApply:function(e){this.hide(),this.element.trigger("apply.daterangepicker",this)},clickCancel:function(e){this.startDate=this.oldStartDate,this.endDate=this.oldEndDate,this.hide(),t(this).val(""),this.element.trigger("cancel.daterangepicker",this)},monthOrYearChanged:function(e){var n=t(e.target).closest(".drp-calendar").hasClass("left"),a=n?"left":"right",s=this.container.find(".drp-calendar."+a),r=parseInt(s.find(".monthselect").val(),10),i=s.find(".yearselect").val();n||(ithis.maxDate.year()||i==this.maxDate.year()&&r>this.maxDate.month())&&(r=this.maxDate.month(),i=this.maxDate.year()),n?(this.leftCalendar.month.month(r).year(i),this.linkedCalendars&&(this.rightCalendar.month=this.leftCalendar.month.clone().add(1,"month"))):(this.rightCalendar.month.month(r).year(i),this.linkedCalendars&&(this.leftCalendar.month=this.rightCalendar.month.clone().subtract(1,"month"))),this.updateCalendars()},timeChanged:function(e){var n=t(e.target).closest(".drp-calendar"),a=n.hasClass("left"),s=parseInt(n.find(".hourselect").val(),10),r=parseInt(n.find(".minuteselect").val(),10);isNaN(r)&&(r=parseInt(n.find(".minuteselect option:last").val(),10));var i=this.timePickerSeconds?parseInt(n.find(".secondselect").val(),10):0;if(!this.timePicker24Hour){var o=n.find(".ampmselect").val();"PM"===o&&s<12&&(s+=12),"AM"===o&&12===s&&(s=0)}if(a){var d=this.startDate.clone();d.hour(s),d.minute(r),d.second(i),this.setStartDate(d),this.singleDatePicker?this.endDate=this.startDate.clone():this.endDate&&this.endDate.format("YYYY-MM-DD")==d.format("YYYY-MM-DD")&&this.endDate.isBefore(d)&&this.setEndDate(d.clone())}else if(this.endDate){var u=this.endDate.clone();u.hour(s),u.minute(r),u.second(i),this.setEndDate(u)}this.updateCalendars(),this.updateFormInputs(),this.renderTimePicker("left"),this.renderTimePicker("right")},elementChanged:function(){if(this.element.is("input")&&this.element.val().length){var t=this.element.val().split(this.locale.separator),n=null,a=null;2===t.length&&(n=e(t[0],this.locale.format),a=e(t[1],this.locale.format)),(this.singleDatePicker||null===n||null===a)&&(a=n=e(this.element.val(),this.locale.format)),n.isValid()&&a.isValid()&&(this.setStartDate(n),this.setEndDate(a),this.updateView())}},keydown:function(e){9!==e.keyCode&&13!==e.keyCode||this.hide(),27===e.keyCode&&(e.preventDefault(),e.stopPropagation(),this.hide())},updateElement:function(){if(this.element.is("input")&&this.autoUpdateInput){var e=this.startDate.format(this.locale.format);this.singleDatePicker||(e+=this.locale.separator+this.endDate.format(this.locale.format)),e!==this.element.val()&&this.element.val(e).trigger("change")}},remove:function(){this.container.remove(),this.element.off(".daterangepicker"),this.element.removeData()}},t.fn.daterangepicker=function(e,a){var s=t.extend(!0,{},t.fn.daterangepicker.defaultOptions,e);return this.each((function(){var e=t(this);e.data("daterangepicker")&&e.data("daterangepicker").remove(),e.data("daterangepicker",new n(e,s,a))})),this},n}(e,t)}.apply(t,a))||(e.exports=s)},4692:function(e,t){var n;!function(t,n){"use strict";"object"==typeof e.exports?e.exports=t.document?n(t,!0):function(e){if(!e.document)throw new Error("jQuery requires a window with a document");return n(e)}:n(t)}("undefined"!=typeof window?window:this,(function(a,s){"use strict";var r=[],i=Object.getPrototypeOf,o=r.slice,d=r.flat?function(e){return r.flat.call(e)}:function(e){return r.concat.apply([],e)},u=r.push,l=r.indexOf,_={},m=_.toString,c=_.hasOwnProperty,h=c.toString,f=h.call(Object),p={},M=function(e){return"function"==typeof e&&"number"!=typeof e.nodeType&&"function"!=typeof e.item},y=function(e){return null!=e&&e===e.window},L=a.document,Y={type:!0,src:!0,nonce:!0,noModule:!0};function g(e,t,n){var a,s,r=(n=n||L).createElement("script");if(r.text=e,t)for(a in Y)(s=t[a]||t.getAttribute&&t.getAttribute(a))&&r.setAttribute(a,s);n.head.appendChild(r).parentNode.removeChild(r)}function k(e){return null==e?e+"":"object"==typeof e||"function"==typeof e?_[m.call(e)]||"object":typeof e}var D="3.7.1",v=/HTML$/i,w=function(e,t){return new w.fn.init(e,t)};function T(e){var t=!!e&&"length"in e&&e.length,n=k(e);return!M(e)&&!y(e)&&("array"===n||0===t||"number"==typeof t&&t>0&&t-1 in e)}function b(e,t){return e.nodeName&&e.nodeName.toLowerCase()===t.toLowerCase()}w.fn=w.prototype={jquery:D,constructor:w,length:0,toArray:function(){return o.call(this)},get:function(e){return null==e?o.call(this):e<0?this[e+this.length]:this[e]},pushStack:function(e){var t=w.merge(this.constructor(),e);return t.prevObject=this,t},each:function(e){return w.each(this,e)},map:function(e){return this.pushStack(w.map(this,(function(t,n){return e.call(t,n,t)})))},slice:function(){return this.pushStack(o.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},even:function(){return this.pushStack(w.grep(this,(function(e,t){return(t+1)%2})))},odd:function(){return this.pushStack(w.grep(this,(function(e,t){return t%2})))},eq:function(e){var t=this.length,n=+e+(e<0?t:0);return this.pushStack(n>=0&&n+~]|"+j+")"+j+"*"),R=new RegExp(j+"|>"),I=new RegExp(C),J=new RegExp("^"+O+"$"),q={ID:new RegExp("^#("+O+")"),CLASS:new RegExp("^\\.("+O+")"),TAG:new RegExp("^("+O+"|[*])"),ATTR:new RegExp("^"+E),PSEUDO:new RegExp("^"+C),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+j+"*(even|odd|(([+-]|)(\\d*)n|)"+j+"*(?:([+-]|)"+j+"*(\\d+)|))"+j+"*\\)|)","i"),bool:new RegExp("^(?:"+T+")$","i"),needsContext:new RegExp("^"+j+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+j+"*((?:-\\d)?\\d*)"+j+"*\\)|)(?=[^-]|$)","i")},U=/^(?:input|select|textarea|button)$/i,B=/^h\d$/i,G=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,V=/[+~]/,$=new RegExp("\\\\[\\da-fA-F]{1,6}"+j+"?|\\\\([^\\r\\n\\f])","g"),K=function(e,t){var n="0x"+e.slice(1)-65536;return t||(n<0?String.fromCharCode(n+65536):String.fromCharCode(n>>10|55296,1023&n|56320))},Z=function(){de()},X=me((function(e){return!0===e.disabled&&b(e,"fieldset")}),{dir:"parentNode",next:"legend"});try{f.apply(r=o.call(W.childNodes),W.childNodes),r[W.childNodes.length].nodeType}catch(e){f={apply:function(e,t){A.apply(e,o.call(t))},call:function(e){A.apply(e,o.call(arguments,1))}}}function Q(e,t,n,a){var s,r,i,o,u,l,c,h=t&&t.ownerDocument,y=t?t.nodeType:9;if(n=n||[],"string"!=typeof e||!e||1!==y&&9!==y&&11!==y)return n;if(!a&&(de(t),t=t||d,_)){if(11!==y&&(u=G.exec(e)))if(s=u[1]){if(9===y){if(!(i=t.getElementById(s)))return n;if(i.id===s)return f.call(n,i),n}else if(h&&(i=h.getElementById(s))&&Q.contains(t,i)&&i.id===s)return f.call(n,i),n}else{if(u[2])return f.apply(n,t.getElementsByTagName(e)),n;if((s=u[3])&&t.getElementsByClassName)return f.apply(n,t.getElementsByClassName(s)),n}if(!(D[e+" "]||m&&m.test(e))){if(c=e,h=t,1===y&&(R.test(e)||z.test(e))){for((h=V.test(e)&&oe(t.parentNode)||t)==t&&p.scope||((o=t.getAttribute("id"))?o=w.escapeSelector(o):t.setAttribute("id",o=M)),r=(l=le(e)).length;r--;)l[r]=(o?"#"+o:":scope")+" "+_e(l[r]);c=l.join(",")}try{return f.apply(n,h.querySelectorAll(c)),n}catch(t){D(e,!0)}finally{o===M&&t.removeAttribute("id")}}}return ye(e.replace(P,"$1"),t,n,a)}function ee(){var e=[];return function n(a,s){return e.push(a+" ")>t.cacheLength&&delete n[e.shift()],n[a+" "]=s}}function te(e){return e[M]=!0,e}function ne(e){var t=d.createElement("fieldset");try{return!!e(t)}catch(e){return!1}finally{t.parentNode&&t.parentNode.removeChild(t),t=null}}function ae(e){return function(t){return b(t,"input")&&t.type===e}}function se(e){return function(t){return(b(t,"input")||b(t,"button"))&&t.type===e}}function re(e){return function(t){return"form"in t?t.parentNode&&!1===t.disabled?"label"in t?"label"in t.parentNode?t.parentNode.disabled===e:t.disabled===e:t.isDisabled===e||t.isDisabled!==!e&&X(t)===e:t.disabled===e:"label"in t&&t.disabled===e}}function ie(e){return te((function(t){return t=+t,te((function(n,a){for(var s,r=e([],n.length,t),i=r.length;i--;)n[s=r[i]]&&(n[s]=!(a[s]=n[s]))}))}))}function oe(e){return e&&void 0!==e.getElementsByTagName&&e}function de(e){var n,a=e?e.ownerDocument||e:W;return a!=d&&9===a.nodeType&&a.documentElement?(u=(d=a).documentElement,_=!w.isXMLDoc(d),h=u.matches||u.webkitMatchesSelector||u.msMatchesSelector,u.msMatchesSelector&&W!=d&&(n=d.defaultView)&&n.top!==n&&n.addEventListener("unload",Z),p.getById=ne((function(e){return u.appendChild(e).id=w.expando,!d.getElementsByName||!d.getElementsByName(w.expando).length})),p.disconnectedMatch=ne((function(e){return h.call(e,"*")})),p.scope=ne((function(){return d.querySelectorAll(":scope")})),p.cssHas=ne((function(){try{return d.querySelector(":has(*,:jqfake)"),!1}catch(e){return!0}})),p.getById?(t.filter.ID=function(e){var t=e.replace($,K);return function(e){return e.getAttribute("id")===t}},t.find.ID=function(e,t){if(void 0!==t.getElementById&&_){var n=t.getElementById(e);return n?[n]:[]}}):(t.filter.ID=function(e){var t=e.replace($,K);return function(e){var n=void 0!==e.getAttributeNode&&e.getAttributeNode("id");return n&&n.value===t}},t.find.ID=function(e,t){if(void 0!==t.getElementById&&_){var n,a,s,r=t.getElementById(e);if(r){if((n=r.getAttributeNode("id"))&&n.value===e)return[r];for(s=t.getElementsByName(e),a=0;r=s[a++];)if((n=r.getAttributeNode("id"))&&n.value===e)return[r]}return[]}}),t.find.TAG=function(e,t){return void 0!==t.getElementsByTagName?t.getElementsByTagName(e):t.querySelectorAll(e)},t.find.CLASS=function(e,t){if(void 0!==t.getElementsByClassName&&_)return t.getElementsByClassName(e)},m=[],ne((function(e){var t;u.appendChild(e).innerHTML="",e.querySelectorAll("[selected]").length||m.push("\\["+j+"*(?:value|"+T+")"),e.querySelectorAll("[id~="+M+"-]").length||m.push("~="),e.querySelectorAll("a#"+M+"+*").length||m.push(".#.+[+~]"),e.querySelectorAll(":checked").length||m.push(":checked"),(t=d.createElement("input")).setAttribute("type","hidden"),e.appendChild(t).setAttribute("name","D"),u.appendChild(e).disabled=!0,2!==e.querySelectorAll(":disabled").length&&m.push(":enabled",":disabled"),(t=d.createElement("input")).setAttribute("name",""),e.appendChild(t),e.querySelectorAll("[name='']").length||m.push("\\["+j+"*name"+j+"*="+j+"*(?:''|\"\")")})),p.cssHas||m.push(":has"),m=m.length&&new RegExp(m.join("|")),v=function(e,t){if(e===t)return i=!0,0;var n=!e.compareDocumentPosition-!t.compareDocumentPosition;return n||(1&(n=(e.ownerDocument||e)==(t.ownerDocument||t)?e.compareDocumentPosition(t):1)||!p.sortDetached&&t.compareDocumentPosition(e)===n?e===d||e.ownerDocument==W&&Q.contains(W,e)?-1:t===d||t.ownerDocument==W&&Q.contains(W,t)?1:s?l.call(s,e)-l.call(s,t):0:4&n?-1:1)},d):d}for(e in Q.matches=function(e,t){return Q(e,null,null,t)},Q.matchesSelector=function(e,t){if(de(e),_&&!D[t+" "]&&(!m||!m.test(t)))try{var n=h.call(e,t);if(n||p.disconnectedMatch||e.document&&11!==e.document.nodeType)return n}catch(e){D(t,!0)}return Q(t,d,null,[e]).length>0},Q.contains=function(e,t){return(e.ownerDocument||e)!=d&&de(e),w.contains(e,t)},Q.attr=function(e,n){(e.ownerDocument||e)!=d&&de(e);var a=t.attrHandle[n.toLowerCase()],s=a&&c.call(t.attrHandle,n.toLowerCase())?a(e,n,!_):void 0;return void 0!==s?s:e.getAttribute(n)},Q.error=function(e){throw new Error("Syntax error, unrecognized expression: "+e)},w.uniqueSort=function(e){var t,n=[],a=0,r=0;if(i=!p.sortStable,s=!p.sortStable&&o.call(e,0),H.call(e,v),i){for(;t=e[r++];)t===e[r]&&(a=n.push(r));for(;a--;)x.call(e,n[a],1)}return s=null,e},w.fn.uniqueSort=function(){return this.pushStack(w.uniqueSort(o.apply(this)))},t=w.expr={cacheLength:50,createPseudo:te,match:q,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace($,K),e[3]=(e[3]||e[4]||e[5]||"").replace($,K),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||Q.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&Q.error(e[0]),e},PSEUDO:function(e){var t,n=!e[6]&&e[2];return q.CHILD.test(e[0])?null:(e[3]?e[2]=e[4]||e[5]||"":n&&I.test(n)&&(t=le(n,!0))&&(t=n.indexOf(")",n.length-t)-n.length)&&(e[0]=e[0].slice(0,t),e[2]=n.slice(0,t)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace($,K).toLowerCase();return"*"===e?function(){return!0}:function(e){return b(e,t)}},CLASS:function(e){var t=Y[e+" "];return t||(t=new RegExp("(^|"+j+")"+e+"("+j+"|$)"))&&Y(e,(function(e){return t.test("string"==typeof e.className&&e.className||void 0!==e.getAttribute&&e.getAttribute("class")||"")}))},ATTR:function(e,t,n){return function(a){var s=Q.attr(a,e);return null==s?"!="===t:!t||(s+="","="===t?s===n:"!="===t?s!==n:"^="===t?n&&0===s.indexOf(n):"*="===t?n&&s.indexOf(n)>-1:"$="===t?n&&s.slice(-n.length)===n:"~="===t?(" "+s.replace(F," ")+" ").indexOf(n)>-1:"|="===t&&(s===n||s.slice(0,n.length+1)===n+"-"))}},CHILD:function(e,t,n,a,s){var r="nth"!==e.slice(0,3),i="last"!==e.slice(-4),o="of-type"===t;return 1===a&&0===s?function(e){return!!e.parentNode}:function(t,n,d){var u,l,_,m,c,h=r!==i?"nextSibling":"previousSibling",f=t.parentNode,p=o&&t.nodeName.toLowerCase(),L=!d&&!o,Y=!1;if(f){if(r){for(;h;){for(_=t;_=_[h];)if(o?b(_,p):1===_.nodeType)return!1;c=h="only"===e&&!c&&"nextSibling"}return!0}if(c=[i?f.firstChild:f.lastChild],i&&L){for(Y=(m=(u=(l=f[M]||(f[M]={}))[e]||[])[0]===y&&u[1])&&u[2],_=m&&f.childNodes[m];_=++m&&_&&_[h]||(Y=m=0)||c.pop();)if(1===_.nodeType&&++Y&&_===t){l[e]=[y,m,Y];break}}else if(L&&(Y=m=(u=(l=t[M]||(t[M]={}))[e]||[])[0]===y&&u[1]),!1===Y)for(;(_=++m&&_&&_[h]||(Y=m=0)||c.pop())&&(!(o?b(_,p):1===_.nodeType)||!++Y||(L&&((l=_[M]||(_[M]={}))[e]=[y,Y]),_!==t)););return(Y-=s)===a||Y%a==0&&Y/a>=0}}},PSEUDO:function(e,n){var a,s=t.pseudos[e]||t.setFilters[e.toLowerCase()]||Q.error("unsupported pseudo: "+e);return s[M]?s(n):s.length>1?(a=[e,e,"",n],t.setFilters.hasOwnProperty(e.toLowerCase())?te((function(e,t){for(var a,r=s(e,n),i=r.length;i--;)e[a=l.call(e,r[i])]=!(t[a]=r[i])})):function(e){return s(e,0,a)}):s}},pseudos:{not:te((function(e){var t=[],n=[],a=Me(e.replace(P,"$1"));return a[M]?te((function(e,t,n,s){for(var r,i=a(e,null,s,[]),o=e.length;o--;)(r=i[o])&&(e[o]=!(t[o]=r))})):function(e,s,r){return t[0]=e,a(t,null,r,n),t[0]=null,!n.pop()}})),has:te((function(e){return function(t){return Q(e,t).length>0}})),contains:te((function(e){return e=e.replace($,K),function(t){return(t.textContent||w.text(t)).indexOf(e)>-1}})),lang:te((function(e){return J.test(e||"")||Q.error("unsupported lang: "+e),e=e.replace($,K).toLowerCase(),function(t){var n;do{if(n=_?t.lang:t.getAttribute("xml:lang")||t.getAttribute("lang"))return(n=n.toLowerCase())===e||0===n.indexOf(e+"-")}while((t=t.parentNode)&&1===t.nodeType);return!1}})),target:function(e){var t=a.location&&a.location.hash;return t&&t.slice(1)===e.id},root:function(e){return e===u},focus:function(e){return e===function(){try{return d.activeElement}catch(e){}}()&&d.hasFocus()&&!!(e.type||e.href||~e.tabIndex)},enabled:re(!1),disabled:re(!0),checked:function(e){return b(e,"input")&&!!e.checked||b(e,"option")&&!!e.selected},selected:function(e){return e.parentNode&&e.parentNode.selectedIndex,!0===e.selected},empty:function(e){for(e=e.firstChild;e;e=e.nextSibling)if(e.nodeType<6)return!1;return!0},parent:function(e){return!t.pseudos.empty(e)},header:function(e){return B.test(e.nodeName)},input:function(e){return U.test(e.nodeName)},button:function(e){return b(e,"input")&&"button"===e.type||b(e,"button")},text:function(e){var t;return b(e,"input")&&"text"===e.type&&(null==(t=e.getAttribute("type"))||"text"===t.toLowerCase())},first:ie((function(){return[0]})),last:ie((function(e,t){return[t-1]})),eq:ie((function(e,t,n){return[n<0?n+t:n]})),even:ie((function(e,t){for(var n=0;nt?t:n;--a>=0;)e.push(a);return e})),gt:ie((function(e,t,n){for(var a=n<0?n+t:n;++a1?function(t,n,a){for(var s=e.length;s--;)if(!e[s](t,n,a))return!1;return!0}:e[0]}function he(e,t,n,a,s){for(var r,i=[],o=0,d=e.length,u=null!=t;o-1&&(r[u]=!(i[u]=m))}}else c=he(c===i?c.splice(M,c.length):c),s?s(null,i,c,d):f.apply(i,c)}))}function pe(e){for(var a,s,r,i=e.length,o=t.relative[e[0].type],d=o||t.relative[" "],u=o?1:0,_=me((function(e){return e===a}),d,!0),m=me((function(e){return l.call(a,e)>-1}),d,!0),c=[function(e,t,s){var r=!o&&(s||t!=n)||((a=t).nodeType?_(e,t,s):m(e,t,s));return a=null,r}];u1&&ce(c),u>1&&_e(e.slice(0,u-1).concat({value:" "===e[u-2].type?"*":""})).replace(P,"$1"),s,u0,r=e.length>0,i=function(i,o,u,l,m){var c,h,p,M=0,L="0",Y=i&&[],g=[],k=n,D=i||r&&t.find.TAG("*",m),v=y+=null==k?1:Math.random()||.1,T=D.length;for(m&&(n=o==d||o||m);L!==T&&null!=(c=D[L]);L++){if(r&&c){for(h=0,o||c.ownerDocument==d||(de(c),u=!_);p=e[h++];)if(p(c,o||d,u)){f.call(l,c);break}m&&(y=v)}s&&((c=!p&&c)&&M--,i&&Y.push(c))}if(M+=L,s&&L!==M){for(h=0;p=a[h++];)p(Y,g,o,u);if(i){if(M>0)for(;L--;)Y[L]||g[L]||(g[L]=S.call(l));g=he(g)}f.apply(l,g),m&&!i&&g.length>0&&M+a.length>1&&w.uniqueSort(l)}return m&&(y=v,n=k),Y};return s?te(i):i}(i,r)),o.selector=e}return o}function ye(e,n,a,s){var r,i,o,d,u,l="function"==typeof e&&e,m=!s&&le(e=l.selector||e);if(a=a||[],1===m.length){if((i=m[0]=m[0].slice(0)).length>2&&"ID"===(o=i[0]).type&&9===n.nodeType&&_&&t.relative[i[1].type]){if(!(n=(t.find.ID(o.matches[0].replace($,K),n)||[])[0]))return a;l&&(n=n.parentNode),e=e.slice(i.shift().value.length)}for(r=q.needsContext.test(e)?0:i.length;r--&&(o=i[r],!t.relative[d=o.type]);)if((u=t.find[d])&&(s=u(o.matches[0].replace($,K),V.test(i[0].type)&&oe(n.parentNode)||n))){if(i.splice(r,1),!(e=s.length&&_e(i)))return f.apply(a,s),a;break}}return(l||Me(e,m))(s,n,!_,a,!n||V.test(e)&&oe(n.parentNode)||n),a}ue.prototype=t.filters=t.pseudos,t.setFilters=new ue,p.sortStable=M.split("").sort(v).join("")===M,de(),p.sortDetached=ne((function(e){return 1&e.compareDocumentPosition(d.createElement("fieldset"))})),w.find=Q,w.expr[":"]=w.expr.pseudos,w.unique=w.uniqueSort,Q.compile=Me,Q.select=ye,Q.setDocument=de,Q.tokenize=le,Q.escape=w.escapeSelector,Q.getText=w.text,Q.isXML=w.isXMLDoc,Q.selectors=w.expr,Q.support=w.support,Q.uniqueSort=w.uniqueSort}();var C=function(e,t,n){for(var a=[],s=void 0!==n;(e=e[t])&&9!==e.nodeType;)if(1===e.nodeType){if(s&&w(e).is(n))break;a.push(e)}return a},F=function(e,t){for(var n=[];e;e=e.nextSibling)1===e.nodeType&&e!==t&&n.push(e);return n},N=w.expr.match.needsContext,z=/^<([a-z][^\/\0>:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i;function R(e,t,n){return M(t)?w.grep(e,(function(e,a){return!!t.call(e,a,e)!==n})):t.nodeType?w.grep(e,(function(e){return e===t!==n})):"string"!=typeof t?w.grep(e,(function(e){return l.call(t,e)>-1!==n})):w.filter(t,e,n)}w.filter=function(e,t,n){var a=t[0];return n&&(e=":not("+e+")"),1===t.length&&1===a.nodeType?w.find.matchesSelector(a,e)?[a]:[]:w.find.matches(e,w.grep(t,(function(e){return 1===e.nodeType})))},w.fn.extend({find:function(e){var t,n,a=this.length,s=this;if("string"!=typeof e)return this.pushStack(w(e).filter((function(){for(t=0;t1?w.uniqueSort(n):n},filter:function(e){return this.pushStack(R(this,e||[],!1))},not:function(e){return this.pushStack(R(this,e||[],!0))},is:function(e){return!!R(this,"string"==typeof e&&N.test(e)?w(e):e||[],!1).length}});var I,J=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]+))$/;(w.fn.init=function(e,t,n){var a,s;if(!e)return this;if(n=n||I,"string"==typeof e){if(!(a="<"===e[0]&&">"===e[e.length-1]&&e.length>=3?[null,e,null]:J.exec(e))||!a[1]&&t)return!t||t.jquery?(t||n).find(e):this.constructor(t).find(e);if(a[1]){if(t=t instanceof w?t[0]:t,w.merge(this,w.parseHTML(a[1],t&&t.nodeType?t.ownerDocument||t:L,!0)),z.test(a[1])&&w.isPlainObject(t))for(a in t)M(this[a])?this[a](t[a]):this.attr(a,t[a]);return this}return(s=L.getElementById(a[2]))&&(this[0]=s,this.length=1),this}return e.nodeType?(this[0]=e,this.length=1,this):M(e)?void 0!==n.ready?n.ready(e):e(w):w.makeArray(e,this)}).prototype=w.fn,I=w(L);var q=/^(?:parents|prev(?:Until|All))/,U={children:!0,contents:!0,next:!0,prev:!0};function B(e,t){for(;(e=e[t])&&1!==e.nodeType;);return e}w.fn.extend({has:function(e){var t=w(e,this),n=t.length;return this.filter((function(){for(var e=0;e-1:1===n.nodeType&&w.find.matchesSelector(n,e))){r.push(n);break}return this.pushStack(r.length>1?w.uniqueSort(r):r)},index:function(e){return e?"string"==typeof e?l.call(w(e),this[0]):l.call(this,e.jquery?e[0]:e):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(e,t){return this.pushStack(w.uniqueSort(w.merge(this.get(),w(e,t))))},addBack:function(e){return this.add(null==e?this.prevObject:this.prevObject.filter(e))}}),w.each({parent:function(e){var t=e.parentNode;return t&&11!==t.nodeType?t:null},parents:function(e){return C(e,"parentNode")},parentsUntil:function(e,t,n){return C(e,"parentNode",n)},next:function(e){return B(e,"nextSibling")},prev:function(e){return B(e,"previousSibling")},nextAll:function(e){return C(e,"nextSibling")},prevAll:function(e){return C(e,"previousSibling")},nextUntil:function(e,t,n){return C(e,"nextSibling",n)},prevUntil:function(e,t,n){return C(e,"previousSibling",n)},siblings:function(e){return F((e.parentNode||{}).firstChild,e)},children:function(e){return F(e.firstChild)},contents:function(e){return null!=e.contentDocument&&i(e.contentDocument)?e.contentDocument:(b(e,"template")&&(e=e.content||e),w.merge([],e.childNodes))}},(function(e,t){w.fn[e]=function(n,a){var s=w.map(this,t,n);return"Until"!==e.slice(-5)&&(a=n),a&&"string"==typeof a&&(s=w.filter(a,s)),this.length>1&&(U[e]||w.uniqueSort(s),q.test(e)&&s.reverse()),this.pushStack(s)}}));var G=/[^\x20\t\r\n\f]+/g;function V(e){return e}function $(e){throw e}function K(e,t,n,a){var s;try{e&&M(s=e.promise)?s.call(e).done(t).fail(n):e&&M(s=e.then)?s.call(e,t,n):t.apply(void 0,[e].slice(a))}catch(e){n.apply(void 0,[e])}}w.Callbacks=function(e){e="string"==typeof e?function(e){var t={};return w.each(e.match(G)||[],(function(e,n){t[n]=!0})),t}(e):w.extend({},e);var t,n,a,s,r=[],i=[],o=-1,d=function(){for(s=s||e.once,a=t=!0;i.length;o=-1)for(n=i.shift();++o-1;)r.splice(n,1),n<=o&&o--})),this},has:function(e){return e?w.inArray(e,r)>-1:r.length>0},empty:function(){return r&&(r=[]),this},disable:function(){return s=i=[],r=n="",this},disabled:function(){return!r},lock:function(){return s=i=[],n||t||(r=n=""),this},locked:function(){return!!s},fireWith:function(e,n){return s||(n=[e,(n=n||[]).slice?n.slice():n],i.push(n),t||d()),this},fire:function(){return u.fireWith(this,arguments),this},fired:function(){return!!a}};return u},w.extend({Deferred:function(e){var t=[["notify","progress",w.Callbacks("memory"),w.Callbacks("memory"),2],["resolve","done",w.Callbacks("once memory"),w.Callbacks("once memory"),0,"resolved"],["reject","fail",w.Callbacks("once memory"),w.Callbacks("once memory"),1,"rejected"]],n="pending",s={state:function(){return n},always:function(){return r.done(arguments).fail(arguments),this},catch:function(e){return s.then(null,e)},pipe:function(){var e=arguments;return w.Deferred((function(n){w.each(t,(function(t,a){var s=M(e[a[4]])&&e[a[4]];r[a[1]]((function(){var e=s&&s.apply(this,arguments);e&&M(e.promise)?e.promise().progress(n.notify).done(n.resolve).fail(n.reject):n[a[0]+"With"](this,s?[e]:arguments)}))})),e=null})).promise()},then:function(e,n,s){var r=0;function i(e,t,n,s){return function(){var o=this,d=arguments,u=function(){var a,u;if(!(e=r&&(n!==$&&(o=void 0,d=[a]),t.rejectWith(o,d))}};e?l():(w.Deferred.getErrorHook?l.error=w.Deferred.getErrorHook():w.Deferred.getStackHook&&(l.error=w.Deferred.getStackHook()),a.setTimeout(l))}}return w.Deferred((function(a){t[0][3].add(i(0,a,M(s)?s:V,a.notifyWith)),t[1][3].add(i(0,a,M(e)?e:V)),t[2][3].add(i(0,a,M(n)?n:$))})).promise()},promise:function(e){return null!=e?w.extend(e,s):s}},r={};return w.each(t,(function(e,a){var i=a[2],o=a[5];s[a[1]]=i.add,o&&i.add((function(){n=o}),t[3-e][2].disable,t[3-e][3].disable,t[0][2].lock,t[0][3].lock),i.add(a[3].fire),r[a[0]]=function(){return r[a[0]+"With"](this===r?void 0:this,arguments),this},r[a[0]+"With"]=i.fireWith})),s.promise(r),e&&e.call(r,r),r},when:function(e){var t=arguments.length,n=t,a=Array(n),s=o.call(arguments),r=w.Deferred(),i=function(e){return function(n){a[e]=this,s[e]=arguments.length>1?o.call(arguments):n,--t||r.resolveWith(a,s)}};if(t<=1&&(K(e,r.done(i(n)).resolve,r.reject,!t),"pending"===r.state()||M(s[n]&&s[n].then)))return r.then();for(;n--;)K(s[n],i(n),r.reject);return r.promise()}});var Z=/^(Eval|Internal|Range|Reference|Syntax|Type|URI)Error$/;w.Deferred.exceptionHook=function(e,t){a.console&&a.console.warn&&e&&Z.test(e.name)&&a.console.warn("jQuery.Deferred exception: "+e.message,e.stack,t)},w.readyException=function(e){a.setTimeout((function(){throw e}))};var X=w.Deferred();function Q(){L.removeEventListener("DOMContentLoaded",Q),a.removeEventListener("load",Q),w.ready()}w.fn.ready=function(e){return X.then(e).catch((function(e){w.readyException(e)})),this},w.extend({isReady:!1,readyWait:1,ready:function(e){(!0===e?--w.readyWait:w.isReady)||(w.isReady=!0,!0!==e&&--w.readyWait>0||X.resolveWith(L,[w]))}}),w.ready.then=X.then,"complete"===L.readyState||"loading"!==L.readyState&&!L.documentElement.doScroll?a.setTimeout(w.ready):(L.addEventListener("DOMContentLoaded",Q),a.addEventListener("load",Q));var ee=function(e,t,n,a,s,r,i){var o=0,d=e.length,u=null==n;if("object"===k(n))for(o in s=!0,n)ee(e,t,o,n[o],!0,r,i);else if(void 0!==a&&(s=!0,M(a)||(i=!0),u&&(i?(t.call(e,a),t=null):(u=t,t=function(e,t,n){return u.call(w(e),n)})),t))for(;o1,null,!0)},removeData:function(e){return this.each((function(){de.remove(this,e)}))}}),w.extend({queue:function(e,t,n){var a;if(e)return t=(t||"fx")+"queue",a=oe.get(e,t),n&&(!a||Array.isArray(n)?a=oe.access(e,t,w.makeArray(n)):a.push(n)),a||[]},dequeue:function(e,t){t=t||"fx";var n=w.queue(e,t),a=n.length,s=n.shift(),r=w._queueHooks(e,t);"inprogress"===s&&(s=n.shift(),a--),s&&("fx"===t&&n.unshift("inprogress"),delete r.stop,s.call(e,(function(){w.dequeue(e,t)}),r)),!a&&r&&r.empty.fire()},_queueHooks:function(e,t){var n=t+"queueHooks";return oe.get(e,n)||oe.access(e,n,{empty:w.Callbacks("once memory").add((function(){oe.remove(e,[t+"queue",n])}))})}}),w.fn.extend({queue:function(e,t){var n=2;return"string"!=typeof e&&(t=e,e="fx",n--),arguments.length\x20\t\r\n\f]*)/i,be=/^$|^module$|\/(?:java|ecma)script/i;De=L.createDocumentFragment().appendChild(L.createElement("div")),(ve=L.createElement("input")).setAttribute("type","radio"),ve.setAttribute("checked","checked"),ve.setAttribute("name","t"),De.appendChild(ve),p.checkClone=De.cloneNode(!0).cloneNode(!0).lastChild.checked,De.innerHTML="",p.noCloneChecked=!!De.cloneNode(!0).lastChild.defaultValue,De.innerHTML="",p.option=!!De.lastChild;var Se={thead:[1,"","
"],col:[2,"","
"],tr:[2,"","
"],td:[3,"","
"],_default:[0,"",""]};function He(e,t){var n;return n=void 0!==e.getElementsByTagName?e.getElementsByTagName(t||"*"):void 0!==e.querySelectorAll?e.querySelectorAll(t||"*"):[],void 0===t||t&&b(e,t)?w.merge([e],n):n}function xe(e,t){for(var n=0,a=e.length;n",""]);var je=/<|&#?\w+;/;function Pe(e,t,n,a,s){for(var r,i,o,d,u,l,_=t.createDocumentFragment(),m=[],c=0,h=e.length;c-1)s&&s.push(r);else if(u=pe(r),i=He(_.appendChild(r),"script"),u&&xe(i),n)for(l=0;r=i[l++];)be.test(r.type||"")&&n.push(r);return _}var Oe=/^([^.]*)(?:\.(.+)|)/;function Ee(){return!0}function We(){return!1}function Ae(e,t,n,a,s,r){var i,o;if("object"==typeof t){for(o in"string"!=typeof n&&(a=a||n,n=void 0),t)Ae(e,o,n,a,t[o],r);return e}if(null==a&&null==s?(s=n,a=n=void 0):null==s&&("string"==typeof n?(s=a,a=void 0):(s=a,a=n,n=void 0)),!1===s)s=We;else if(!s)return e;return 1===r&&(i=s,s=function(e){return w().off(e),i.apply(this,arguments)},s.guid=i.guid||(i.guid=w.guid++)),e.each((function(){w.event.add(this,t,s,a,n)}))}function Ce(e,t,n){n?(oe.set(e,t,!1),w.event.add(e,t,{namespace:!1,handler:function(e){var n,a=oe.get(this,t);if(1&e.isTrigger&&this[t]){if(a)(w.event.special[t]||{}).delegateType&&e.stopPropagation();else if(a=o.call(arguments),oe.set(this,t,a),this[t](),n=oe.get(this,t),oe.set(this,t,!1),a!==n)return e.stopImmediatePropagation(),e.preventDefault(),n}else a&&(oe.set(this,t,w.event.trigger(a[0],a.slice(1),this)),e.stopPropagation(),e.isImmediatePropagationStopped=Ee)}})):void 0===oe.get(e,t)&&w.event.add(e,t,Ee)}w.event={global:{},add:function(e,t,n,a,s){var r,i,o,d,u,l,_,m,c,h,f,p=oe.get(e);if(re(e))for(n.handler&&(n=(r=n).handler,s=r.selector),s&&w.find.matchesSelector(fe,s),n.guid||(n.guid=w.guid++),(d=p.events)||(d=p.events=Object.create(null)),(i=p.handle)||(i=p.handle=function(t){return void 0!==w&&w.event.triggered!==t.type?w.event.dispatch.apply(e,arguments):void 0}),u=(t=(t||"").match(G)||[""]).length;u--;)c=f=(o=Oe.exec(t[u])||[])[1],h=(o[2]||"").split(".").sort(),c&&(_=w.event.special[c]||{},c=(s?_.delegateType:_.bindType)||c,_=w.event.special[c]||{},l=w.extend({type:c,origType:f,data:a,handler:n,guid:n.guid,selector:s,needsContext:s&&w.expr.match.needsContext.test(s),namespace:h.join(".")},r),(m=d[c])||((m=d[c]=[]).delegateCount=0,_.setup&&!1!==_.setup.call(e,a,h,i)||e.addEventListener&&e.addEventListener(c,i)),_.add&&(_.add.call(e,l),l.handler.guid||(l.handler.guid=n.guid)),s?m.splice(m.delegateCount++,0,l):m.push(l),w.event.global[c]=!0)},remove:function(e,t,n,a,s){var r,i,o,d,u,l,_,m,c,h,f,p=oe.hasData(e)&&oe.get(e);if(p&&(d=p.events)){for(u=(t=(t||"").match(G)||[""]).length;u--;)if(c=f=(o=Oe.exec(t[u])||[])[1],h=(o[2]||"").split(".").sort(),c){for(_=w.event.special[c]||{},m=d[c=(a?_.delegateType:_.bindType)||c]||[],o=o[2]&&new RegExp("(^|\\.)"+h.join("\\.(?:.*\\.|)")+"(\\.|$)"),i=r=m.length;r--;)l=m[r],!s&&f!==l.origType||n&&n.guid!==l.guid||o&&!o.test(l.namespace)||a&&a!==l.selector&&("**"!==a||!l.selector)||(m.splice(r,1),l.selector&&m.delegateCount--,_.remove&&_.remove.call(e,l));i&&!m.length&&(_.teardown&&!1!==_.teardown.call(e,h,p.handle)||w.removeEvent(e,c,p.handle),delete d[c])}else for(c in d)w.event.remove(e,c+t[u],n,a,!0);w.isEmptyObject(d)&&oe.remove(e,"handle events")}},dispatch:function(e){var t,n,a,s,r,i,o=new Array(arguments.length),d=w.event.fix(e),u=(oe.get(this,"events")||Object.create(null))[d.type]||[],l=w.event.special[d.type]||{};for(o[0]=d,t=1;t=1))for(;u!==this;u=u.parentNode||this)if(1===u.nodeType&&("click"!==e.type||!0!==u.disabled)){for(r=[],i={},n=0;n-1:w.find(s,this,null,[u]).length),i[s]&&r.push(a);r.length&&o.push({elem:u,handlers:r})}return u=this,d\s*$/g;function Re(e,t){return b(e,"table")&&b(11!==t.nodeType?t:t.firstChild,"tr")&&w(e).children("tbody")[0]||e}function Ie(e){return e.type=(null!==e.getAttribute("type"))+"/"+e.type,e}function Je(e){return"true/"===(e.type||"").slice(0,5)?e.type=e.type.slice(5):e.removeAttribute("type"),e}function qe(e,t){var n,a,s,r,i,o;if(1===t.nodeType){if(oe.hasData(e)&&(o=oe.get(e).events))for(s in oe.remove(t,"handle events"),o)for(n=0,a=o[s].length;n1&&"string"==typeof h&&!p.checkClone&&Ne.test(h))return e.each((function(s){var r=e.eq(s);f&&(t[0]=h.call(this,s,r.html())),Be(r,t,n,a)}));if(m&&(r=(s=Pe(t,e[0].ownerDocument,!1,e,a)).firstChild,1===s.childNodes.length&&(s=r),r||a)){for(o=(i=w.map(He(s,"script"),Ie)).length;_0&&xe(i,!d&&He(e,"script")),o},cleanData:function(e){for(var t,n,a,s=w.event.special,r=0;void 0!==(n=e[r]);r++)if(re(n)){if(t=n[oe.expando]){if(t.events)for(a in t.events)s[a]?w.event.remove(n,a):w.removeEvent(n,a,t.handle);n[oe.expando]=void 0}n[de.expando]&&(n[de.expando]=void 0)}}}),w.fn.extend({detach:function(e){return Ge(this,e,!0)},remove:function(e){return Ge(this,e)},text:function(e){return ee(this,(function(e){return void 0===e?w.text(this):this.empty().each((function(){1!==this.nodeType&&11!==this.nodeType&&9!==this.nodeType||(this.textContent=e)}))}),null,e,arguments.length)},append:function(){return Be(this,arguments,(function(e){1!==this.nodeType&&11!==this.nodeType&&9!==this.nodeType||Re(this,e).appendChild(e)}))},prepend:function(){return Be(this,arguments,(function(e){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var t=Re(this,e);t.insertBefore(e,t.firstChild)}}))},before:function(){return Be(this,arguments,(function(e){this.parentNode&&this.parentNode.insertBefore(e,this)}))},after:function(){return Be(this,arguments,(function(e){this.parentNode&&this.parentNode.insertBefore(e,this.nextSibling)}))},empty:function(){for(var e,t=0;null!=(e=this[t]);t++)1===e.nodeType&&(w.cleanData(He(e,!1)),e.textContent="");return this},clone:function(e,t){return e=null!=e&&e,t=null==t?e:t,this.map((function(){return w.clone(this,e,t)}))},html:function(e){return ee(this,(function(e){var t=this[0]||{},n=0,a=this.length;if(void 0===e&&1===t.nodeType)return t.innerHTML;if("string"==typeof e&&!Fe.test(e)&&!Se[(Te.exec(e)||["",""])[1].toLowerCase()]){e=w.htmlPrefilter(e);try{for(;n=0&&(d+=Math.max(0,Math.ceil(e["offset"+t[0].toUpperCase()+t.slice(1)]-r-d-o-.5))||0),d+u}function lt(e,t,n){var a=Ke(e),s=(!p.boxSizingReliable()||n)&&"border-box"===w.css(e,"boxSizing",!1,a),r=s,i=Qe(e,t,a),o="offset"+t[0].toUpperCase()+t.slice(1);if(Ve.test(i)){if(!n)return i;i="auto"}return(!p.boxSizingReliable()&&s||!p.reliableTrDimensions()&&b(e,"tr")||"auto"===i||!parseFloat(i)&&"inline"===w.css(e,"display",!1,a))&&e.getClientRects().length&&(s="border-box"===w.css(e,"boxSizing",!1,a),(r=o in e)&&(i=e[o])),(i=parseFloat(i)||0)+ut(e,t,n||(s?"border":"content"),r,a,i)+"px"}function _t(e,t,n,a,s){return new _t.prototype.init(e,t,n,a,s)}w.extend({cssHooks:{opacity:{get:function(e,t){if(t){var n=Qe(e,"opacity");return""===n?"1":n}}}},cssNumber:{animationIterationCount:!0,aspectRatio:!0,borderImageSlice:!0,columnCount:!0,flexGrow:!0,flexShrink:!0,fontWeight:!0,gridArea:!0,gridColumn:!0,gridColumnEnd:!0,gridColumnStart:!0,gridRow:!0,gridRowEnd:!0,gridRowStart:!0,lineHeight:!0,opacity:!0,order:!0,orphans:!0,scale:!0,widows:!0,zIndex:!0,zoom:!0,fillOpacity:!0,floodOpacity:!0,stopOpacity:!0,strokeMiterlimit:!0,strokeOpacity:!0},cssProps:{},style:function(e,t,n,a){if(e&&3!==e.nodeType&&8!==e.nodeType&&e.style){var s,r,i,o=se(t),d=$e.test(t),u=e.style;if(d||(t=st(o)),i=w.cssHooks[t]||w.cssHooks[o],void 0===n)return i&&"get"in i&&void 0!==(s=i.get(e,!1,a))?s:u[t];"string"===(r=typeof n)&&(s=ce.exec(n))&&s[1]&&(n=Le(e,t,s),r="number"),null!=n&&n==n&&("number"!==r||d||(n+=s&&s[3]||(w.cssNumber[o]?"":"px")),p.clearCloneStyle||""!==n||0!==t.indexOf("background")||(u[t]="inherit"),i&&"set"in i&&void 0===(n=i.set(e,n,a))||(d?u.setProperty(t,n):u[t]=n))}},css:function(e,t,n,a){var s,r,i,o=se(t);return $e.test(t)||(t=st(o)),(i=w.cssHooks[t]||w.cssHooks[o])&&"get"in i&&(s=i.get(e,!0,n)),void 0===s&&(s=Qe(e,t,a)),"normal"===s&&t in ot&&(s=ot[t]),""===n||n?(r=parseFloat(s),!0===n||isFinite(r)?r||0:s):s}}),w.each(["height","width"],(function(e,t){w.cssHooks[t]={get:function(e,n,a){if(n)return!rt.test(w.css(e,"display"))||e.getClientRects().length&&e.getBoundingClientRect().width?lt(e,t,a):Ze(e,it,(function(){return lt(e,t,a)}))},set:function(e,n,a){var s,r=Ke(e),i=!p.scrollboxSize()&&"absolute"===r.position,o=(i||a)&&"border-box"===w.css(e,"boxSizing",!1,r),d=a?ut(e,t,a,o,r):0;return o&&i&&(d-=Math.ceil(e["offset"+t[0].toUpperCase()+t.slice(1)]-parseFloat(r[t])-ut(e,t,"border",!1,r)-.5)),d&&(s=ce.exec(n))&&"px"!==(s[3]||"px")&&(e.style[t]=n,n=w.css(e,t)),dt(0,n,d)}}})),w.cssHooks.marginLeft=et(p.reliableMarginLeft,(function(e,t){if(t)return(parseFloat(Qe(e,"marginLeft"))||e.getBoundingClientRect().left-Ze(e,{marginLeft:0},(function(){return e.getBoundingClientRect().left})))+"px"})),w.each({margin:"",padding:"",border:"Width"},(function(e,t){w.cssHooks[e+t]={expand:function(n){for(var a=0,s={},r="string"==typeof n?n.split(" "):[n];a<4;a++)s[e+he[a]+t]=r[a]||r[a-2]||r[0];return s}},"margin"!==e&&(w.cssHooks[e+t].set=dt)})),w.fn.extend({css:function(e,t){return ee(this,(function(e,t,n){var a,s,r={},i=0;if(Array.isArray(t)){for(a=Ke(e),s=t.length;i1)}}),w.Tween=_t,_t.prototype={constructor:_t,init:function(e,t,n,a,s,r){this.elem=e,this.prop=n,this.easing=s||w.easing._default,this.options=t,this.start=this.now=this.cur(),this.end=a,this.unit=r||(w.cssNumber[n]?"":"px")},cur:function(){var e=_t.propHooks[this.prop];return e&&e.get?e.get(this):_t.propHooks._default.get(this)},run:function(e){var t,n=_t.propHooks[this.prop];return this.options.duration?this.pos=t=w.easing[this.easing](e,this.options.duration*e,0,1,this.options.duration):this.pos=t=e,this.now=(this.end-this.start)*t+this.start,this.options.step&&this.options.step.call(this.elem,this.now,this),n&&n.set?n.set(this):_t.propHooks._default.set(this),this}},_t.prototype.init.prototype=_t.prototype,_t.propHooks={_default:{get:function(e){var t;return 1!==e.elem.nodeType||null!=e.elem[e.prop]&&null==e.elem.style[e.prop]?e.elem[e.prop]:(t=w.css(e.elem,e.prop,""))&&"auto"!==t?t:0},set:function(e){w.fx.step[e.prop]?w.fx.step[e.prop](e):1!==e.elem.nodeType||!w.cssHooks[e.prop]&&null==e.elem.style[st(e.prop)]?e.elem[e.prop]=e.now:w.style(e.elem,e.prop,e.now+e.unit)}}},_t.propHooks.scrollTop=_t.propHooks.scrollLeft={set:function(e){e.elem.nodeType&&e.elem.parentNode&&(e.elem[e.prop]=e.now)}},w.easing={linear:function(e){return e},swing:function(e){return.5-Math.cos(e*Math.PI)/2},_default:"swing"},w.fx=_t.prototype.init,w.fx.step={};var mt,ct,ht=/^(?:toggle|show|hide)$/,ft=/queueHooks$/;function pt(){ct&&(!1===L.hidden&&a.requestAnimationFrame?a.requestAnimationFrame(pt):a.setTimeout(pt,w.fx.interval),w.fx.tick())}function Mt(){return a.setTimeout((function(){mt=void 0})),mt=Date.now()}function yt(e,t){var n,a=0,s={height:e};for(t=t?1:0;a<4;a+=2-t)s["margin"+(n=he[a])]=s["padding"+n]=e;return t&&(s.opacity=s.width=e),s}function Lt(e,t,n){for(var a,s=(Yt.tweeners[t]||[]).concat(Yt.tweeners["*"]),r=0,i=s.length;r1)},removeAttr:function(e){return this.each((function(){w.removeAttr(this,e)}))}}),w.extend({attr:function(e,t,n){var a,s,r=e.nodeType;if(3!==r&&8!==r&&2!==r)return void 0===e.getAttribute?w.prop(e,t,n):(1===r&&w.isXMLDoc(e)||(s=w.attrHooks[t.toLowerCase()]||(w.expr.match.bool.test(t)?gt:void 0)),void 0!==n?null===n?void w.removeAttr(e,t):s&&"set"in s&&void 0!==(a=s.set(e,n,t))?a:(e.setAttribute(t,n+""),n):s&&"get"in s&&null!==(a=s.get(e,t))?a:null==(a=w.find.attr(e,t))?void 0:a)},attrHooks:{type:{set:function(e,t){if(!p.radioValue&&"radio"===t&&b(e,"input")){var n=e.value;return e.setAttribute("type",t),n&&(e.value=n),t}}}},removeAttr:function(e,t){var n,a=0,s=t&&t.match(G);if(s&&1===e.nodeType)for(;n=s[a++];)e.removeAttribute(n)}}),gt={set:function(e,t,n){return!1===t?w.removeAttr(e,n):e.setAttribute(n,n),n}},w.each(w.expr.match.bool.source.match(/\w+/g),(function(e,t){var n=kt[t]||w.find.attr;kt[t]=function(e,t,a){var s,r,i=t.toLowerCase();return a||(r=kt[i],kt[i]=s,s=null!=n(e,t,a)?i:null,kt[i]=r),s}}));var Dt=/^(?:input|select|textarea|button)$/i,vt=/^(?:a|area)$/i;function wt(e){return(e.match(G)||[]).join(" ")}function Tt(e){return e.getAttribute&&e.getAttribute("class")||""}function bt(e){return Array.isArray(e)?e:"string"==typeof e&&e.match(G)||[]}w.fn.extend({prop:function(e,t){return ee(this,w.prop,e,t,arguments.length>1)},removeProp:function(e){return this.each((function(){delete this[w.propFix[e]||e]}))}}),w.extend({prop:function(e,t,n){var a,s,r=e.nodeType;if(3!==r&&8!==r&&2!==r)return 1===r&&w.isXMLDoc(e)||(t=w.propFix[t]||t,s=w.propHooks[t]),void 0!==n?s&&"set"in s&&void 0!==(a=s.set(e,n,t))?a:e[t]=n:s&&"get"in s&&null!==(a=s.get(e,t))?a:e[t]},propHooks:{tabIndex:{get:function(e){var t=w.find.attr(e,"tabindex");return t?parseInt(t,10):Dt.test(e.nodeName)||vt.test(e.nodeName)&&e.href?0:-1}}},propFix:{for:"htmlFor",class:"className"}}),p.optSelected||(w.propHooks.selected={get:function(e){var t=e.parentNode;return t&&t.parentNode&&t.parentNode.selectedIndex,null},set:function(e){var t=e.parentNode;t&&(t.selectedIndex,t.parentNode&&t.parentNode.selectedIndex)}}),w.each(["tabIndex","readOnly","maxLength","cellSpacing","cellPadding","rowSpan","colSpan","useMap","frameBorder","contentEditable"],(function(){w.propFix[this.toLowerCase()]=this})),w.fn.extend({addClass:function(e){var t,n,a,s,r,i;return M(e)?this.each((function(t){w(this).addClass(e.call(this,t,Tt(this)))})):(t=bt(e)).length?this.each((function(){if(a=Tt(this),n=1===this.nodeType&&" "+wt(a)+" "){for(r=0;r-1;)n=n.replace(" "+s+" "," ");i=wt(n),a!==i&&this.setAttribute("class",i)}})):this:this.attr("class","")},toggleClass:function(e,t){var n,a,s,r,i=typeof e,o="string"===i||Array.isArray(e);return M(e)?this.each((function(n){w(this).toggleClass(e.call(this,n,Tt(this),t),t)})):"boolean"==typeof t&&o?t?this.addClass(e):this.removeClass(e):(n=bt(e),this.each((function(){if(o)for(r=w(this),s=0;s-1)return!0;return!1}});var St=/\r/g;w.fn.extend({val:function(e){var t,n,a,s=this[0];return arguments.length?(a=M(e),this.each((function(n){var s;1===this.nodeType&&(null==(s=a?e.call(this,n,w(this).val()):e)?s="":"number"==typeof s?s+="":Array.isArray(s)&&(s=w.map(s,(function(e){return null==e?"":e+""}))),(t=w.valHooks[this.type]||w.valHooks[this.nodeName.toLowerCase()])&&"set"in t&&void 0!==t.set(this,s,"value")||(this.value=s))}))):s?(t=w.valHooks[s.type]||w.valHooks[s.nodeName.toLowerCase()])&&"get"in t&&void 0!==(n=t.get(s,"value"))?n:"string"==typeof(n=s.value)?n.replace(St,""):null==n?"":n:void 0}}),w.extend({valHooks:{option:{get:function(e){var t=w.find.attr(e,"value");return null!=t?t:wt(w.text(e))}},select:{get:function(e){var t,n,a,s=e.options,r=e.selectedIndex,i="select-one"===e.type,o=i?null:[],d=i?r+1:s.length;for(a=r<0?d:i?r:0;a-1)&&(n=!0);return n||(e.selectedIndex=-1),r}}}}),w.each(["radio","checkbox"],(function(){w.valHooks[this]={set:function(e,t){if(Array.isArray(t))return e.checked=w.inArray(w(e).val(),t)>-1}},p.checkOn||(w.valHooks[this].get=function(e){return null===e.getAttribute("value")?"on":e.value})}));var Ht=a.location,xt={guid:Date.now()},jt=/\?/;w.parseXML=function(e){var t,n;if(!e||"string"!=typeof e)return null;try{t=(new a.DOMParser).parseFromString(e,"text/xml")}catch(e){}return n=t&&t.getElementsByTagName("parsererror")[0],t&&!n||w.error("Invalid XML: "+(n?w.map(n.childNodes,(function(e){return e.textContent})).join("\n"):e)),t};var Pt=/^(?:focusinfocus|focusoutblur)$/,Ot=function(e){e.stopPropagation()};w.extend(w.event,{trigger:function(e,t,n,s){var r,i,o,d,u,l,_,m,h=[n||L],f=c.call(e,"type")?e.type:e,p=c.call(e,"namespace")?e.namespace.split("."):[];if(i=m=o=n=n||L,3!==n.nodeType&&8!==n.nodeType&&!Pt.test(f+w.event.triggered)&&(f.indexOf(".")>-1&&(p=f.split("."),f=p.shift(),p.sort()),u=f.indexOf(":")<0&&"on"+f,(e=e[w.expando]?e:new w.Event(f,"object"==typeof e&&e)).isTrigger=s?2:3,e.namespace=p.join("."),e.rnamespace=e.namespace?new RegExp("(^|\\.)"+p.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,e.result=void 0,e.target||(e.target=n),t=null==t?[e]:w.makeArray(t,[e]),_=w.event.special[f]||{},s||!_.trigger||!1!==_.trigger.apply(n,t))){if(!s&&!_.noBubble&&!y(n)){for(d=_.delegateType||f,Pt.test(d+f)||(i=i.parentNode);i;i=i.parentNode)h.push(i),o=i;o===(n.ownerDocument||L)&&h.push(o.defaultView||o.parentWindow||a)}for(r=0;(i=h[r++])&&!e.isPropagationStopped();)m=i,e.type=r>1?d:_.bindType||f,(l=(oe.get(i,"events")||Object.create(null))[e.type]&&oe.get(i,"handle"))&&l.apply(i,t),(l=u&&i[u])&&l.apply&&re(i)&&(e.result=l.apply(i,t),!1===e.result&&e.preventDefault());return e.type=f,s||e.isDefaultPrevented()||_._default&&!1!==_._default.apply(h.pop(),t)||!re(n)||u&&M(n[f])&&!y(n)&&((o=n[u])&&(n[u]=null),w.event.triggered=f,e.isPropagationStopped()&&m.addEventListener(f,Ot),n[f](),e.isPropagationStopped()&&m.removeEventListener(f,Ot),w.event.triggered=void 0,o&&(n[u]=o)),e.result}},simulate:function(e,t,n){var a=w.extend(new w.Event,n,{type:e,isSimulated:!0});w.event.trigger(a,null,t)}}),w.fn.extend({trigger:function(e,t){return this.each((function(){w.event.trigger(e,t,this)}))},triggerHandler:function(e,t){var n=this[0];if(n)return w.event.trigger(e,t,n,!0)}});var Et=/\[\]$/,Wt=/\r?\n/g,At=/^(?:submit|button|image|reset|file)$/i,Ct=/^(?:input|select|textarea|keygen)/i;function Ft(e,t,n,a){var s;if(Array.isArray(t))w.each(t,(function(t,s){n||Et.test(e)?a(e,s):Ft(e+"["+("object"==typeof s&&null!=s?t:"")+"]",s,n,a)}));else if(n||"object"!==k(t))a(e,t);else for(s in t)Ft(e+"["+s+"]",t[s],n,a)}w.param=function(e,t){var n,a=[],s=function(e,t){var n=M(t)?t():t;a[a.length]=encodeURIComponent(e)+"="+encodeURIComponent(null==n?"":n)};if(null==e)return"";if(Array.isArray(e)||e.jquery&&!w.isPlainObject(e))w.each(e,(function(){s(this.name,this.value)}));else for(n in e)Ft(n,e[n],t,s);return a.join("&")},w.fn.extend({serialize:function(){return w.param(this.serializeArray())},serializeArray:function(){return this.map((function(){var e=w.prop(this,"elements");return e?w.makeArray(e):this})).filter((function(){var e=this.type;return this.name&&!w(this).is(":disabled")&&Ct.test(this.nodeName)&&!At.test(e)&&(this.checked||!we.test(e))})).map((function(e,t){var n=w(this).val();return null==n?null:Array.isArray(n)?w.map(n,(function(e){return{name:t.name,value:e.replace(Wt,"\r\n")}})):{name:t.name,value:n.replace(Wt,"\r\n")}})).get()}});var Nt=/%20/g,zt=/#.*$/,Rt=/([?&])_=[^&]*/,It=/^(.*?):[ \t]*([^\r\n]*)$/gm,Jt=/^(?:GET|HEAD)$/,qt=/^\/\//,Ut={},Bt={},Gt="*/".concat("*"),Vt=L.createElement("a");function $t(e){return function(t,n){"string"!=typeof t&&(n=t,t="*");var a,s=0,r=t.toLowerCase().match(G)||[];if(M(n))for(;a=r[s++];)"+"===a[0]?(a=a.slice(1)||"*",(e[a]=e[a]||[]).unshift(n)):(e[a]=e[a]||[]).push(n)}}function Kt(e,t,n,a){var s={},r=e===Bt;function i(o){var d;return s[o]=!0,w.each(e[o]||[],(function(e,o){var u=o(t,n,a);return"string"!=typeof u||r||s[u]?r?!(d=u):void 0:(t.dataTypes.unshift(u),i(u),!1)})),d}return i(t.dataTypes[0])||!s["*"]&&i("*")}function Zt(e,t){var n,a,s=w.ajaxSettings.flatOptions||{};for(n in t)void 0!==t[n]&&((s[n]?e:a||(a={}))[n]=t[n]);return a&&w.extend(!0,e,a),e}Vt.href=Ht.href,w.extend({active:0,lastModified:{},etag:{},ajaxSettings:{url:Ht.href,type:"GET",isLocal:/^(?:about|app|app-storage|.+-extension|file|res|widget):$/.test(Ht.protocol),global:!0,processData:!0,async:!0,contentType:"application/x-www-form-urlencoded; charset=UTF-8",accepts:{"*":Gt,text:"text/plain",html:"text/html",xml:"application/xml, text/xml",json:"application/json, text/javascript"},contents:{xml:/\bxml\b/,html:/\bhtml/,json:/\bjson\b/},responseFields:{xml:"responseXML",text:"responseText",json:"responseJSON"},converters:{"* text":String,"text html":!0,"text json":JSON.parse,"text xml":w.parseXML},flatOptions:{url:!0,context:!0}},ajaxSetup:function(e,t){return t?Zt(Zt(e,w.ajaxSettings),t):Zt(w.ajaxSettings,e)},ajaxPrefilter:$t(Ut),ajaxTransport:$t(Bt),ajax:function(e,t){"object"==typeof e&&(t=e,e=void 0),t=t||{};var n,s,r,i,o,d,u,l,_,m,c=w.ajaxSetup({},t),h=c.context||c,f=c.context&&(h.nodeType||h.jquery)?w(h):w.event,p=w.Deferred(),M=w.Callbacks("once memory"),y=c.statusCode||{},Y={},g={},k="canceled",D={readyState:0,getResponseHeader:function(e){var t;if(u){if(!i)for(i={};t=It.exec(r);)i[t[1].toLowerCase()+" "]=(i[t[1].toLowerCase()+" "]||[]).concat(t[2]);t=i[e.toLowerCase()+" "]}return null==t?null:t.join(", ")},getAllResponseHeaders:function(){return u?r:null},setRequestHeader:function(e,t){return null==u&&(e=g[e.toLowerCase()]=g[e.toLowerCase()]||e,Y[e]=t),this},overrideMimeType:function(e){return null==u&&(c.mimeType=e),this},statusCode:function(e){var t;if(e)if(u)D.always(e[D.status]);else for(t in e)y[t]=[y[t],e[t]];return this},abort:function(e){var t=e||k;return n&&n.abort(t),v(0,t),this}};if(p.promise(D),c.url=((e||c.url||Ht.href)+"").replace(qt,Ht.protocol+"//"),c.type=t.method||t.type||c.method||c.type,c.dataTypes=(c.dataType||"*").toLowerCase().match(G)||[""],null==c.crossDomain){d=L.createElement("a");try{d.href=c.url,d.href=d.href,c.crossDomain=Vt.protocol+"//"+Vt.host!=d.protocol+"//"+d.host}catch(e){c.crossDomain=!0}}if(c.data&&c.processData&&"string"!=typeof c.data&&(c.data=w.param(c.data,c.traditional)),Kt(Ut,c,t,D),u)return D;for(_ in(l=w.event&&c.global)&&0==w.active++&&w.event.trigger("ajaxStart"),c.type=c.type.toUpperCase(),c.hasContent=!Jt.test(c.type),s=c.url.replace(zt,""),c.hasContent?c.data&&c.processData&&0===(c.contentType||"").indexOf("application/x-www-form-urlencoded")&&(c.data=c.data.replace(Nt,"+")):(m=c.url.slice(s.length),c.data&&(c.processData||"string"==typeof c.data)&&(s+=(jt.test(s)?"&":"?")+c.data,delete c.data),!1===c.cache&&(s=s.replace(Rt,"$1"),m=(jt.test(s)?"&":"?")+"_="+xt.guid+++m),c.url=s+m),c.ifModified&&(w.lastModified[s]&&D.setRequestHeader("If-Modified-Since",w.lastModified[s]),w.etag[s]&&D.setRequestHeader("If-None-Match",w.etag[s])),(c.data&&c.hasContent&&!1!==c.contentType||t.contentType)&&D.setRequestHeader("Content-Type",c.contentType),D.setRequestHeader("Accept",c.dataTypes[0]&&c.accepts[c.dataTypes[0]]?c.accepts[c.dataTypes[0]]+("*"!==c.dataTypes[0]?", "+Gt+"; q=0.01":""):c.accepts["*"]),c.headers)D.setRequestHeader(_,c.headers[_]);if(c.beforeSend&&(!1===c.beforeSend.call(h,D,c)||u))return D.abort();if(k="abort",M.add(c.complete),D.done(c.success),D.fail(c.error),n=Kt(Bt,c,t,D)){if(D.readyState=1,l&&f.trigger("ajaxSend",[D,c]),u)return D;c.async&&c.timeout>0&&(o=a.setTimeout((function(){D.abort("timeout")}),c.timeout));try{u=!1,n.send(Y,v)}catch(e){if(u)throw e;v(-1,e)}}else v(-1,"No Transport");function v(e,t,i,d){var _,m,L,Y,g,k=t;u||(u=!0,o&&a.clearTimeout(o),n=void 0,r=d||"",D.readyState=e>0?4:0,_=e>=200&&e<300||304===e,i&&(Y=function(e,t,n){for(var a,s,r,i,o=e.contents,d=e.dataTypes;"*"===d[0];)d.shift(),void 0===a&&(a=e.mimeType||t.getResponseHeader("Content-Type"));if(a)for(s in o)if(o[s]&&o[s].test(a)){d.unshift(s);break}if(d[0]in n)r=d[0];else{for(s in n){if(!d[0]||e.converters[s+" "+d[0]]){r=s;break}i||(i=s)}r=r||i}if(r)return r!==d[0]&&d.unshift(r),n[r]}(c,D,i)),!_&&w.inArray("script",c.dataTypes)>-1&&w.inArray("json",c.dataTypes)<0&&(c.converters["text script"]=function(){}),Y=function(e,t,n,a){var s,r,i,o,d,u={},l=e.dataTypes.slice();if(l[1])for(i in e.converters)u[i.toLowerCase()]=e.converters[i];for(r=l.shift();r;)if(e.responseFields[r]&&(n[e.responseFields[r]]=t),!d&&a&&e.dataFilter&&(t=e.dataFilter(t,e.dataType)),d=r,r=l.shift())if("*"===r)r=d;else if("*"!==d&&d!==r){if(!(i=u[d+" "+r]||u["* "+r]))for(s in u)if((o=s.split(" "))[1]===r&&(i=u[d+" "+o[0]]||u["* "+o[0]])){!0===i?i=u[s]:!0!==u[s]&&(r=o[0],l.unshift(o[1]));break}if(!0!==i)if(i&&e.throws)t=i(t);else try{t=i(t)}catch(e){return{state:"parsererror",error:i?e:"No conversion from "+d+" to "+r}}}return{state:"success",data:t}}(c,Y,D,_),_?(c.ifModified&&((g=D.getResponseHeader("Last-Modified"))&&(w.lastModified[s]=g),(g=D.getResponseHeader("etag"))&&(w.etag[s]=g)),204===e||"HEAD"===c.type?k="nocontent":304===e?k="notmodified":(k=Y.state,m=Y.data,_=!(L=Y.error))):(L=k,!e&&k||(k="error",e<0&&(e=0))),D.status=e,D.statusText=(t||k)+"",_?p.resolveWith(h,[m,k,D]):p.rejectWith(h,[D,k,L]),D.statusCode(y),y=void 0,l&&f.trigger(_?"ajaxSuccess":"ajaxError",[D,c,_?m:L]),M.fireWith(h,[D,k]),l&&(f.trigger("ajaxComplete",[D,c]),--w.active||w.event.trigger("ajaxStop")))}return D},getJSON:function(e,t,n){return w.get(e,t,n,"json")},getScript:function(e,t){return w.get(e,void 0,t,"script")}}),w.each(["get","post"],(function(e,t){w[t]=function(e,n,a,s){return M(n)&&(s=s||a,a=n,n=void 0),w.ajax(w.extend({url:e,type:t,dataType:s,data:n,success:a},w.isPlainObject(e)&&e))}})),w.ajaxPrefilter((function(e){var t;for(t in e.headers)"content-type"===t.toLowerCase()&&(e.contentType=e.headers[t]||"")})),w._evalUrl=function(e,t,n){return w.ajax({url:e,type:"GET",dataType:"script",cache:!0,async:!1,global:!1,converters:{"text script":function(){}},dataFilter:function(e){w.globalEval(e,t,n)}})},w.fn.extend({wrapAll:function(e){var t;return this[0]&&(M(e)&&(e=e.call(this[0])),t=w(e,this[0].ownerDocument).eq(0).clone(!0),this[0].parentNode&&t.insertBefore(this[0]),t.map((function(){for(var e=this;e.firstElementChild;)e=e.firstElementChild;return e})).append(this)),this},wrapInner:function(e){return M(e)?this.each((function(t){w(this).wrapInner(e.call(this,t))})):this.each((function(){var t=w(this),n=t.contents();n.length?n.wrapAll(e):t.append(e)}))},wrap:function(e){var t=M(e);return this.each((function(n){w(this).wrapAll(t?e.call(this,n):e)}))},unwrap:function(e){return this.parent(e).not("body").each((function(){w(this).replaceWith(this.childNodes)})),this}}),w.expr.pseudos.hidden=function(e){return!w.expr.pseudos.visible(e)},w.expr.pseudos.visible=function(e){return!!(e.offsetWidth||e.offsetHeight||e.getClientRects().length)},w.ajaxSettings.xhr=function(){try{return new a.XMLHttpRequest}catch(e){}};var Xt={0:200,1223:204},Qt=w.ajaxSettings.xhr();p.cors=!!Qt&&"withCredentials"in Qt,p.ajax=Qt=!!Qt,w.ajaxTransport((function(e){var t,n;if(p.cors||Qt&&!e.crossDomain)return{send:function(s,r){var i,o=e.xhr();if(o.open(e.type,e.url,e.async,e.username,e.password),e.xhrFields)for(i in e.xhrFields)o[i]=e.xhrFields[i];for(i in e.mimeType&&o.overrideMimeType&&o.overrideMimeType(e.mimeType),e.crossDomain||s["X-Requested-With"]||(s["X-Requested-With"]="XMLHttpRequest"),s)o.setRequestHeader(i,s[i]);t=function(e){return function(){t&&(t=n=o.onload=o.onerror=o.onabort=o.ontimeout=o.onreadystatechange=null,"abort"===e?o.abort():"error"===e?"number"!=typeof o.status?r(0,"error"):r(o.status,o.statusText):r(Xt[o.status]||o.status,o.statusText,"text"!==(o.responseType||"text")||"string"!=typeof o.responseText?{binary:o.response}:{text:o.responseText},o.getAllResponseHeaders()))}},o.onload=t(),n=o.onerror=o.ontimeout=t("error"),void 0!==o.onabort?o.onabort=n:o.onreadystatechange=function(){4===o.readyState&&a.setTimeout((function(){t&&n()}))},t=t("abort");try{o.send(e.hasContent&&e.data||null)}catch(e){if(t)throw e}},abort:function(){t&&t()}}})),w.ajaxPrefilter((function(e){e.crossDomain&&(e.contents.script=!1)})),w.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/\b(?:java|ecma)script\b/},converters:{"text script":function(e){return w.globalEval(e),e}}}),w.ajaxPrefilter("script",(function(e){void 0===e.cache&&(e.cache=!1),e.crossDomain&&(e.type="GET")})),w.ajaxTransport("script",(function(e){var t,n;if(e.crossDomain||e.scriptAttrs)return{send:function(a,s){t=w("