Add avatar providers (#1192)

* Add avatar providers

* fix exists check for local avatar

* Use avatar in user lists

---------

Co-authored-by: Charles <charles@pelican.dev>
This commit is contained in:
Boy132 2025-04-07 16:06:19 +02:00 committed by GitHub
parent 377b3f170d
commit fa8ae0aea5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 221 additions and 36 deletions

View File

@ -0,0 +1,40 @@
<?php
namespace App\Extensions\Avatar;
use Filament\AvatarProviders\Contracts\AvatarProvider as AvatarProviderContract;
use Illuminate\Support\Arr;
use Illuminate\Support\Str;
abstract class AvatarProvider implements AvatarProviderContract
{
/**
* @var array<string, static>
*/
protected static array $providers = [];
public static function getProvider(string $id): ?self
{
return Arr::get(static::$providers, $id);
}
/**
* @return array<string, static>
*/
public static function getAll(): array
{
return static::$providers;
}
public function __construct()
{
static::$providers[$this->getId()] = $this;
}
abstract public function getId(): string;
public function getName(): string
{
return Str::title($this->getId());
}
}

View File

@ -0,0 +1,27 @@
<?php
namespace App\Extensions\Avatar\Providers;
use App\Extensions\Avatar\AvatarProvider;
use App\Models\User;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Database\Eloquent\Model;
class GravatarProvider extends AvatarProvider
{
public function getId(): string
{
return 'gravatar';
}
public function get(Model|Authenticatable $record): string
{
/** @var User $record */
return 'https://gravatar.com/avatar/' . md5($record->email);
}
public static function register(): self
{
return new self();
}
}

View File

@ -0,0 +1,29 @@
<?php
namespace App\Extensions\Avatar\Providers;
use App\Extensions\Avatar\AvatarProvider;
use Filament\AvatarProviders\UiAvatarsProvider as FilamentUiAvatarsProvider;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Storage;
class LocalAvatarProvider extends AvatarProvider
{
public function getId(): string
{
return 'local';
}
public function get(Model|Authenticatable $record): string
{
$path = 'avatars/' . $record->getKey() . '.png';
return Storage::disk('public')->exists($path) ? Storage::url($path) : (new FilamentUiAvatarsProvider())->get($record);
}
public static function register(): self
{
return new self();
}
}

View File

@ -0,0 +1,31 @@
<?php
namespace App\Extensions\Avatar\Providers;
use App\Extensions\Avatar\AvatarProvider;
use Filament\AvatarProviders\UiAvatarsProvider as FilamentUiAvatarsProvider;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Database\Eloquent\Model;
class UiAvatarsProvider extends AvatarProvider
{
public function getId(): string
{
return 'uiavatars';
}
public function getName(): string
{
return 'UI Avatars';
}
public function get(Model|Authenticatable $record): string
{
return (new FilamentUiAvatarsProvider())->get($record);
}
public static function register(): self
{
return new self();
}
}

View File

@ -2,6 +2,7 @@
namespace App\Filament\Admin\Pages; namespace App\Filament\Admin\Pages;
use App\Extensions\Avatar\AvatarProvider;
use App\Extensions\Captcha\Providers\CaptchaProvider; use App\Extensions\Captcha\Providers\CaptchaProvider;
use App\Extensions\OAuth\Providers\OAuthProvider; use App\Extensions\OAuth\Providers\OAuthProvider;
use App\Models\Backup; use App\Models\Backup;
@ -134,26 +135,38 @@ class Settings extends Page implements HasForms
->default(env('APP_FAVICON', '/pelican.ico')) ->default(env('APP_FAVICON', '/pelican.ico'))
->placeholder('/pelican.ico'), ->placeholder('/pelican.ico'),
]), ]),
Toggle::make('APP_DEBUG') Group::make()
->label(trans('admin/setting.general.debug_mode')) ->columnSpan(2)
->inline(false) ->columns(4)
->onIcon('tabler-check') ->schema([
->offIcon('tabler-x') Toggle::make('APP_DEBUG')
->onColor('success') ->label(trans('admin/setting.general.debug_mode'))
->offColor('danger') ->inline(false)
->formatStateUsing(fn ($state): bool => (bool) $state) ->onIcon('tabler-check')
->afterStateUpdated(fn ($state, Set $set) => $set('APP_DEBUG', (bool) $state)) ->offIcon('tabler-x')
->default(env('APP_DEBUG', config('app.debug'))), ->onColor('success')
ToggleButtons::make('FILAMENT_TOP_NAVIGATION') ->offColor('danger')
->label(trans('admin/setting.general.navigation')) ->formatStateUsing(fn ($state): bool => (bool) $state)
->inline() ->afterStateUpdated(fn ($state, Set $set) => $set('APP_DEBUG', (bool) $state))
->options([ ->default(env('APP_DEBUG', config('app.debug'))),
false => trans('admin/setting.general.sidebar'), ToggleButtons::make('FILAMENT_TOP_NAVIGATION')
true => trans('admin/setting.general.topbar'), ->label(trans('admin/setting.general.navigation'))
]) ->inline()
->formatStateUsing(fn ($state): bool => (bool) $state) ->options([
->afterStateUpdated(fn ($state, Set $set) => $set('FILAMENT_TOP_NAVIGATION', (bool) $state)) false => trans('admin/setting.general.sidebar'),
->default(env('FILAMENT_TOP_NAVIGATION', config('panel.filament.top-navigation'))), true => trans('admin/setting.general.topbar'),
])
->formatStateUsing(fn ($state): bool => (bool) $state)
->afterStateUpdated(fn ($state, Set $set) => $set('FILAMENT_TOP_NAVIGATION', (bool) $state))
->default(env('FILAMENT_TOP_NAVIGATION', config('panel.filament.top-navigation'))),
Select::make('FILAMENT_AVATAR_PROVIDER')
->label(trans('admin/setting.general.avatar_provider'))
->columnSpan(2)
->native(false)
->options(collect(AvatarProvider::getAll())->mapWithKeys(fn ($provider) => [$provider->getId() => $provider->getName()]))
->selectablePlaceholder(false)
->default(env('FILAMENT_AVATAR_PROVIDER', config('panel.filament.avatar-provider'))),
]),
ToggleButtons::make('PANEL_USE_BINARY_PREFIX') ToggleButtons::make('PANEL_USE_BINARY_PREFIX')
->label(trans('admin/setting.general.unit_prefix')) ->label(trans('admin/setting.general.unit_prefix'))
->inline() ->inline()

