[ADD] Simple views for accounts and channels

This commit is contained in:
max
2025-09-22 15:50:30 +02:00
parent a7baeb0d73
commit a478943792
10 changed files with 180 additions and 79 deletions

View File

@@ -1,9 +1,14 @@
 
<MudNavMenu> <MudNavMenu>
<MudNavLink Href="/" Icon="@Icons.Material.Filled.Home" Match="NavLinkMatch.All">Home</MudNavLink> <MudNavLink Href="/" Icon="@Icons.Material.Filled.Home" Match="NavLinkMatch.All">Home</MudNavLink>
<MudNavLink Href="/Channels" Icon="@Icons.Material.Filled.SupervisorAccount" Match="NavLinkMatch.All">Channels</MudNavLink> <MudNavGroup Title="Library" Expanded Icon="@Icons.Custom.Brands.YouTube" IconColor="Color.Error">
<MudNavLink Href="/Library" Icon="@Icons.Material.Filled.LocalLibrary" Match="NavLinkMatch.All">Library</MudNavLink> <MudNavLink Href="/Accounts" Icon="@Icons.Material.Filled.AccountBox" Match="NavLinkMatch.All">Accounts</MudNavLink>
<MudNavLink Href="/Playlists" Icon="@Icons.Material.Filled.ViewList" Match="NavLinkMatch.All">Playlists</MudNavLink> <MudNavLink Href="/Channels" Icon="@Icons.Material.Filled.AccountCircle" Match="NavLinkMatch.All">Channels</MudNavLink>
<MudNavLink Href="/Development" Icon="@Icons.Material.Filled.DeveloperMode" Match="NavLinkMatch.All">Development</MudNavLink> <MudNavLink Href="/Playlists" Icon="@Icons.Material.Filled.ViewList" Match="NavLinkMatch.All">Playlists</MudNavLink>
<MudNavLink Href="/Services" Icon="@Icons.Material.Filled.MiscellaneousServices" Match="NavLinkMatch.All">Services</MudNavLink> <MudNavLink Href="/Library" Icon="@Icons.Material.Filled.Info" Match="NavLinkMatch.All" IconColor="Color.Info">Info</MudNavLink>
</MudNavGroup>
<MudNavGroup Title="Application" Expanded Icon="@Icons.Material.Filled.SettingsSystemDaydream" IconColor="Color.Primary">
<MudNavLink Href="/Development" Icon="@Icons.Material.Filled.DeveloperMode" Match="NavLinkMatch.All">Development</MudNavLink>
<MudNavLink Href="/Services" Icon="@Icons.Material.Filled.MiscellaneousServices" Match="NavLinkMatch.All">Services</MudNavLink>
</MudNavGroup>
</MudNavMenu> </MudNavMenu>

View File

@@ -0,0 +1,48 @@
@page "/Accounts"
@using Manager.App.Models.Settings
@using Manager.App.Services.System
@using Microsoft.Extensions.Options
@inject ILibraryService LibraryService
@inject IDialogService DialogService
@inject IOptions<LibrarySettings> LibraryOptions
@inject ClientService ClientService
@inject ISnackbar Snackbar
<PageTitle>Accounts</PageTitle>
<MudStack Spacing="2">
<MudPaper Elevation="0" Outlined>
<MudStack Row Class="ma-2">
<MudButton IconSize="Size.Small" StartIcon="@Icons.Material.Filled.Add" Variant="Variant.Outlined" OnClick="OnAddAccountDialogAsync">Add account</MudButton>
</MudStack>
</MudPaper>
<MudTable @ref="@_table" ServerData="ServerReload">
<ToolBarContent>
<MudText Typo="Typo.h6">Accounts</MudText>
<MudSpacer />
<MudTextField T="string" ValueChanged="@(s=>OnSearch(s))" Placeholder="Search" Adornment="Adornment.Start" DebounceInterval="300"
AdornmentIcon="@Icons.Material.Filled.Search" IconSize="Size.Medium" Class="mt-0"></MudTextField>
</ToolBarContent>
<HeaderContent>
<MudTh>Name</MudTh>
<MudTh>Handle</MudTh>
<MudTh>ID</MudTh>
</HeaderContent>
<RowTemplate>
<MudTd>@context.Channel?.Name</MudTd>
<MudTd>@context.Channel?.Handle</MudTd>
<MudTd>@context.Id</MudTd>
</RowTemplate>
<NoRecordsContent>
<MudText>No channels found</MudText>
</NoRecordsContent>
<LoadingContent>
<MudText>Loading...</MudText>
</LoadingContent>
<PagerContent>
<MudTablePager/>
</PagerContent>
</MudTable>
</MudStack>

