R5 the server's own settings in decePub: instance, rules, announcements, relays, media limits, the staff's log, About
Build / Build (push) Successful in 1m5s
Build / Build (push) Successful in 1m5s
Administration's Pleroma placeholders give way to real sections: Instance (title, descriptions, contact, thumbnail, privacy policy, terms, the weekly activity), Rules, Announcements, Relays, Media limits and the Moderation log, next to Reports, blocked servers (now published or obfuscated), statistics and backups; moderators see Reports and the log. Every save sends the whole settings form built from the latest read, since PrivaPub replaces what it carries. /about shows the server's documents to anyone. The report dialog picks the rules a violation breaks, and reports show them. Announcements show above the feed tabs, dismissed and reacted to, live from the user stream. Login names the server. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WpVCoDeM1qUTRp4fXQdfof
This commit is contained in:
1 parent
b8d7858acb
commit
4c88980374
33 files changed
+3743
-239
No files matched your search
@@ -0,0 +1,214 @@
|
||||
@inherits LocalizableComponentBase
|
||||
|
||||
@* The media limits the administrator sets over the configuration's (Media:*, /clientapi/admin/settings): what a login's
|
||||
media may take in all, how big a picture and a video may be, and how long a sound or a video may last. Left empty, a
|
||||
limit is the configuration's; beside each, the configured value and the one in force now, and PrivaPub's hard caps
|
||||
are the maxima (40 MB, 99 MB, four hours). Saving starts from the settings PrivaPub has now
|
||||
(InstanceAdminService.SaveSettings), so what Instance or Rules saved stays. *@
|
||||
<div class="flex flex-col gap-3 w-full" data-media-limits>
|
||||
@if (IsLoading)
|
||||
{
|
||||
<p class="flex w-full items-center justify-center gap-2">
|
||||
<DLoading Variant="DLoadingVariant.Dots" LabelText="@Localizer["loading"]" />
|
||||
@Localizer["loading"]
|
||||
</p>
|
||||
}
|
||||
else if (settings is null)
|
||||
{
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<p class="text-sm text-error">@Localizer["The server's settings could not be read."]</p>
|
||||
<DButton Size="DSize.Sm" Disabled="IsDefaultDisabled" OnClick="Retry">@Localizer["Retry"]</DButton>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<p class="text-xs opacity-70">@Localizer["Empty, a limit is the one the server's configuration sets. Every change applies at once, to the next upload."]</p>
|
||||
|
||||
<div class="flex flex-col gap-1 p-2 rounded-lg neo-raised neo-2xs" data-media-limit="quota">
|
||||
<label class="flex flex-wrap items-center gap-2">
|
||||
<span class="text-sm font-semibold flex-1 min-w-40">@Localizer["Media a login may keep in all"]</span>
|
||||
<DInput TValue="long?" Type="number" Size="DSize.Sm" class="w-36" @bind-Value="quotaMb" Immediate="true" autocomplete="off" min="0" step="1"
|
||||
placeholder="@Localizer["as configured"]" data-media-quota>
|
||||
<Suffix><span class="text-xs opacity-70">@Localizer["MB"]</span></Suffix>
|
||||
</DInput>
|
||||
</label>
|
||||
<p class="text-xs opacity-70">
|
||||
@Localizer["Configured: {0}", Quota(settings.Media.ConfiguredQuotaBytesPerRoot)] ·
|
||||
<span data-media-effective>@Localizer["In force: {0}", Quota(settings.Media.EffectiveQuotaBytesPerRoot)]</span> ·
|
||||
@Localizer["0: no quota"]
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-1 p-2 rounded-lg neo-raised neo-2xs" data-media-limit="image">
|
||||
<label class="flex flex-wrap items-center gap-2">
|
||||
<span class="text-sm font-semibold flex-1 min-w-40">@Localizer["Largest picture"]</span>
|
||||
<DInput TValue="long?" Type="number" Size="DSize.Sm" class="w-36" @bind-Value="imageMb" Immediate="true" autocomplete="off" min="1"
|
||||
max="@(settings.Media.HardMaxImageBytes / Megabyte)" step="1" placeholder="@Localizer["as configured"]" data-media-image>
|
||||
<Suffix><span class="text-xs opacity-70">@Localizer["MB"]</span></Suffix>
|
||||
</DInput>
|
||||
</label>
|
||||
<p class="text-xs opacity-70">
|
||||
@Localizer["Configured: {0}", settings.Media.ConfiguredMaxImageBytes.GetFileSize(Localizer)] ·
|
||||
<span data-media-effective>@Localizer["In force: {0}", settings.Media.EffectiveMaxImageBytes.GetFileSize(Localizer)]</span> ·
|
||||
@Localizer["At most {0}", settings.Media.HardMaxImageBytes.GetFileSize(Localizer)]
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-1 p-2 rounded-lg neo-raised neo-2xs" data-media-limit="video">
|
||||
<label class="flex flex-wrap items-center gap-2">
|
||||
<span class="text-sm font-semibold flex-1 min-w-40">@Localizer["Largest video or sound"]</span>
|
||||
<DInput TValue="long?" Type="number" Size="DSize.Sm" class="w-36" @bind-Value="videoMb" Immediate="true" autocomplete="off" min="1"
|
||||
max="@(settings.Media.HardMaxVideoBytes / Megabyte)" step="1" placeholder="@Localizer["as configured"]" data-media-video>
|
||||
<Suffix><span class="text-xs opacity-70">@Localizer["MB"]</span></Suffix>
|
||||
</DInput>
|
||||
</label>
|
||||
<p class="text-xs opacity-70">
|
||||
@Localizer["Configured: {0}", settings.Media.ConfiguredMaxVideoBytes.GetFileSize(Localizer)] ·
|
||||
<span data-media-effective>@Localizer["In force: {0}", settings.Media.EffectiveMaxVideoBytes.GetFileSize(Localizer)]</span> ·
|
||||
@Localizer["At most {0}", settings.Media.HardMaxVideoBytes.GetFileSize(Localizer)]
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-1 p-2 rounded-lg neo-raised neo-2xs" data-media-limit="seconds">
|
||||
<label class="flex flex-wrap items-center gap-2">
|
||||
<span class="text-sm font-semibold flex-1 min-w-40">@Localizer["Longest video or sound"]</span>
|
||||
<DInput TValue="int?" Type="number" Size="DSize.Sm" class="w-36" @bind-Value="seconds" Immediate="true" autocomplete="off" min="1"
|
||||
max="@settings.Media.HardMaxSeconds" step="1" placeholder="@Localizer["as configured"]" data-media-seconds>
|
||||
<Suffix><span class="text-xs opacity-70">@Localizer["seconds"]</span></Suffix>
|
||||
</DInput>
|
||||
</label>
|
||||
<p class="text-xs opacity-70">
|
||||
@Localizer["Configured: {0}", Length(settings.Media.ConfiguredMaxSeconds)] ·
|
||||
<span data-media-effective>@Localizer["In force: {0}", Length(settings.Media.EffectiveMaxSeconds)]</span> ·
|
||||
@Localizer["At most {0}", Length(settings.Media.HardMaxSeconds)]
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<ErrorViewer AdditionalErrors="errors" />
|
||||
<div class="flex flex-wrap items-center justify-end gap-2 pointer-coarse:gap-2">
|
||||
<DButton Size="DSize.Sm" Disabled="IsDefaultDisabled || isSaving" OnClick="UseConfigured" data-media-use-configured>
|
||||
@Localizer["Use the configured values"]
|
||||
</DButton>
|
||||
<DButton Size="DSize.Sm" Disabled="IsDefaultDisabled || isSaving" OnClick="Reset" data-media-reset>@Localizer["Discard changes"]</DButton>
|
||||
<DButton Size="DSize.Sm" Color="DColor.Primary" Disabled="IsDefaultDisabled || isSaving" OnClick="Save" data-media-save>
|
||||
@if (isSaving)
|
||||
{
|
||||
<DLoading Size="DSize.Sm" LabelText="@Localizer["loading"]" />
|
||||
}
|
||||
<span>@Localizer["Save the limits"]</span>
|
||||
</DButton>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@code {
|
||||
const long Megabyte = 1024 * 1024;
|
||||
|
||||
[Inject] InstanceAdminService InstanceAdminService { get; set; }
|
||||
|
||||
ViewServerSettings settings;
|
||||
// the overrides as being written, in megabytes and seconds; null: the configured value
|
||||
long? quotaMb;
|
||||
long? imageMb;
|
||||
long? videoMb;
|
||||
int? seconds;
|
||||
List<string> errors = [];
|
||||
bool isSaving;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
if (CascadingState.IsOffline)
|
||||
{
|
||||
WhenOnline(LoadData);
|
||||
StartOnlinePolling();
|
||||
IsLoading = false;
|
||||
return;
|
||||
}
|
||||
|
||||
await LoadData();
|
||||
IsLoading = false;
|
||||
}
|
||||
|
||||
async Task LoadData()
|
||||
{
|
||||
settings = await InstanceAdminService.Settings();
|
||||
Reset();
|
||||
}
|
||||
|
||||
async Task Retry()
|
||||
{
|
||||
IsLoading = true;
|
||||
await LoadData();
|
||||
IsLoading = false;
|
||||
}
|
||||
|
||||
void Reset()
|
||||
{
|
||||
errors = [];
|
||||
if (settings is null)
|
||||
return;
|
||||
quotaMb = Megabytes(settings.Media.QuotaBytesPerRoot);
|
||||
imageMb = Megabytes(settings.Media.MaxImageBytes);
|
||||
videoMb = Megabytes(settings.Media.MaxVideoBytes);
|
||||
seconds = settings.Media.MaxSeconds;
|
||||
}
|
||||
|
||||
void UseConfigured()
|
||||
{
|
||||
errors = [];
|
||||
quotaMb = null;
|
||||
imageMb = null;
|
||||
videoMb = null;
|
||||
seconds = null;
|
||||
}
|
||||
|
||||
string Quota(long bytes) => bytes <= 0 ? Localizer["no quota"] : bytes.GetFileSize(Localizer);
|
||||
|
||||
static string Length(int value) => NerdRow.Duration(TimeSpan.FromSeconds(value));
|
||||
|
||||
static long? Megabytes(long? bytes) => bytes is { } value ? (long)Math.Round(value / (double)Megabyte) : null;
|
||||
|
||||
// what was set stays to the byte while its megabytes are left as they were shown
|
||||
static long? Bytes(long? megabytes, long? before) =>
|
||||
megabytes is not { } value ? null : before is { } kept && Megabytes(kept) == value ? kept : value * Megabyte;
|
||||
|
||||
async Task Save()
|
||||
{
|
||||
errors = [];
|
||||
var media = settings.Media;
|
||||
if (quotaMb is < 0)
|
||||
errors.Add(Localizer["The quota can't be below 0."]);
|
||||
if (imageMb is { } image && (image < 1 || image * Megabyte > media.HardMaxImageBytes))
|
||||
errors.Add(Localizer["A picture's limit goes from 1 MB to {0}.", media.HardMaxImageBytes.GetFileSize(Localizer)]);
|
||||
if (videoMb is { } video && (video < 1 || video * Megabyte > media.HardMaxVideoBytes))
|
||||
errors.Add(Localizer["A video's limit goes from 1 MB to {0}.", media.HardMaxVideoBytes.GetFileSize(Localizer)]);
|
||||
if (seconds is { } length && (length < 1 || length > media.HardMaxSeconds))
|
||||
errors.Add(Localizer["A length goes from 1 second to {0}.", Length(media.HardMaxSeconds)]);
|
||||
if (errors.Count > 0)
|
||||
return;
|
||||
|
||||
// only the limits changed here: what another administrator saved meanwhile to the others stays
|
||||
var quotaChanged = quotaMb != Megabytes(media.QuotaBytesPerRoot);
|
||||
var imageChanged = imageMb != Megabytes(media.MaxImageBytes);
|
||||
var videoChanged = videoMb != Megabytes(media.MaxVideoBytes);
|
||||
var secondsChanged = seconds != media.MaxSeconds;
|
||||
isSaving = true;
|
||||
var result = await InstanceAdminService.SaveSettings(latest => latest.Media = new ServerMediaLimitsForm
|
||||
{
|
||||
QuotaBytesPerRoot = quotaChanged ? Bytes(quotaMb, media.QuotaBytesPerRoot) : latest.Media.QuotaBytesPerRoot,
|
||||
MaxImageBytes = imageChanged ? Bytes(imageMb, media.MaxImageBytes) : latest.Media.MaxImageBytes,
|
||||
MaxVideoBytes = videoChanged ? Bytes(videoMb, media.MaxVideoBytes) : latest.Media.MaxVideoBytes,
|
||||
MaxSeconds = secondsChanged ? seconds : latest.Media.MaxSeconds
|
||||
});
|
||||
isSaving = false;
|
||||
if (result.IsInvalid)
|
||||
{
|
||||
Toast.AddResponseError(result);
|
||||
return;
|
||||
}
|
||||
|
||||
settings = (ViewServerSettings)result.Data;
|
||||
Reset();
|
||||
Toast.AddResponseSuccess(Localizer["The media limits are saved."]);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user