Move SealShare onto Livewire Material

Replaces maryUI and daisyUI with nonameweb/livewire-material: the Vibrant
indigo scheme, a system/light/dark theme under sealshare-theme, one top
app bar with the account menu, the upload drop zone and link-ready
moments, M3 fields, dialogs instead of wire:confirm, snackbars instead of
flashed messages, a sortable admin table, and the starter-kit cleanup.
Docker builds assets after Composer; CI drops the Flux step.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V9NnLxnPp8vaaurb3Z1MFy
This commit is contained in:
Andreas Reinhold / reini
2026-09-13 10:49:38 +02:00
co-authored by Claude Opus 5
parent 6d7120e8e2
commit c4a17b65c8
70 changed files with 3043 additions and 1680 deletions
@@ -1,100 +1,66 @@
<div class="max-w-2xl mx-auto">
<h1 class="text-2xl font-bold mb-6">{{ __('System Settings') }}</h1>
<div class="mx-auto max-w-2xl">
<h1 class="mb-6 type-headline-md">{{ __('System Settings') }}</h1>
@if (session('message'))
<div class="alert alert-success mb-6">
<x-icon name="o-check-circle" class="w-5 h-5" />
<span>{{ session('message') }}</span>
</div>
@endif
<form wire:submit="saveSettings" class="grid gap-6">
<x-card :title="__('Branding')" variant="outlined">
<div class="grid gap-5">
<x-input wire:model="siteTitle" :label="__('Site Title')" :hint="__('Displayed as the heading on the upload page.')" />
<form wire:submit="saveSettings">
<x-card title="{{ __('Branding') }}" shadow class="mb-6">
<div class="space-y-4">
<x-input
wire:model="siteTitle"
label="{{ __('Site Title') }}"
hint="{{ __('Displayed as the heading on the upload page.') }}"
/>
<x-textarea
wire:model="siteDescription"
label="{{ __('Site Description') }}"
hint="{{ __('Displayed below the title on the upload page.') }}"
rows="3"
/>
<div>
<label class="label label-text font-semibold">{{ __('Logo') }}</label>
<x-textarea wire:model="siteDescription" :label="__('Site Description')" :hint="__('Displayed below the title on the upload page.')" rows="3" />
<div class="grid gap-3">
@if ($currentLogo)
<div class="flex items-center gap-4 mb-3">
<img src="{{ Storage::disk('public')->url($currentLogo) }}" alt="{{ __('Site Logo') }}" class="h-16 w-auto rounded" />
<x-button
label="{{ __('Remove Logo') }}"
class="btn-sm btn-ghost text-error"
wire:click="removeLogo"
wire:confirm="{{ __('Remove the logo?') }}"
/>
<div class="flex flex-wrap items-center gap-4">
<img src="{{ Storage::disk('public')->url($currentLogo) }}" alt="{{ __('Site Logo') }}" class="h-16 w-auto rounded-corner-sm" />
<x-button :label="__('Remove Logo')" icon="delete" color="error" wire:click="$set('confirmingLogoRemoval', true)" data-test="remove-logo" />
</div>
@endif
<input type="file" wire:model="siteLogo" accept="image/*,.svg,.svgz" class="file-input file-input-bordered w-full" />
<x-file wire:model="siteLogo" :label="__('Logo')" accept="image/*,.svg,.svgz" :hint="__('Max 2MB. Recommended: PNG or SVG.')" />
@if ($siteLogo && is_object($siteLogo))
<div class="mt-2">
@if (str_contains($siteLogo->getMimeType(), 'svg'))
<p class="text-sm opacity-60">{{ __('SVG selected: :name', ['name' => $siteLogo->getClientOriginalName()]) }}</p>
@else
<p class="text-sm opacity-60">{{ __('Preview:') }}</p>
<img src="{{ $siteLogo->temporaryUrl() }}" alt="{{ __('Logo preview') }}" class="h-16 w-auto rounded mt-1" />
@endif
</div>
@endif
@error('siteLogo')
<p class="text-error text-sm mt-1">{{ $message }}</p>
@enderror
<p class="text-xs opacity-50 mt-1">{{ __('Max 2MB. Recommended: PNG or SVG.') }}</p>
</div>
</div>
</x-card>
<x-card title="{{ __('Upload Protection') }}" shadow class="mb-6">
<div class="space-y-4">
<div>
<x-password
wire:model="systemPassword"
label="{{ __('System Upload Password') }}"
hint="{{ __('Leave blank to keep current. Set a password to require it before uploading.') }}"
/>
@if ($hasSystemPassword)
<div class="mt-2">
<x-button
label="{{ __('Clear System Password') }}"
class="btn-sm btn-ghost text-error"
wire:click="clearSystemPassword"
wire:confirm="{{ __('Remove the system password?') }}"
/>
</div>
@if (str_contains($siteLogo->getMimeType(), 'svg'))
<p class="type-body-md text-on-surface-variant">{{ __('SVG selected: :name', ['name' => $siteLogo->getClientOriginalName()]) }}</p>
@else
<div>
<p class="type-label-lg text-on-surface-variant">{{ __('Preview:') }}</p>
<img src="{{ $siteLogo->temporaryUrl() }}" alt="{{ __('Logo preview') }}" class="mt-1 h-16 w-auto rounded-corner-sm" />
</div>
@endif
@endif
</div>
</div>
</x-card>
<x-card title="{{ __('Upload Limits') }}" shadow class="mb-6">
<div class="space-y-4">
<x-card :title="__('Upload Protection')" variant="outlined">
<div class="grid gap-3">
<x-password
wire:model="systemPassword"
:label="__('System Upload Password')"
:hint="__('Leave blank to keep current. Set a password to require it before uploading.')"
autocomplete="new-password"
/>
@if ($hasSystemPassword)
<div>
<x-button :label="__('Clear System Password')" icon="lock_reset" color="error" wire:click="$set('confirmingPasswordRemoval', true)" data-test="clear-system-password" />
</div>
@endif
</div>
</x-card>
<x-card :title="__('Upload Limits')" variant="outlined">
<div class="grid gap-5">
<x-toggle
wire:model.live="allowNeverExpire"
label="{{ __('Allow shares to never expire') }}"
hint="{{ __('When disabled, users must select an expiration time.') }}"
:label="__('Allow shares to never expire')"
:hint="__('When disabled, users must select an expiration time.')"
right
/>
<x-select
wire:model="defaultExpiration"
label="{{ __('Default Expiration') }}"
:label="__('Default Expiration')"
:placeholder="$allowNeverExpire ? __('None') : null"
:options="[
['id' => '1h', 'name' => __('1 Hour')],
@@ -108,44 +74,49 @@
<x-input
wire:model="maxFileSize"
label="{{ __('Max file size (MB)') }}"
:label="__('Max file size (MB)')"
type="number"
min="1"
max="{{ $phpMaxUploadMb }}"
:max="$phpMaxUploadMb"
suffix="MB"
hint="{{ __('PHP limit: :max MB (upload_max_filesize / post_max_size)', ['max' => $phpMaxUploadMb]) }}"
:hint="__('PHP limit: :max MB (upload_max_filesize / post_max_size)', ['max' => $phpMaxUploadMb])"
/>
<x-input
wire:model="maxFilesPerShare"
label="{{ __('Max files per share') }}"
type="number"
min="1"
/>
<x-input wire:model="maxFilesPerShare" :label="__('Max files per share')" type="number" min="1" />
<x-input
wire:model="maxSizePerShare"
label="{{ __('Max total size per share (GB)') }}"
type="number"
min="1"
suffix="GB"
/>
<x-input wire:model="maxSizePerShare" :label="__('Max total size per share (GB)')" type="number" min="1" suffix="GB" />
</div>
</x-card>
<x-card title="{{ __('Storage') }}" shadow class="mb-6">
<div class="space-y-4">
<x-input
wire:model="maxStorageQuota"
label="{{ __('Max storage quota (GB)') }}"
type="number"
min="1"
suffix="GB"
hint="{{ __('When reached, new uploads are blocked.') }}"
/>
</div>
<x-card :title="__('Storage')" variant="outlined">
<x-input
wire:model="maxStorageQuota"
:label="__('Max storage quota (GB)')"
type="number"
min="1"
suffix="GB"
:hint="__('When reached, new uploads are blocked.')"
/>
</x-card>
<x-button type="submit" label="{{ __('Save Settings') }}" class="btn-primary w-full" icon="o-check" spinner="saveSettings" />
<x-button type="submit" :label="__('Save Settings')" variant="filled" icon="check" spinner="saveSettings" class="w-full" />
</form>
<x-modal wire:model="confirmingLogoRemoval" :title="__('Remove the logo?')" icon="delete">
{{ __('The upload and download pages show the default mark again.') }}
<x-slot:actions>
<x-button :label="__('Cancel')" x-on:click="close()" />
<x-button :label="__('Remove')" danger wire:click="removeLogo" data-test="confirm-remove-logo" />
</x-slot:actions>
</x-modal>
<x-modal wire:model="confirmingPasswordRemoval" :title="__('Remove the system password?')" icon="lock_open">
{{ __('Anyone who can reach the upload page can upload files again.') }}
<x-slot:actions>
<x-button :label="__('Cancel')" x-on:click="close()" />
<x-button :label="__('Remove')" danger wire:click="clearSystemPassword" data-test="confirm-clear-system-password" />
</x-slot:actions>
</x-modal>
</div>