View File

@@ -0,0 +1,74 @@
using Manager.App.Components.Dialogs;
using Manager.App.Models.Library;
using Manager.Data.Entities.LibraryContext;
using Microsoft.AspNetCore.Components;
using MudBlazor;
namespace Manager.App.Components.Pages;
public partial class Accounts : ComponentBase
{
private MudTable<ClientAccountEntity>? _table;
private readonly DialogOptions _dialogOptions = new() { BackdropClick = false, CloseButton = true, FullWidth = true, MaxWidth = MaxWidth.ExtraLarge };
private string _search = "";
private async Task<TableData<ClientAccountEntity>> ServerReload(TableState state, CancellationToken token)
{
var results = await LibraryService.GetAccountsAsync(_search, state.Page * state.PageSize, state.PageSize, token);
return !results.IsSuccess ? new TableData<ClientAccountEntity>() : new TableData<ClientAccountEntity>() { Items = results.Value, TotalItems = results.Total };
}
private void OnSearch(string text)
{
_search = text;
_table?.ReloadServerData();
}
private async Task OnAddAccountDialogAsync()
{
var libSettings = LibraryOptions.Value;
var parameters = new DialogParameters<AccountDialog> { { x => x.DefaultUserAgent, libSettings.DefaultUserAgent } };
var dialog = await DialogService.ShowAsync<AccountDialog>("Add account", parameters, _dialogOptions);
var result = await dialog.Result;
if (result == null || result.Canceled || result.Data == null)
{
return;
}
var clientChannel = (ClientChannel)result.Data;
if (clientChannel?.YouTubeClient == null)
{
Snackbar.Add("No YouTube client received.", Severity.Error);
return;
}
var savedClientResult = await ClientService.SaveClientAsync(clientChannel.YouTubeClient);
if (savedClientResult.IsSuccess)
{
if (_table != null)
{
await _table.ReloadServerData();
}
Snackbar.Add($"Client {clientChannel.Channel?.Handle ?? clientChannel.YouTubeClient.Id} saved!", Severity.Success);
ClientService.AddClient(clientChannel.YouTubeClient);
}
else
{
Snackbar.Add($"Failed to store client: {savedClientResult.Error?.Description ?? "Unknown!"}", Severity.Error);
}
if (clientChannel.Channel == null)
{
Snackbar.Add("No channel information received!", Severity.Warning);
}
else
{
var saveChannelResult = await LibraryService.SaveChannelAsync(clientChannel.Channel);
if (!saveChannelResult.IsSuccess)
{
Snackbar.Add("Failed to save channel information", Severity.Warning);
}
}
}
}

View File

@@ -1,37 +1,26 @@
@page "/Channels" @page "/Channels"
@using Manager.App.Models.Settings
@using Manager.App.Services.System
@using Microsoft.Extensions.Options
@inject ILibraryService LibraryService @inject ILibraryService LibraryService
@inject IDialogService DialogService
@inject IOptions<LibrarySettings> LibraryOptions
@inject ClientService ClientService
@inject ISnackbar Snackbar
<PageTitle>Channels</PageTitle> <PageTitle>Channels</PageTitle>
<MudStack Spacing="2"> <MudStack Spacing="2">
<MudPaper Elevation="0" Outlined>
<MudStack Row Class="ma-2">
<MudButton IconSize="Size.Small" StartIcon="@Icons.Material.Filled.Add" Variant="Variant.Outlined" OnClick="OnAddAccountDialogAsync">Add account</MudButton>
</MudStack>
</MudPaper>
<MudTable @ref="@_table" ServerData="ServerReload"> <MudTable @ref="@_table" ServerData="ServerReload">
<ToolBarContent> <ToolBarContent>
<MudText Typo="Typo.h6">Channels</MudText> <MudText Typo="Typo.h6">Channels stored in the library</MudText>
<MudSpacer />
<MudTextField T="string" ValueChanged="@(s=>OnSearch(s))" Placeholder="Search" Adornment="Adornment.Start" DebounceInterval="300"
AdornmentIcon="@Icons.Material.Filled.Search" IconSize="Size.Medium" Class="mt-0"></MudTextField>
</ToolBarContent> </ToolBarContent>
<HeaderContent> <HeaderContent>
<MudTh>Name</MudTh> <MudTh>Name</MudTh>
<MudTh>Handle</MudTh>
<MudTh>Channel id</MudTh> <MudTh>Channel id</MudTh>
<MudTh>Has login</MudTh>
</HeaderContent> </HeaderContent>
<RowTemplate> <RowTemplate>
<MudTd>@context.Channel?.Name</MudTd> <MudTd>@context.Name</MudTd>
<MudTd>@context.Handle</MudTd>
<MudTd>@context.Id</MudTd> <MudTd>@context.Id</MudTd>
<MudTd>@(context.HttpCookies.Any())</MudTd>
</RowTemplate> </RowTemplate>
<NoRecordsContent> <NoRecordsContent>
<MudText>No channels found</MudText> <MudText>No channels found</MudText>