View File

@ -6,6 +6,7 @@ use App\Filament\Admin\Resources\UserResource\Pages;
use App\Filament\Admin\Resources\UserResource\RelationManagers; use App\Filament\Admin\Resources\UserResource\RelationManagers;
use App\Models\Role; use App\Models\Role;
use App\Models\User; use App\Models\User;
use Filament\Facades\Filament;
use Filament\Forms\Components\CheckboxList; use Filament\Forms\Components\CheckboxList;
use Filament\Forms\Components\TextInput; use Filament\Forms\Components\TextInput;
use Filament\Forms\Form; use Filament\Forms\Form;
@ -58,8 +59,9 @@ class UserResource extends Resource
ImageColumn::make('picture') ImageColumn::make('picture')
->visibleFrom('lg') ->visibleFrom('lg')
->label('') ->label('')
->extraImgAttributes(['class' => 'rounded-full']) ->circular()
->defaultImageUrl(fn (User $user) => 'https://gravatar.com/avatar/' . md5(strtolower($user->email))), ->alignCenter()
->defaultImageUrl(fn (User $user) => Filament::getUserAvatarUrl($user)),
TextColumn::make('username') TextColumn::make('username')
->label(trans('admin/user.username')), ->label(trans('admin/user.username')),
TextColumn::make('email') TextColumn::make('email')

View File

