Compare commits
3 Commits
ff3fbc1ff1
...
d6d167f40c
| Author | SHA1 | Date | |
|---|---|---|---|
| d6d167f40c | |||
| e33d87bb30 | |||
| e297a81ebb |
@@ -10,7 +10,7 @@
|
||||
<input class="form-control form-control-sm mx-1 @Edit" type="text" @bind="Folder.BasePath"/>
|
||||
<button class="btn btn-sm btn-success @Edit" @onclick="OnConfirmEdit"><i class="bi bi-check"></i></button>
|
||||
<div class="ms-auto form-switch">
|
||||
<input class="form-check-input m-1" type="checkbox" id="@Folder.Id" @bind="@Folder.Active"/>
|
||||
<input class="form-check-input m-1" type="checkbox" id="@Folder.Id" @bind="@Folder.Active" @bind:after="() => OnToggleActive()"/>
|
||||
</div>
|
||||
<button class="btn btn-outline-warning btn-sm @Display" @onclick="OnEdit"><i class="bi bi-pencil"></i></button>
|
||||
<button class="btn btn-outline-danger btn-sm mx-1" @onclick="OnDelete"><i class="bi bi-folder-minus"></i></button>
|
||||
@@ -25,20 +25,8 @@
|
||||
|
||||
[Parameter]
|
||||
public FolderFullDto Folder { get; set; } = new FolderFullDto();
|
||||
|
||||
protected override void OnInitialized() {
|
||||
FoldersService.SaveFolders += async (_, _) => {
|
||||
try {
|
||||
await FoldersService.UpdateFolderAsync(
|
||||
Folder.Id,
|
||||
new FolderUpdateDto() {
|
||||
Active = Folder.Active,
|
||||
BasePath = Folder.BasePath
|
||||
}
|
||||
);
|
||||
} catch (Exception) { /* fire-and-forget: failure is silent to avoid cascading errors */ }
|
||||
};
|
||||
}
|
||||
[Parameter]
|
||||
public EventCallback<Guid> OnDeleted { get; set; }
|
||||
|
||||
void OnEdit() {
|
||||
editing = true;
|
||||
@@ -57,7 +45,16 @@
|
||||
|
||||
async Task OnDelete() {
|
||||
await FoldersService.RemoveFolderAsync(Folder.Id);
|
||||
NavigationManager.Refresh(true);
|
||||
await OnDeleted.InvokeAsync(Folder.Id);
|
||||
}
|
||||
|
||||
async Task OnToggleActive() {
|
||||
await FoldersService.UpdateFolderAsync(
|
||||
Folder.Id,
|
||||
new FolderUpdateDto() {
|
||||
Active = Folder.Active,
|
||||
BasePath = Folder.BasePath
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,7 +56,6 @@
|
||||
<li><a class="dropdown-item" @onclick="OnProfileClick"><i class="bi bi-person-lines-fill"></i> Profile</a></li>
|
||||
@if(Admin) {
|
||||
<li><a class="dropdown-item" @onclick="OnSettingsClick"><i class="bi bi-gear-wide-connected"></i> Settings</a></li>
|
||||
<li><a class="dropdown-item" @onclick="OnSettings1Click"><i class="bi bi-gear-wide-connected"></i> Settings 1</a></li>
|
||||
<li><a class="dropdown-item" @onclick="OnJobsClick"><i class="bi bi-kanban-fill"></i> Jobs</a></li>
|
||||
}
|
||||
<li><a class="dropdown-item" @onclick="OnLogout"><i class="bi bi-door-closed"></i> Logout</a></li>
|
||||
@@ -98,8 +97,6 @@
|
||||
|
||||
void OnSettingsClick() => navigation.NavigateTo("/settings");
|
||||
|
||||
void OnSettings1Click() => navigation.NavigateTo("/Settings1");
|
||||
|
||||
void OnJobsClick() => navigation.NavigateTo("/jobs");
|
||||
|
||||
void OnProfileClick() => navigation.NavigateTo($"/User/{loginService.AuthInfo?.UserId}", true);
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
@using Butter.Settings
|
||||
@using MilkStream.Client.Components.SettingBoxes
|
||||
@using MilkStream.Client.Services
|
||||
@using System.Linq;
|
||||
@using System.Linq
|
||||
|
||||
@inject NavigationManager NavigationManager
|
||||
@inject LoginService LoginService
|
||||
@@ -13,91 +13,151 @@
|
||||
|
||||
<PageTitle>Settings</PageTitle>
|
||||
|
||||
<h3>Settings</h3>
|
||||
@if (settings.Any()) {
|
||||
foreach (var option in settings) {
|
||||
switch (option.DisplayType) {
|
||||
// TODO: should implement text and password at some point
|
||||
case DisplayType.Text:
|
||||
<SettingString Setting="option"/>
|
||||
break;
|
||||
case DisplayType.Password:
|
||||
<input type="password" id="@option.Name" class="form-control" @bind="@option.Value"/>
|
||||
<div class="settings-layout">
|
||||
<nav class="settings-sidebar">
|
||||
@foreach (var group in _groups) {
|
||||
<button class="settings-nav-item @(_activeGroup == group.Key ? "active" : "")"
|
||||
@onclick="() => SelectGroup(group.Key)">
|
||||
<i class="bi @group.Value.Icon"></i>
|
||||
<span>@group.Key</span>
|
||||
</button>
|
||||
}
|
||||
</nav>
|
||||
|
||||
<main class="settings-content">
|
||||
@if (_isLoading) {
|
||||
<div class="d-flex justify-content-center align-items-center h-100">
|
||||
<LoadSpinner/>
|
||||
</div>
|
||||
} else if (_activeGroup == "Folders") {
|
||||
<div class="d-flex align-items-center justify-content-between mb-3">
|
||||
<h4 class="m-0"><i class="bi bi-folder2-open"></i> Folders</h4>
|
||||
<button class="btn btn-outline-primary btn-sm" @onclick="OnAddFolder">
|
||||
<i class="bi bi-folder-plus"></i> Add folder
|
||||
</button>
|
||||
</div>
|
||||
@if (_folders.Any()) {
|
||||
@foreach (var folder in _folders) {
|
||||
<FolderBox Folder="folder" OnDeleted="HandleFolderDeleted"/>
|
||||
}
|
||||
} else {
|
||||
<div class="alert alert-secondary">No folders configured yet.</div>
|
||||
}
|
||||
} else {
|
||||
<h4 class="mb-3"><i class="bi @_groups[_activeGroup].Icon"></i> @_activeGroup</h4>
|
||||
@if (_groupedSettings.TryGetValue(_activeGroup, out var settings) && settings.Count > 0) {
|
||||
<div class="settings-grid">
|
||||
@foreach (var setting in settings) {
|
||||
@switch (setting.DisplayType) {
|
||||
case DisplayType.Switch:
|
||||
<SettingSwitch Setting="setting"/>
|
||||
break;
|
||||
case DisplayType.IntNumber:
|
||||
case DisplayType.FloatNumber:
|
||||
<SettingNumber Setting="option"/>
|
||||
<SettingNumber Setting="setting"/>
|
||||
break;
|
||||
case DisplayType.Checkbox:
|
||||
// TODO: maybe decide if we ever need it?
|
||||
case DisplayType.Switch:
|
||||
<SettingSwitch Setting="option"/>
|
||||
break;
|
||||
case DisplayType.DateTimePicker:
|
||||
<div class="">DATETIME NOT IMPLEMENTED Value: @option.Value</div>
|
||||
case DisplayType.Text:
|
||||
case DisplayType.Password:
|
||||
<SettingString Setting="setting"/>
|
||||
break;
|
||||
case DisplayType.TimePicker:
|
||||
<SettingRange Setting="option" MinValue="Convert.ToInt32(option.Options?[0])" MaxValue="Convert.ToInt32(option.Options?[1])" Step="Convert.ToInt32(option.Options?[2])"/>
|
||||
<SettingRange Setting="setting"
|
||||
MinValue="Convert.ToInt32(setting.Options?[0])"
|
||||
MaxValue="Convert.ToInt32(setting.Options?[1])"
|
||||
Step="Convert.ToInt32(setting.Options?[2])"/>
|
||||
break;
|
||||
case DisplayType.Po2W:
|
||||
<SettingRange Setting="option" IsPowerOfTwo="true" MinValue="Convert.ToInt32(option.Options?[0])" MaxValue="Convert.ToInt32(option.Options?[1])" Step="Convert.ToInt32(option.Options?[2])"/>
|
||||
<SettingRange Setting="setting" IsPowerOfTwo="true"
|
||||
MinValue="Convert.ToInt32(setting.Options?[0])"
|
||||
MaxValue="Convert.ToInt32(setting.Options?[1])"
|
||||
Step="Convert.ToInt32(setting.Options?[2])"/>
|
||||
break;
|
||||
default:
|
||||
<div class="alert alert-warning py-1 small">Unsupported: @setting.DisplayType</div>
|
||||
break;
|
||||
}
|
||||
}
|
||||
<div>
|
||||
<button class="btn btn-outline-primary my-2" @onclick="OnSaveChanges"><i class="bi bi-floppy2 mx-1"></i>Save Changes</button>
|
||||
</div>
|
||||
} else {
|
||||
<div class="alert alert-danger my-2 my-sm-5 mx-auto" style="max-width: fit-content" role="alert">
|
||||
No settings available.
|
||||
</div>
|
||||
}
|
||||
<hr/>
|
||||
<h3>Folders</h3>
|
||||
@if (folders.Any()) {
|
||||
foreach (var folder in folders) {
|
||||
<FolderBox Folder="folder"/>
|
||||
} else {
|
||||
<div class="alert alert-secondary">No settings in this category.</div>
|
||||
}
|
||||
<div>
|
||||
<button class="btn btn-outline-primary my-2" @onclick="OnAddFolder"><i class="bi bi-folder-plus mx-1"></i>Add folder</button>
|
||||
</div>
|
||||
} else {
|
||||
<div class="alert alert-danger my-2 my-sm-5 mx-auto" style="max-width: fit-content" role="alert">
|
||||
No folders available.
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</main>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
List<SettingDto> settings = new();
|
||||
List<FolderFullDto> folders = new();
|
||||
private record GroupInfo(string Icon, string[] SettingNames);
|
||||
|
||||
protected override void OnInitialized() {
|
||||
base.OnInitialized();
|
||||
LoginService.LoggedUserChanged += async (_, _) => {
|
||||
private static readonly Dictionary<string, GroupInfo> _groups = new() {
|
||||
["General"] = new("bi-gear-wide-connected", ["User Registration Enabled"]),
|
||||
["Scanning"] = new("bi-arrow-repeat", ["Folder Scan Enabled", "Folder Scan Interval"]),
|
||||
["Upload"] = new("bi-upload", ["File Upload Enabled", "File Upload MaxSize"]),
|
||||
["Paths"] = new("bi-folder2", ["Thumbnail Path", "Preview Path", "Thumbnail Size", "Preview Size"]),
|
||||
["Jobs"] = new("bi-kanban-fill", ["Max Concurrent Jobs", "Job Retention Days"]),
|
||||
["Folders"] = new("bi-folder2-open", [])
|
||||
};
|
||||
|
||||
private Dictionary<string, List<SettingDto>> _groupedSettings = new();
|
||||
private List<FolderFullDto> _folders = new();
|
||||
private string _activeGroup = "General";
|
||||
private bool _isLoading = true;
|
||||
|
||||
protected override async Task OnInitializedAsync() {
|
||||
if (LoginService.IsLoggedIn) {
|
||||
await LoadData();
|
||||
} else {
|
||||
LoginService.AuthInfoChanged += async (_, _) => {
|
||||
await LoadData();
|
||||
StateHasChanged();
|
||||
};
|
||||
}
|
||||
|
||||
protected async override Task OnInitializedAsync() {
|
||||
if (LoginService.IsLoggedIn)
|
||||
await LoadData();
|
||||
}
|
||||
|
||||
async Task LoadData() {
|
||||
settings = await SettingsService.GetAllSettings() ?? new List<SettingDto>();
|
||||
folders = await FoldersService.GetAllFolders() ?? new List<FolderFullDto>();
|
||||
_isLoading = true;
|
||||
StateHasChanged();
|
||||
|
||||
var settingsTask = SettingsService.GetAllSettings();
|
||||
var foldersTask = FoldersService.GetAllFolders();
|
||||
|
||||
await Task.WhenAll(settingsTask, foldersTask);
|
||||
|
||||
var settings = settingsTask.Result;
|
||||
var folders = foldersTask.Result;
|
||||
|
||||
if (settings != null) {
|
||||
_groupedSettings = GroupSettings(settings);
|
||||
}
|
||||
|
||||
if (folders != null) {
|
||||
_folders = folders;
|
||||
}
|
||||
|
||||
_isLoading = false;
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
void OnSaveChanges() {
|
||||
SettingsService.OnBeginSave();
|
||||
FoldersService.OnSaveFolders();
|
||||
StateHasChanged();
|
||||
static Dictionary<string, List<SettingDto>> GroupSettings(List<SettingDto> settings) {
|
||||
var lookup = _groups
|
||||
.Where(g => g.Key != "Folders")
|
||||
.SelectMany(g => g.Value.SettingNames.Select(name => (Group: g.Key, Name: name)))
|
||||
.ToDictionary(x => x.Name, x => x.Group);
|
||||
|
||||
return settings
|
||||
.GroupBy(s => lookup.GetValueOrDefault(s.Name, "Other"))
|
||||
.ToDictionary(g => g.Key, g => g.ToList());
|
||||
}
|
||||
|
||||
void SelectGroup(string group) {
|
||||
_activeGroup = group;
|
||||
}
|
||||
|
||||
async Task OnAddFolder() {
|
||||
await FoldersService.CreateFolderAsync(new FolderCreateDto() { BasePath = "New Folder" });
|
||||
NavigationManager.Refresh(true);
|
||||
await FoldersService.CreateFolderAsync(new FolderCreateDto { BasePath = "New Folder" });
|
||||
await LoadData();
|
||||
}
|
||||
|
||||
void HandleFolderDeleted(Guid id) {
|
||||
_folders.RemoveAll(f => f.Id == id);
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
|
||||
55
MilkStream.Client/Components/Pages/Settings.razor.css
Normal file
55
MilkStream.Client/Components/Pages/Settings.razor.css
Normal file
@@ -0,0 +1,55 @@
|
||||
.settings-layout {
|
||||
display: flex;
|
||||
gap: 1.5rem;
|
||||
min-height: calc(100vh - 80px);
|
||||
padding: 1.5rem 0;
|
||||
}
|
||||
|
||||
.settings-sidebar {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
min-width: 180px;
|
||||
padding: 0.5rem;
|
||||
background: var(--bs-dark-bg-subtle);
|
||||
border-radius: 0.5rem;
|
||||
height: fit-content;
|
||||
position: sticky;
|
||||
top: 1.5rem;
|
||||
}
|
||||
|
||||
.settings-nav-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.5rem 0.75rem;
|
||||
border: none;
|
||||
background: none;
|
||||
border-radius: 0.375rem;
|
||||
color: var(--bs-body-color);
|
||||
cursor: pointer;
|
||||
transition: background 0.15s, color 0.15s;
|
||||
font-size: 0.9rem;
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.settings-nav-item:hover {
|
||||
background: var(--bs-tertiary-bg);
|
||||
}
|
||||
|
||||
.settings-nav-item.active {
|
||||
background: var(--bs-primary);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.settings-content {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.settings-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(380px, 1fr));
|
||||
gap: 1rem;
|
||||
}
|
||||
@@ -1,92 +0,0 @@
|
||||
@page "/Settings1"
|
||||
@using Butter.Dtos.Folder
|
||||
@using Butter.Dtos.Settings
|
||||
@using Butter.Settings
|
||||
@using Butter.Types
|
||||
@using MilkStream.Client.Components.SettingBoxes
|
||||
@using MilkStream.Client.Services
|
||||
@using System.Linq
|
||||
|
||||
@inject NavigationManager NavigationManager
|
||||
@inject LoginService LoginService
|
||||
@inject SettingsService SettingsService
|
||||
@inject FoldersService FoldersService
|
||||
|
||||
<h3>Settings</h3>
|
||||
@if (areSettingsLoading || settings is {Count: 0}) {
|
||||
<LoadSpinner/>
|
||||
} else {
|
||||
<div class="row row-cols-2">
|
||||
<div class="col">
|
||||
<!--Folder Scan Enabled-->
|
||||
<SettingSwitch Setting="settings!.FirstOrDefault(s => s.Name == Butter.Settings.Settings.FolderScanEnabled.AsString())"/>
|
||||
</div>
|
||||
<div class="col">
|
||||
<!--Folder Scan Interval-->
|
||||
<SettingRange Setting="settings!.FirstOrDefault(s => s.Name == Butter.Settings.Settings.FolderScanInterval.AsString())" MinValue="5" MaxValue="360" Step="5"/>
|
||||
</div>
|
||||
<div class="col">
|
||||
<!--File Upload Enabled-->
|
||||
<SettingSwitch Setting="settings!.FirstOrDefault(s => s.Name == Butter.Settings.Settings.FileUploadEnabled.AsString())"/>
|
||||
</div>
|
||||
<div class="col">
|
||||
<!--File Upload Max Size-->
|
||||
<SettingRange Setting="settings!.FirstOrDefault(s => s.Name == Butter.Settings.Settings.FileUploadMaxSize.AsString())" MinValue="1024" MaxValue="1000000" Step="1000"/>
|
||||
</div>
|
||||
<div class="col">
|
||||
<!--Thumbnail Path-->
|
||||
<SettingString Setting="settings!.FirstOrDefault(s => s.Name == Butter.Settings.Settings.ThumbnailPath.AsString())"/>
|
||||
</div>
|
||||
<div class="col">
|
||||
<!--Thumbnail Size-->
|
||||
<SettingRange Setting="settings!.FirstOrDefault(s => s.Name == Butter.Settings.Settings.ThumbnailSize.AsString())" IsPowerOfTwo="true" MinValue="8" MaxValue="12" Step="1"/>
|
||||
</div>
|
||||
<div class="col">
|
||||
<!--Previews Path-->
|
||||
<SettingString Setting="settings!.FirstOrDefault(s => s.Name == Butter.Settings.Settings.PreviewPath.AsString())"/>
|
||||
</div>
|
||||
<div class="col">
|
||||
<!--Previews Size-->
|
||||
<SettingRange Setting="settings!.FirstOrDefault(s => s.Name == Butter.Settings.Settings.PreviewSize.AsString())" IsPowerOfTwo="true" MinValue="8" MaxValue="14" Step="1"/>
|
||||
</div>
|
||||
<div class="col">
|
||||
<!--Max concurrent jobs -->
|
||||
<SettingRange Setting="settings!.FirstOrDefault(s => s.Name == Butter.Settings.Settings.MaxConcurrentJobs.AsString())" MinValue="1" MaxValue="32" Step="1"/>
|
||||
</div>
|
||||
<div class="col">
|
||||
<!-- User registration enabled -->
|
||||
<SettingSwitch Setting="settings!.FirstOrDefault(s => s.Name == Butter.Settings.Settings.UserRegistrationEnabled.AsString())"/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<button class="btn btn-outline-primary my-2" @onclick="SaveSettings"><i class="bi bi-floppy2 mx-1"></i>Save Changes</button>
|
||||
</div>
|
||||
}
|
||||
<h3>Folders</h3>
|
||||
|
||||
|
||||
@code {
|
||||
bool areSettingsLoading = true;
|
||||
List<SettingDto>? settings = new();
|
||||
|
||||
protected async override Task OnInitializedAsync() {
|
||||
if (LoginService.IsLoggedIn) {
|
||||
await LoadSettings();
|
||||
} else {
|
||||
LoginService.AuthInfoChanged += async (_, _) => {
|
||||
await LoadSettings();
|
||||
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async Task LoadSettings() {
|
||||
settings = await SettingsService.GetAllSettings();
|
||||
if(settings != null) areSettingsLoading = false;
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
void SaveSettings() {
|
||||
SettingsService.OnBeginSave();
|
||||
}
|
||||
}
|
||||
@@ -7,16 +7,41 @@
|
||||
[Parameter]
|
||||
public required SettingDto? Setting { get; set; }
|
||||
|
||||
private CancellationTokenSource? _debounceCts;
|
||||
|
||||
protected bool IsSaving { get; set; }
|
||||
protected bool IsSaved { get; set; }
|
||||
|
||||
protected virtual SettingDto? GetSetting() {
|
||||
return Setting;
|
||||
}
|
||||
|
||||
protected override void OnInitialized() {
|
||||
SettingsService.BeginSave += async (sender, args) => {
|
||||
protected async Task OnValueChanged() {
|
||||
_debounceCts?.Cancel();
|
||||
_debounceCts = new CancellationTokenSource();
|
||||
var token = _debounceCts.Token;
|
||||
|
||||
try {
|
||||
await Task.Delay(500, token);
|
||||
token.ThrowIfCancellationRequested();
|
||||
|
||||
var setting = GetSetting();
|
||||
if (setting == null) return;
|
||||
try { await SettingsService.UpdateSettingAsync(setting); }
|
||||
catch (Exception) { /* fire-and-forget: exception logged by SettingsService */ }
|
||||
};
|
||||
|
||||
IsSaving = true;
|
||||
await InvokeAsync(StateHasChanged);
|
||||
|
||||
await SettingsService.UpdateSettingAsync(setting);
|
||||
|
||||
IsSaving = false;
|
||||
IsSaved = true;
|
||||
await InvokeAsync(StateHasChanged);
|
||||
|
||||
await Task.Delay(2000);
|
||||
IsSaved = false;
|
||||
await InvokeAsync(StateHasChanged);
|
||||
} catch (OperationCanceledException) {
|
||||
// Debounce cancelled by another change
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,27 +8,19 @@
|
||||
<div class="card-header">@(Setting?.Name ?? "NO NAME")</div>
|
||||
<div class="card-body d-flex justify-content-between align-items-center">
|
||||
<p class="card-text">@(Setting?.Description ?? "No description provided")</p>
|
||||
<div>
|
||||
<input type="number" id="@Setting?.Name" class="form-control w-auto" @bind="currentValue" />
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<input type="number" id="@Setting?.Name" class="form-control w-auto" @bind="currentValue" @bind:after="() => OnValueChanged()" />
|
||||
@SaveIndicator
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
|
||||
private float currentValue;
|
||||
|
||||
protected override void OnInitialized() {
|
||||
SettingsService.BeginSave += async (sender, args) => {
|
||||
var setting = GetSetting();
|
||||
if (setting == null) return;
|
||||
try { await SettingsService.UpdateSettingAsync(setting); }
|
||||
catch (Exception) { /* fire-and-forget: exception logged by SettingsService */ }
|
||||
};
|
||||
if (float.TryParse(Setting?.Value, out float value)) {
|
||||
currentValue = value;
|
||||
} else {
|
||||
currentValue = 0; // Default value if parsing fails
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,4 +29,18 @@
|
||||
Setting.Value = currentValue.ToString(CultureInfo.InvariantCulture);
|
||||
return Setting;
|
||||
}
|
||||
|
||||
private RenderFragment SaveIndicator => builder => {
|
||||
if (IsSaving) {
|
||||
builder.OpenElement(0, "small");
|
||||
builder.AddAttribute(1, "class", "text-secondary");
|
||||
builder.AddContent(2, "Saving...");
|
||||
builder.CloseElement();
|
||||
} else if (IsSaved) {
|
||||
builder.OpenElement(0, "small");
|
||||
builder.AddAttribute(1, "class", "text-success");
|
||||
builder.AddContent(2, "Saved");
|
||||
builder.CloseElement();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -5,39 +5,34 @@
|
||||
|
||||
<div class="card mb-3">
|
||||
<div class="card-header">@(Setting?.Name ?? "NO NAME")</div>
|
||||
<div class="card-body d-flex justify-content-between align-items-center">
|
||||
<div class="card-body">
|
||||
<p class="card-text">@(Setting?.Description ?? "No description provided")</p>
|
||||
<div class="d-flex">
|
||||
<div class="mx-2">@(IsPowerOfTwo ? Math.Pow(2, currentValue) : currentValue)</div>
|
||||
<input type="range" id="@Setting?.Name" class="form-range" @bind="@currentValue" min="@MinValue" max="@MaxValue"/>
|
||||
<div class="d-flex align-items-center gap-3 mt-2">
|
||||
<span class="badge bg-secondary fs-6" style="min-width: 4.5rem;">@(IsPowerOfTwo ? Math.Pow(2, currentValue) : currentValue)</span>
|
||||
<input type="range" id="@Setting?.Name" class="form-range flex-grow-1"
|
||||
@bind:event="oninput" @bind="@currentValue" @bind:after="() => OnValueChanged()"
|
||||
min="@MinValue" max="@MaxValue" step="@Step"/>
|
||||
@SaveIndicator
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
[Parameter]
|
||||
public int MinValue {get; set;}
|
||||
public int MinValue { get; set; }
|
||||
[Parameter]
|
||||
public int MaxValue {get; set;} = 100;
|
||||
public int MaxValue { get; set; } = 100;
|
||||
[Parameter]
|
||||
public bool IsPowerOfTwo {get; set;}
|
||||
public bool IsPowerOfTwo { get; set; }
|
||||
[Parameter]
|
||||
public int Step {get; set;} = 1;
|
||||
public int Step { get; set; } = 1;
|
||||
private int currentValue;
|
||||
|
||||
protected override void OnInitialized() {
|
||||
SettingsService.BeginSave += async (sender, args) => {
|
||||
var setting = GetSetting();
|
||||
if (setting == null) return;
|
||||
try { await SettingsService.UpdateSettingAsync(setting); }
|
||||
catch (Exception) { /* fire-and-forget: exception logged by SettingsService */ }
|
||||
};
|
||||
|
||||
if (IsPowerOfTwo) {
|
||||
var converted = int.TryParse(Setting?.Value, out int value);
|
||||
currentValue = converted ? (int)Math.Log2(value) : 0; // Default value if parsing fails
|
||||
currentValue = int.TryParse(Setting?.Value, out int value) ? (int)Math.Log2(value) : 0;
|
||||
} else {
|
||||
currentValue = int.TryParse(Setting?.Value, out int value) ? value : 0; // Default value if parsing fails
|
||||
currentValue = int.TryParse(Setting?.Value, out int value) ? value : 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,4 +42,17 @@
|
||||
return Setting;
|
||||
}
|
||||
|
||||
private RenderFragment SaveIndicator => builder => {
|
||||
if (IsSaving) {
|
||||
builder.OpenElement(0, "small");
|
||||
builder.AddAttribute(1, "class", "text-secondary");
|
||||
builder.AddContent(2, "Saving...");
|
||||
builder.CloseElement();
|
||||
} else if (IsSaved) {
|
||||
builder.OpenElement(0, "small");
|
||||
builder.AddAttribute(1, "class", "text-success");
|
||||
builder.AddContent(2, "Saved");
|
||||
builder.CloseElement();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -7,8 +7,9 @@
|
||||
<div class="card-header">@(Setting?.Name ?? "NO NAME")</div>
|
||||
<div class="card-body d-flex justify-content-between align-items-center">
|
||||
<p class="card-text">@(Setting?.Description ?? "No description provided")</p>
|
||||
<div class="d-flex">
|
||||
<input type="text" id="@Setting?.Name" class="form-control" @bind="@currentValue" />
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<input type="text" id="@Setting?.Name" class="form-control" @bind="@currentValue" @bind:after="() => OnValueChanged()" />
|
||||
@SaveIndicator
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -18,12 +19,6 @@
|
||||
|
||||
protected override void OnInitialized() {
|
||||
base.OnInitialized();
|
||||
SettingsService.BeginSave += async (sender, args) => {
|
||||
var setting = GetSetting();
|
||||
if (setting == null) return;
|
||||
try { await SettingsService.UpdateSettingAsync(setting); }
|
||||
catch (Exception) { /* fire-and-forget: exception logged by SettingsService */ }
|
||||
};
|
||||
currentValue = Setting?.Value ?? string.Empty;
|
||||
}
|
||||
|
||||
@@ -32,4 +27,18 @@
|
||||
Setting.Value = currentValue;
|
||||
return Setting;
|
||||
}
|
||||
|
||||
private RenderFragment SaveIndicator => builder => {
|
||||
if (IsSaving) {
|
||||
builder.OpenElement(0, "small");
|
||||
builder.AddAttribute(1, "class", "text-secondary");
|
||||
builder.AddContent(2, "Saving...");
|
||||
builder.CloseElement();
|
||||
} else if (IsSaved) {
|
||||
builder.OpenElement(0, "small");
|
||||
builder.AddAttribute(1, "class", "text-success");
|
||||
builder.AddContent(2, "Saved");
|
||||
builder.CloseElement();
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -8,8 +8,11 @@
|
||||
<div class="card-header">@(Setting?.Name ?? "NO NAME")</div>
|
||||
<div class="card-body d-flex justify-content-between align-items-center">
|
||||
<p class="card-text">@(Setting?.Description ?? "No description provided")</p>
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<div class="form-switch">
|
||||
<input type="checkbox" id="@Setting?.Name" class="form-check-input m-1" @bind="isChecked"/>
|
||||
<input type="checkbox" id="@Setting?.Name" class="form-check-input m-1" @bind="isChecked" @bind:after="() => OnValueChanged()"/>
|
||||
</div>
|
||||
@SaveIndicator
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -18,12 +21,6 @@
|
||||
private bool isChecked;
|
||||
|
||||
protected override void OnInitialized() {
|
||||
SettingsService.BeginSave += async (sender, args) => {
|
||||
var setting = GetSetting();
|
||||
if (setting == null) return;
|
||||
try { await SettingsService.UpdateSettingAsync(setting); }
|
||||
catch (Exception) { /* fire-and-forget: exception logged by SettingsService */ }
|
||||
};
|
||||
if (bool.TryParse(Setting?.Value, out var parsedValue)) {
|
||||
isChecked = parsedValue;
|
||||
}
|
||||
@@ -34,4 +31,18 @@
|
||||
Setting.Value = isChecked.ToString();
|
||||
return Setting;
|
||||
}
|
||||
|
||||
private RenderFragment SaveIndicator => builder => {
|
||||
if (IsSaving) {
|
||||
builder.OpenElement(0, "small");
|
||||
builder.AddAttribute(1, "class", "text-secondary");
|
||||
builder.AddContent(2, "Saving...");
|
||||
builder.CloseElement();
|
||||
} else if (IsSaved) {
|
||||
builder.OpenElement(0, "small");
|
||||
builder.AddAttribute(1, "class", "text-success");
|
||||
builder.AddContent(2, "Saved");
|
||||
builder.CloseElement();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -26,15 +26,6 @@ public sealed class SettingsService : AuthServiceBase {
|
||||
Logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fired when a save operation begins.
|
||||
/// </summary>
|
||||
public event EventHandler? BeginSave;
|
||||
/// <summary>
|
||||
/// Invokes the <see cref="BeginSave"/> event.
|
||||
/// </summary>
|
||||
public void OnBeginSave() => BeginSave?.Invoke(this, EventArgs.Empty);
|
||||
|
||||
/// <summary>
|
||||
/// Gets all application settings.
|
||||
/// </summary>
|
||||
|
||||
@@ -20,8 +20,8 @@
|
||||
volumes:
|
||||
#- "/root/MilkyShots/storageImages:/diary:ro"
|
||||
- "./storageImages:/diary:ro"
|
||||
#- "/root/MilkyShots/thumbs:/app/thumbnails:rw"
|
||||
#- "/root/MilkyShots/previews:/app/previews:rw"
|
||||
- "thumbs:/app/thumbnails"
|
||||
- "previews:/app/previews"
|
||||
|
||||
database:
|
||||
#image: tensorchord/pgvecto-rs:pg17-v0.4.0
|
||||
@@ -57,3 +57,5 @@
|
||||
|
||||
volumes:
|
||||
pg_dbdata:
|
||||
thumbs:
|
||||
previews:
|
||||
|
||||
Reference in New Issue
Block a user