View File

@@ -1,5 +1,3 @@
using Manager.App.Components.Dialogs;
using Manager.App.Models.Library;
using Manager.Data.Entities.LibraryContext; using Manager.Data.Entities.LibraryContext;
using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components;
using MudBlazor; using MudBlazor;
@@ -8,60 +6,18 @@ namespace Manager.App.Components.Pages;
public partial class Channels : ComponentBase public partial class Channels : ComponentBase
{ {
private readonly DialogOptions _dialogOptions = new() { BackdropClick = false, CloseButton = true, FullWidth = true, MaxWidth = MaxWidth.ExtraLarge }; private MudTable<ChannelEntity>? _table;
private MudTable<ClientAccountEntity>? _table; private string _search = "";
private async Task<TableData<ClientAccountEntity>> ServerReload(TableState state, CancellationToken token) private async Task<TableData<ChannelEntity>> ServerReload(TableState state, CancellationToken token)
{ {
var results = await LibraryService.GetAccountsAsync(null, state.Page * state.PageSize, state.PageSize, token); var results = await LibraryService.GetChannelsAsync(_search, state.Page * state.PageSize, state.PageSize, token);
return !results.IsSuccess ? new TableData<ClientAccountEntity>() : new TableData<ClientAccountEntity> { Items = results.Value, TotalItems = results.Total }; return !results.IsSuccess ? new TableData<ChannelEntity>() : new TableData<ChannelEntity> { Items = results.Value, TotalItems = results.Total };
} }
private async Task OnAddAccountDialogAsync() private void OnSearch(string text)
{ {
var libSettings = LibraryOptions.Value; _search = text;
var parameters = new DialogParameters<AccountDialog> { { x => x.DefaultUserAgent, libSettings.DefaultUserAgent } }; _table?.ReloadServerData();
var dialog = await DialogService.ShowAsync<AccountDialog>("Add account", parameters, _dialogOptions);
var result = await dialog.Result;
if (result == null || result.Canceled || result.Data == null)
{
return;
}
var clientChannel = (ClientChannel)result.Data;
if (clientChannel?.YouTubeClient == null)
{
Snackbar.Add("No YouTube client received.", Severity.Error);
return;
}
var savedClientResult = await ClientService.SaveClientAsync(clientChannel.YouTubeClient);
if (savedClientResult.IsSuccess)
{
if (_table != null)
{
await _table.ReloadServerData();
}
Snackbar.Add($"Client {clientChannel.Channel?.Handle ?? clientChannel.YouTubeClient.Id} saved!", Severity.Success);
ClientService.AddClient(clientChannel.YouTubeClient);
}
else
{
Snackbar.Add($"Failed to store client: {savedClientResult.Error?.Description ?? "Unknown!"}", Severity.Error);
}
if (clientChannel.Channel == null)
{
Snackbar.Add("No channel information received!", Severity.Warning);
}
else
{
var saveChannelResult = await LibraryService.SaveChannelAsync(clientChannel.Channel);
if (!saveChannelResult.IsSuccess)
{
Snackbar.Add("Failed to save channel information", Severity.Warning);
}
}
} }
} }

View File

@@ -14,5 +14,5 @@ public interface ILibraryService
public Task<Result> SaveChannelAsync(InnertubeChannel innertubeChannel, CancellationToken cancellationToken = default); public Task<Result> SaveChannelAsync(InnertubeChannel innertubeChannel, CancellationToken cancellationToken = default);
public Task<Result<LibraryInformation>> GetLibraryInfoAsync(CancellationToken cancellationToken = default); public Task<Result<LibraryInformation>> GetLibraryInfoAsync(CancellationToken cancellationToken = default);
public Task<ListResult<ClientAccountEntity>> GetAccountsAsync(string? search, int offset = 0, int total = 20, CancellationToken cancellationToken = default); public Task<ListResult<ClientAccountEntity>> GetAccountsAsync(string? search, int offset = 0, int total = 20, CancellationToken cancellationToken = default);
public Task<ListResult<ChannelEntity>> GetChannelsAsync(int offset = 0, int total = 20, CancellationToken cancellationToken = default); public Task<ListResult<ChannelEntity>> GetChannelsAsync(string? search, int offset = 0, int total = 20, CancellationToken cancellationToken = default);
} }