@ -19,6 +19,7 @@ use chillerlan\QRCode\QROptions;
use DateTimeZone; use DateTimeZone;
use Filament\Forms\Components\Actions; use Filament\Forms\Components\Actions;
use Filament\Forms\Components\Actions\Action; use Filament\Forms\Components\Actions\Action;
use Filament\Forms\Components\FileUpload;
use Filament\Forms\Components\Grid; use Filament\Forms\Components\Grid;
use Filament\Forms\Components\Placeholder; use Filament\Forms\Components\Placeholder;
use Filament\Forms\Components\Repeater; use Filament\Forms\Components\Repeater;
@ -126,6 +127,11 @@ class EditProfile extends BaseEditProfile
->helperText(fn ($state, LanguageService $languageService) => new HtmlString($languageService->isLanguageTranslated($state) ? '' : trans('profile.language_help', ['state' => $state]))) ->helperText(fn ($state, LanguageService $languageService) => new HtmlString($languageService->isLanguageTranslated($state) ? '' : trans('profile.language_help', ['state' => $state])))
->options(fn (LanguageService $languageService) => $languageService->getAvailableLanguages()) ->options(fn (LanguageService $languageService) => $languageService->getAvailableLanguages())
->native(false), ->native(false),
FileUpload::make('avatar')
->visible(fn () => config('panel.filament.avatar-provider') === 'local')
->avatar()
->directory('avatars')
->getUploadedFileNameForStorageUsing(fn () => $this->getUser()->id . '.png'),
]), ]),
Tab::make(trans('profile.tabs.oauth')) Tab::make(trans('profile.tabs.oauth'))

View File

@ -90,8 +90,8 @@ class UserResource extends Resource
ImageColumn::make('picture') ImageColumn::make('picture')
->visibleFrom('lg') ->visibleFrom('lg')
->label('') ->label('')
->extraImgAttributes(['class' => 'rounded-full']) ->alignCenter()->circular()
->defaultImageUrl(fn (User $user) => 'https://gravatar.com/avatar/' . md5(strtolower($user->email))), ->defaultImageUrl(fn (User $user) => Filament::getUserAvatarUrl($user)),
TextColumn::make('username') TextColumn::make('username')
->searchable(), ->searchable(),
TextColumn::make('email') TextColumn::make('email')

View File

@ -6,6 +6,7 @@ use App\Traits\HasValidation;
use Carbon\Carbon; use Carbon\Carbon;
use Illuminate\Support\Facades\Event; use Illuminate\Support\Facades\Event;
use App\Events\ActivityLogged; use App\Events\ActivityLogged;
use Filament\Facades\Filament;
use Filament\Support\Contracts\HasIcon; use Filament\Support\Contracts\HasIcon;
use Filament\Support\Contracts\HasLabel; use Filament\Support\Contracts\HasLabel;
use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Builder;
@ -172,9 +173,11 @@ class ActivityLog extends Model implements HasIcon, HasLabel
]); ]);
} }
$avatarUrl = Filament::getUserAvatarUrl($user);
return " return "
<div style='display: flex; align-items: center;'> <div style='display: flex; align-items: center;'>
<img width='50px' height='50px' src='{$user->getFilamentAvatarUrl()}' style='margin-right: 15px' /> <img width='50px' height='50px' src='{$avatarUrl}' style='margin-right: 15px' />
<div> <div>
<p>$user->username $this->event</p> <p>$user->username $this->event</p>

View File

