A share can now hold a private text (a password, a key, a short note) with its files or on its own. The upload page's new "Private text" card takes up to 100 KB; the browser encrypts the text and sends it through the same chunk pipeline as a file, flagged is_text on share_files, so it gets the share's password, expiry, download limit and cleanup. The recipient sees the text only after pressing "Show text", which counts as their download, so a messenger link preview cannot use up a share limited to one download. The text is left out of the file list, the ZIP and the file counts; the admin dashboard marks shares that hold one with "Text". The website gains a Private text feature card and a fifth phone screenshot; every screenshot is retaken. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
86 lines
3.0 KiB
PHP
86 lines
3.0 KiB
PHP
<?php
|
|
|
|
namespace App\Livewire\Admin;
|
|
|
|
use App\Models\Share;
|
|
use App\Models\ShareFile;
|
|
use App\Services\ShareService;
|
|
use Livewire\Attributes\Layout;
|
|
use Livewire\Component;
|
|
use Livewire\WithPagination;
|
|
|
|
#[Layout('layouts.app')]
|
|
class AdminDashboard extends Component
|
|
{
|
|
use WithPagination;
|
|
|
|
/**
|
|
* The orders the shares list offers, each a column and a direction.
|
|
*
|
|
* @var array<string, array{0: string, 1: string}>
|
|
*/
|
|
public const SORTS = [
|
|
'newest' => ['created_at', 'desc'],
|
|
'oldest' => ['created_at', 'asc'],
|
|
'expiring' => ['expires_at', 'asc'],
|
|
'largest' => ['total_size', 'desc'],
|
|
'most-downloaded' => ['download_count', 'desc'],
|
|
'most-files' => ['files_count', 'desc'],
|
|
];
|
|
|
|
public string $sort = 'newest';
|
|
|
|
/** The share the delete dialog is asking about, while it is open. */
|
|
public ?int $deletingShareId = null;
|
|
|
|
public function deleteShare(int $shareId, ShareService $shareService): void
|
|
{
|
|
$share = Share::query()->findOrFail($shareId);
|
|
$shareService->deleteShare($share);
|
|
|
|
$this->deletingShareId = null;
|
|
}
|
|
|
|
/**
|
|
* A new order starts again from the first page.
|
|
*/
|
|
public function updatedSort(): void
|
|
{
|
|
$this->resetPage();
|
|
}
|
|
|
|
public function render(): mixed
|
|
{
|
|
$shareService = app(ShareService::class);
|
|
|
|
// The sort comes from the browser: only a known order reaches the query.
|
|
[$column, $direction] = self::SORTS[$this->sort] ?? self::SORTS['newest'];
|
|
|
|
// Shares whose files are still being uploaded are not shares yet; their bytes do count as used space.
|
|
$shares = Share::query()
|
|
->whereNotNull('completed_at')
|
|
// A share's private text is not one of its files.
|
|
->withCount(['files' => fn ($query) => $query->where('is_text', false)])
|
|
->withExists('textFile')
|
|
// Shares that never expire come after every share that does, whichever way expiry is sorted.
|
|
->when($column === 'expires_at', fn ($query) => $query->orderByRaw('expires_at is null'))
|
|
->orderBy($column, $direction)
|
|
->orderByDesc('id')
|
|
->paginate(15);
|
|
|
|
return view('livewire.admin.admin-dashboard', [
|
|
'shares' => $shares,
|
|
'totalShares' => Share::query()->whereNotNull('completed_at')->count(),
|
|
'activeShares' => Share::query()->whereNotNull('completed_at')->where(function ($q) {
|
|
$q->whereNull('expires_at')->orWhere('expires_at', '>', now());
|
|
})->where(function ($q) {
|
|
$q->whereNull('max_downloads')->orWhereColumn('download_count', '<', 'max_downloads');
|
|
})->count(),
|
|
'totalFiles' => ShareFile::query()->where('is_text', false)->whereHas('share', fn ($query) => $query->whereNotNull('completed_at'))->count(),
|
|
'usedSpace' => $shareService->getTotalUsedSpace(),
|
|
'maxQuota' => $shareService->getMaxStorageQuota(),
|
|
'version' => config('app.version'),
|
|
]);
|
|
}
|
|
}
|