View File

@@ -151,6 +151,7 @@ public class LibraryService : ILibraryService
var channel = await context.Channels var channel = await context.Channels
.Include(c => c.ClientAccount) .Include(c => c.ClientAccount)
.ThenInclude(p => p!.HttpCookies) .ThenInclude(p => p!.HttpCookies)
.Include(f => f.Files)
.FirstOrDefaultAsync(c => c.Id == id, cancellationToken); .FirstOrDefaultAsync(c => c.Id == id, cancellationToken);
if (channel == null) if (channel == null)
@@ -288,13 +289,33 @@ public class LibraryService : ILibraryService
} }
} }
public async Task<ListResult<ChannelEntity>> GetChannelsAsync(int offset = 0, int total = 20, CancellationToken cancellationToken = default) public async Task<ListResult<ChannelEntity>> GetChannelsAsync(string? search, int offset = 0, int total = 20, CancellationToken cancellationToken = default)
{ {
try try
{ {
await using var context = await _dbContextFactory.CreateDbContextAsync(cancellationToken); await using var context = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
var orderedAccounts = context.Channels.Include(x => x.ClientAccount).OrderBy(x => x.Id); var orderedAccounts = context.Channels.OrderBy(x => x.Id);
return new ListResultReturn<ChannelEntity>(orderedAccounts.Skip(offset).Take(total).ToList(),orderedAccounts.Count());
var totalChannels = orderedAccounts.Count();
if (!string.IsNullOrWhiteSpace(search) && orderedAccounts.Any())
{
var normalizedSearch = $"%{search.ToLower()}%";
var searched = orderedAccounts
.Where(ca =>
EF.Functions.Like(
(
ca.Id.ToString() + " " +
ca.Name + " " +
ca.Handle
).ToLower(),
normalizedSearch
)
);
totalChannels = searched.Count();
orderedAccounts = searched.OrderByDescending(ca => ca.Id);
}
return new ListResultReturn<ChannelEntity>(totalChannels == 0 ? [] : orderedAccounts.Skip(offset).Take(total).ToList(), totalChannels);
} }
catch (Exception e) catch (Exception e)
{ {

View File

@@ -58,6 +58,9 @@ public sealed class LibraryDbContext : DbContext
.WithOne(x => x.Channel) .WithOne(x => x.Channel)
.HasForeignKey<ClientAccountEntity>(e => e.Id) .HasForeignKey<ClientAccountEntity>(e => e.Id)
.IsRequired(false); .IsRequired(false);
channel.HasMany(x => x.Files)
.WithOne()
.HasForeignKey(f => f.ForeignKey);
}); });
modelBuilder.Entity<ClientAccountEntity>(cae => modelBuilder.Entity<ClientAccountEntity>(cae =>
@@ -71,6 +74,9 @@ public sealed class LibraryDbContext : DbContext
.WithOne(ca => ca.ClientAccount) .WithOne(ca => ca.ClientAccount)
.HasForeignKey<ChannelEntity>(ce => ce.Id) .HasForeignKey<ChannelEntity>(ce => ce.Id)
.IsRequired(false); .IsRequired(false);
cae.HasMany(x => x.Files)
.WithOne()
.HasForeignKey(f => f.ForeignKey);
}); });
modelBuilder.Entity<HttpCookieEntity>(httpce => modelBuilder.Entity<HttpCookieEntity>(httpce =>

View File

@@ -17,4 +17,5 @@ public class ChannelEntity : DateTimeBase
public List<MediaEntity> Media { get; set; } = []; public List<MediaEntity> Media { get; set; } = [];
public List<PlaylistEntity> Playlists { get; set; } = []; public List<PlaylistEntity> Playlists { get; set; } = [];
public ClientAccountEntity? ClientAccount { get; set; } public ClientAccountEntity? ClientAccount { get; set; }
public List<FileEntity>? Files { get; set; }
} }

View File

@@ -12,4 +12,5 @@ public class ClientAccountEntity : DateTimeBase
[MaxLength(DataConstants.DbContext.DefaultDbStringSize)] [MaxLength(DataConstants.DbContext.DefaultDbStringSize)]
public string? UserAgent { get; set; } public string? UserAgent { get; set; }
public ChannelEntity? Channel { get; set; } public ChannelEntity? Channel { get; set; }
public List<FileEntity>? Files { get; set; }
} }