@ -9,7 +9,6 @@ use App\Facades\Activity;
use App\Traits\HasValidation; use App\Traits\HasValidation;
use DateTimeZone; use DateTimeZone;
use Filament\Models\Contracts\FilamentUser; use Filament\Models\Contracts\FilamentUser;
use Filament\Models\Contracts\HasAvatar;
use Filament\Models\Contracts\HasName; use Filament\Models\Contracts\HasName;
use Filament\Models\Contracts\HasTenants; use Filament\Models\Contracts\HasTenants;
use Filament\Panel; use Filament\Panel;
@ -50,7 +49,6 @@ use Spatie\Permission\Traits\HasRoles;
* @property string|null $totp_secret * @property string|null $totp_secret
* @property \Illuminate\Support\Carbon|null $totp_authenticated_at * @property \Illuminate\Support\Carbon|null $totp_authenticated_at
* @property string[]|null $oauth * @property string[]|null $oauth
* @property bool $gravatar
* @property \Illuminate\Support\Carbon|null $created_at * @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at * @property \Illuminate\Support\Carbon|null $updated_at
* @property \Illuminate\Database\Eloquent\Collection|\App\Models\ApiKey[] $apiKeys * @property \Illuminate\Database\Eloquent\Collection|\App\Models\ApiKey[] $apiKeys
@ -77,7 +75,6 @@ use Spatie\Permission\Traits\HasRoles;
* @method static Builder|User whereCreatedAt($value) * @method static Builder|User whereCreatedAt($value)
* @method static Builder|User whereEmail($value) * @method static Builder|User whereEmail($value)
* @method static Builder|User whereExternalId($value) * @method static Builder|User whereExternalId($value)
* @method static Builder|User whereGravatar($value)
* @method static Builder|User whereId($value) * @method static Builder|User whereId($value)
* @method static Builder|User whereLanguage($value) * @method static Builder|User whereLanguage($value)
* @method static Builder|User whereTimezone($value) * @method static Builder|User whereTimezone($value)
@ -90,7 +87,7 @@ use Spatie\Permission\Traits\HasRoles;
* @method static Builder|User whereUsername($value) * @method static Builder|User whereUsername($value)
* @method static Builder|User whereUuid($value) * @method static Builder|User whereUuid($value)
*/ */
class User extends Model implements AuthenticatableContract, AuthorizableContract, CanResetPasswordContract, FilamentUser, HasAvatar, HasName, HasTenants, Validatable class User extends Model implements AuthenticatableContract, AuthorizableContract, CanResetPasswordContract, FilamentUser, HasName, HasTenants, Validatable
{ {
use Authenticatable; use Authenticatable;
use Authorizable { can as protected canned; } use Authorizable { can as protected canned; }
@ -124,7 +121,6 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac
'use_totp', 'use_totp',
'totp_secret', 'totp_secret',
'totp_authenticated_at', 'totp_authenticated_at',
'gravatar',
'oauth', 'oauth',
'customization', 'customization',
]; ];
@ -169,7 +165,6 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac
{ {
return [ return [
'use_totp' => 'boolean', 'use_totp' => 'boolean',
'gravatar' => 'boolean',
'totp_authenticated_at' => 'datetime', 'totp_authenticated_at' => 'datetime',
'totp_secret' => 'encrypted', 'totp_secret' => 'encrypted',
'oauth' => 'array', 'oauth' => 'array',
@ -377,11 +372,6 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac
return $this->username; return $this->username;
} }
public function getFilamentAvatarUrl(): ?string
{
return 'https://gravatar.com/avatar/' . md5(strtolower($this->email));
}
public function canTarget(Model $user): bool public function canTarget(Model $user): bool
{ {
if ($this->isRootAdmin()) { if ($this->isRootAdmin()) {

View File

@ -10,6 +10,9 @@ use App\Checks\NodeVersionsCheck;
use App\Checks\PanelVersionCheck; use App\Checks\PanelVersionCheck;
use App\Checks\ScheduleCheck; use App\Checks\ScheduleCheck;
use App\Checks\UsedDiskSpaceCheck; use App\Checks\UsedDiskSpaceCheck;
use App\Extensions\Avatar\Providers\GravatarProvider;
use App\Extensions\Avatar\Providers\LocalAvatarProvider;
use App\Extensions\Avatar\Providers\UiAvatarsProvider;
use App\Extensions\OAuth\Providers\GitlabProvider; use App\Extensions\OAuth\Providers\GitlabProvider;
use App\Models; use App\Models;
use App\Extensions\Captcha\Providers\TurnstileProvider; use App\Extensions\Captcha\Providers\TurnstileProvider;
@ -115,6 +118,11 @@ class AppServiceProvider extends ServiceProvider
// Default Captcha provider // Default Captcha provider
TurnstileProvider::register($app); TurnstileProvider::register($app);
// Default Avatar providers
GravatarProvider::register();
UiAvatarsProvider::register();
LocalAvatarProvider::register();
FilamentColor::register([ FilamentColor::register([
'danger' => Color::Red, 'danger' => Color::Red,
'gray' => Color::Zinc, 'gray' => Color::Zinc,

View File

@ -2,6 +2,7 @@
namespace App\Providers\Filament; namespace App\Providers\Filament;
use App\Extensions\Avatar\AvatarProvider;
use App\Filament\Pages\Auth\Login; use App\Filament\Pages\Auth\Login;
use App\Filament\Pages\Auth\EditProfile; use App\Filament\Pages\Auth\EditProfile;
use App\Http\Middleware\LanguageMiddleware; use App\Http\Middleware\LanguageMiddleware;
@ -38,6 +39,7 @@ class AdminPanelProvider extends PanelProvider
->favicon(config('app.favicon', '/pelican.ico')) ->favicon(config('app.favicon', '/pelican.ico'))
->topNavigation(config('panel.filament.top-navigation', true)) ->topNavigation(config('panel.filament.top-navigation', true))
->maxContentWidth(config('panel.filament.display-width', 'screen-2xl')) ->maxContentWidth(config('panel.filament.display-width', 'screen-2xl'))
->defaultAvatarProvider(fn () => get_class(AvatarProvider::getProvider(config('panel.filament.avatar-provider'))))
->login(Login::class) ->login(Login::class)
->passwordReset() ->passwordReset()
->userMenuItems([ ->userMenuItems([

View File

@ -2,6 +2,7 @@
namespace App\Providers\Filament; namespace App\Providers\Filament;
use App\Extensions\Avatar\AvatarProvider;
use App\Filament\Pages\Auth\Login; use App\Filament\Pages\Auth\Login;
use App\Filament\Pages\Auth\EditProfile; use App\Filament\Pages\Auth\EditProfile;
use Filament\Facades\Filament; use Filament\Facades\Filament;
@ -34,6 +35,7 @@ class AppPanelProvider extends PanelProvider
->favicon(config('app.favicon', '/pelican.ico')) ->favicon(config('app.favicon', '/pelican.ico'))
->topNavigation(config('panel.filament.top-navigation', true)) ->topNavigation(config('panel.filament.top-navigation', true))
->maxContentWidth(config('panel.filament.display-width', 'screen-2xl')) ->maxContentWidth(config('panel.filament.display-width', 'screen-2xl'))
->defaultAvatarProvider(fn () => get_class(AvatarProvider::getProvider(config('panel.filament.avatar-provider'))))
->navigation(false) ->navigation(false)
->profile(EditProfile::class, false) ->profile(EditProfile::class, false)
->login(Login::class) ->login(Login::class)

View File

@ -2,6 +2,7 @@
namespace App\Providers\Filament; namespace App\Providers\Filament;
use App\Extensions\Avatar\AvatarProvider;
use App\Filament\App\Resources\ServerResource\Pages\ListServers; use App\Filament\App\Resources\ServerResource\Pages\ListServers;
use App\Filament\Pages\Auth\Login; use App\Filament\Pages\Auth\Login;
use App\Filament\Admin\Resources\ServerResource\Pages\EditServer; use App\Filament\Admin\Resources\ServerResource\Pages\EditServer;
@ -41,6 +42,7 @@ class ServerPanelProvider extends PanelProvider
->favicon(config('app.favicon', '/pelican.ico')) ->favicon(config('app.favicon', '/pelican.ico'))
->topNavigation(config('panel.filament.top-navigation', true)) ->topNavigation(config('panel.filament.top-navigation', true))
->maxContentWidth(config('panel.filament.display-width', 'screen-2xl')) ->maxContentWidth(config('panel.filament.display-width', 'screen-2xl'))
->defaultAvatarProvider(fn () => get_class(AvatarProvider::getProvider(config('panel.filament.avatar-provider'))))
->login(Login::class) ->login(Login::class)
->passwordReset() ->passwordReset()
->userMenuItems([ ->userMenuItems([

View File

@ -52,6 +52,7 @@ return [
'filament' => [ 'filament' => [
'top-navigation' => env('FILAMENT_TOP_NAVIGATION', false), 'top-navigation' => env('FILAMENT_TOP_NAVIGATION', false),
'display-width' => env('FILAMENT_WIDTH', 'screen-2xl'), 'display-width' => env('FILAMENT_WIDTH', 'screen-2xl'),
'avatar-provider' => env('FILAMENT_AVATAR_PROVIDER', 'gravatar'),
], ],
'use_binary_prefix' => env('PANEL_USE_BINARY_PREFIX', true), 'use_binary_prefix' => env('PANEL_USE_BINARY_PREFIX', true),

View File

@ -0,0 +1,28 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('users', function (Blueprint $table) {
$table->dropColumn('gravatar');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('users', function (Blueprint $table) {
$table->boolean('gravatar')->default(true);
});
}
};

View File

@ -34,6 +34,7 @@ return [
'clear' => 'Clear', 'clear' => 'Clear',
'set_to_cf' => 'Set to Cloudflare IPs', 'set_to_cf' => 'Set to Cloudflare IPs',
'display_width' => 'Display Width', 'display_width' => 'Display Width',
'avatar_provider' => 'Avatar Provider',
], ],
'captcha' => [ 'captcha' => [
'enable' => 'Enable', 'enable' => 'Enable',