mirror of
https://github.com/aliasvault/aliasvault.git
synced 2026-05-18 05:18:05 -04:00
Add auto refresh to emails page (#976)
This commit is contained in:
committed by
Leendert de Borst
parent
70b7063af2
commit
d9d84dd90f
@@ -92,7 +92,7 @@
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="flex items-center justify-center h-full text-gray-500 dark:text-gray-400">
|
||||
<div class="flex bg-gray-50 dark:bg-gray-700 items-center justify-center h-full text-gray-500 dark:text-gray-400">
|
||||
<div class="text-center">
|
||||
<svg class="mx-auto h-12 w-12 text-gray-300 dark:text-gray-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 8l7.89 4.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"/>
|
||||
|
||||
@@ -17,6 +17,10 @@
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15.172 7l-6.586 6.586a2 2 0 102.828 2.828l6.414-6.586a4 4 0 00-5.656-5.656l-6.415 6.585a6 6 0 108.486 8.486L20.5 13"></path>
|
||||
</svg>
|
||||
}
|
||||
@if (IsNewEmail)
|
||||
{
|
||||
<div class="w-2 h-2 ml-1 bg-yellow-500 rounded-full animate-pulse flex-shrink-0" title="New email"></div>
|
||||
}
|
||||
</div>
|
||||
<!-- Subject (smaller, below from) -->
|
||||
<div class="text-sm text-gray-600 dark:text-gray-300 truncate mb-1">
|
||||
@@ -55,4 +59,10 @@
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public bool IsSelected { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether this email is new and should show an indicator.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public bool IsNewEmail { get; set; }
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
@inject HttpClient HttpClient
|
||||
@inject ILogger<Home> Logger
|
||||
@inject MinDurationLoadingService LoadingService
|
||||
@implements IAsyncDisposable
|
||||
|
||||
<LayoutPageTitle>Emails</LayoutPageTitle>
|
||||
|
||||
@@ -26,6 +27,10 @@
|
||||
Title="Emails"
|
||||
Description="You can view all emails received by your private email addresses below.">
|
||||
<CustomActions>
|
||||
@if (DbService.Settings.AutoEmailRefresh)
|
||||
{
|
||||
<div class="w-3 h-3 mr-2 rounded-full bg-primary-300 border-2 border-primary-100 animate-pulse" title="Auto-refresh enabled"></div>
|
||||
}
|
||||
<RefreshButton OnClick="RefreshData" ButtonText="Refresh" />
|
||||
</CustomActions>
|
||||
</PageHeader>
|
||||
@@ -96,7 +101,8 @@ else
|
||||
<EmailRow
|
||||
Email="email"
|
||||
OnEmailClick="ShowAliasVaultEmailInModal"
|
||||
IsSelected="false" />
|
||||
IsSelected="false"
|
||||
IsNewEmail="@(NewEmailIds.Contains(email.Id))" />
|
||||
}
|
||||
}
|
||||
</ul>
|
||||
@@ -104,65 +110,71 @@ else
|
||||
</div>
|
||||
|
||||
<!-- Desktop Layout (lg and up) - Sidebar and Preview with Load More -->
|
||||
<div class="hidden lg:flex mt-6 h-[calc(100vh-300px)] min-h-[600px] rounded-lg overflow-hidden">
|
||||
<!-- Left Sidebar - Email List -->
|
||||
<div class="w-1/4 bg-white border border-r-0 dark:bg-gray-800 dark:border-gray-700 flex flex-col">
|
||||
<div class="flex-1 overflow-y-auto" id="email-list-container">
|
||||
<ul>
|
||||
@if (EmailList.Count == 0)
|
||||
{
|
||||
<li class="p-4 text-center text-gray-500 dark:text-gray-300">
|
||||
No emails have been received yet.
|
||||
</li>
|
||||
}
|
||||
else
|
||||
{
|
||||
@foreach (var email in EmailList)
|
||||
{
|
||||
<EmailRow
|
||||
Email="email"
|
||||
OnEmailClick="SelectEmailForPreview"
|
||||
IsSelected="@(SelectedEmailId == email.Id)" />
|
||||
}
|
||||
}
|
||||
<!-- Load More Button for Desktop -->
|
||||
@if (HasMoreEmails && EmailList.Count > 0)
|
||||
{
|
||||
<li class="border-t border-gray-200 dark:border-gray-600 p-3 bg-gray-50 dark:bg-gray-700">
|
||||
<button @onclick="LoadMoreEmails"
|
||||
disabled="@IsLoadingMore"
|
||||
class="w-full px-4 py-2 text-sm font-medium text-primary-600 bg-primary-50 border border-primary-200 rounded-md hover:bg-primary-100 focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed dark:text-primary-400 dark:bg-primary-900/20 dark:border-primary-800 dark:hover:bg-primary-900/30">
|
||||
@if (IsLoadingMore)
|
||||
{
|
||||
<span class="flex items-center justify-center">
|
||||
<svg class="animate-spin -ml-1 mr-2 h-4 w-4" fill="none" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>
|
||||
Loading...
|
||||
</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span>Load More (@(TotalRecords - EmailList.Count) remaining)</span>
|
||||
}
|
||||
</button>
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
<div class="hidden lg:flex mt-6 rounded-lg overflow-hidden">
|
||||
@if (EmailList.Count == 0)
|
||||
{
|
||||
<!-- Single row message for desktop when no emails -->
|
||||
<div class="w-full bg-white border rounded-lg dark:bg-gray-800 dark:border-gray-700 overflow-hidden">
|
||||
<div class="p-4 text-center text-gray-500 dark:text-gray-300">
|
||||
No emails have been received yet.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="w-full h-[calc(100vh-300px)] min-h-[600px] flex rounded-lg overflow-hidden">
|
||||
<!-- Left Sidebar - Email List -->
|
||||
<div class="w-1/4 bg-white border border-r-0 dark:bg-gray-800 dark:border-gray-700 flex flex-col">
|
||||
<div class="flex-1 overflow-y-auto" id="email-list-container">
|
||||
<ul>
|
||||
@foreach (var email in EmailList)
|
||||
{
|
||||
<EmailRow
|
||||
Email="email"
|
||||
OnEmailClick="SelectEmailForPreview"
|
||||
IsSelected="@(SelectedEmailId == email.Id)"
|
||||
IsNewEmail="@(NewEmailIds.Contains(email.Id))" />
|
||||
}
|
||||
<!-- Load More Button for Desktop -->
|
||||
@if (HasMoreEmails && EmailList.Count > 0)
|
||||
{
|
||||
<li class="border-t border-gray-200 dark:border-gray-600 p-3 bg-gray-50 dark:bg-gray-700">
|
||||
<button @onclick="LoadMoreEmails"
|
||||
disabled="@IsLoadingMore"
|
||||
class="w-full px-4 py-2 text-sm font-medium text-primary-600 bg-primary-50 border border-primary-200 rounded-md hover:bg-primary-100 focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed dark:text-primary-400 dark:bg-primary-900/20 dark:border-primary-800 dark:hover:bg-primary-900/30">
|
||||
@if (IsLoadingMore)
|
||||
{
|
||||
<span class="flex items-center justify-center">
|
||||
<svg class="animate-spin -ml-1 mr-2 h-4 w-4" fill="none" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>
|
||||
Loading...
|
||||
</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span>Load More (@(TotalRecords - EmailList.Count) remaining)</span>
|
||||
}
|
||||
</button>
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Right Panel - Email Preview -->
|
||||
<div class="w-3/4">
|
||||
<EmailPreview
|
||||
Email="SelectedEmail"
|
||||
IsSpamOk="false"
|
||||
OnEmailDeleted="HandleEmailDeleted"
|
||||
CredentialId="@GetSelectedEmailCredentialId()"
|
||||
CredentialName="@GetSelectedEmailCredentialName()"
|
||||
OnCredentialClick="NavigateToCredential" />
|
||||
</div>
|
||||
<!-- Right Panel - Email Preview -->
|
||||
<div class="w-3/4">
|
||||
<EmailPreview
|
||||
Email="SelectedEmail"
|
||||
IsSpamOk="false"
|
||||
OnEmailDeleted="HandleEmailDeleted"
|
||||
CredentialId="@GetSelectedEmailCredentialId()"
|
||||
CredentialName="@GetSelectedEmailCredentialName()"
|
||||
OnCredentialClick="NavigateToCredential" />
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@@ -181,6 +193,241 @@ else
|
||||
private bool IsLoadingMore { get; set; }
|
||||
private bool HasMoreEmails => TotalRecords > EmailList.Count;
|
||||
|
||||
// Auto-refresh related properties
|
||||
private const int ACTIVE_TAB_REFRESH_INTERVAL = 2000; // 2 seconds
|
||||
private CancellationTokenSource? _pollingCts;
|
||||
private DotNetObjectReference<Home>? _dotNetRef;
|
||||
private bool _isPageVisible = true;
|
||||
private HashSet<int> NewEmailIds { get; set; } = new();
|
||||
private HashSet<int> _knownEmailIds = new();
|
||||
|
||||
/// <summary>
|
||||
/// Callback invoked by JavaScript when the page visibility changes. This is used to start/stop the polling for new emails.
|
||||
/// </summary>
|
||||
/// <param name="isVisible">Indicates whether the page is visible or not.</param>
|
||||
[JSInvokable]
|
||||
public void OnVisibilityChange(bool isVisible)
|
||||
{
|
||||
_isPageVisible = isVisible;
|
||||
|
||||
if (isVisible && DbService.Settings.AutoEmailRefresh)
|
||||
{
|
||||
// Start polling if visible and auto-refresh is enabled
|
||||
StartPolling();
|
||||
}
|
||||
else
|
||||
{
|
||||
// Stop polling if hidden
|
||||
StopPolling();
|
||||
}
|
||||
|
||||
// If becoming visible, do an immediate refresh
|
||||
if (isVisible)
|
||||
{
|
||||
_ = CheckForNewEmails();
|
||||
}
|
||||
}
|
||||
|
||||
private void StartPolling()
|
||||
{
|
||||
// If already polling, no need to start again
|
||||
if (_pollingCts != null) {
|
||||
return;
|
||||
}
|
||||
|
||||
_pollingCts = new CancellationTokenSource();
|
||||
|
||||
// Start polling task
|
||||
_ = PollForNewEmails(_pollingCts.Token);
|
||||
}
|
||||
|
||||
private void StopPolling()
|
||||
{
|
||||
if (_pollingCts != null)
|
||||
{
|
||||
_pollingCts.Cancel();
|
||||
_pollingCts.Dispose();
|
||||
_pollingCts = null;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task PollForNewEmails(CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
while (!cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
await CheckForNewEmails();
|
||||
await Task.Delay(ACTIVE_TAB_REFRESH_INTERVAL, cancellationToken);
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// Normal cancellation, ignore.
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError(ex, "Error in email refresh polling");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check for new emails without disrupting the current view.
|
||||
/// </summary>
|
||||
private async Task CheckForNewEmails()
|
||||
{
|
||||
if (!_isPageVisible || !DbService.Settings.AutoEmailRefresh)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var emailClaimList = await DbService.GetEmailClaimListAsync();
|
||||
|
||||
if (emailClaimList.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var requestModel = new MailboxBulkRequest
|
||||
{
|
||||
Page = 1,
|
||||
PageSize = 10, // Only check the latest 10 emails for new ones
|
||||
Addresses = emailClaimList,
|
||||
};
|
||||
|
||||
var request = new HttpRequestMessage(HttpMethod.Post, $"v1/EmailBox/bulk");
|
||||
request.Content = new StringContent(JsonSerializer.Serialize(requestModel), Encoding.UTF8, "application/json");
|
||||
|
||||
var response = await HttpClient.SendAsync(request);
|
||||
if (response.IsSuccessStatusCode)
|
||||
{
|
||||
var mailbox = await response.Content.ReadFromJsonAsync<MailboxBulkResponse>();
|
||||
if (mailbox?.Mails != null)
|
||||
{
|
||||
var context = await DbService.GetDbContextAsync();
|
||||
var credentialLookup = await context.Credentials
|
||||
.Include(x => x.Service)
|
||||
.Include(x => x.Alias)
|
||||
.Where(x => x.Alias.Email != null)
|
||||
.GroupBy(x => x.Alias.Email!.ToLower())
|
||||
.ToDictionaryAsync(
|
||||
g => g.Key,
|
||||
g => new { Id = g.First().Id, ServiceName = g.First().Service.Name ?? "Unknown" }
|
||||
);
|
||||
|
||||
List<MailboxEmailApiModel> decryptedEmailList;
|
||||
try
|
||||
{
|
||||
decryptedEmailList = await EmailService.DecryptEmailList(mailbox.Mails);
|
||||
}
|
||||
catch (InvalidOperationException ex) when (ex.Message.Contains("Sequence contains no matching element"))
|
||||
{
|
||||
// Handle case where encryption keys are not available for some emails
|
||||
Logger.LogWarning(ex, "Failed to decrypt some emails due to missing encryption keys");
|
||||
return;
|
||||
}
|
||||
|
||||
var newEmails = decryptedEmailList.Select(email =>
|
||||
{
|
||||
var toEmail = email.ToLocal + "@" + email.ToDomain;
|
||||
var credentialInfo = credentialLookup.TryGetValue(toEmail.ToLower(), out var info)
|
||||
? info
|
||||
: new { Id = Guid.Empty, ServiceName = "Unknown" };
|
||||
|
||||
return new MailListViewModel
|
||||
{
|
||||
Id = email.Id,
|
||||
Date = email.DateSystem,
|
||||
FromName = email.FromDisplay,
|
||||
FromEmail = email.FromLocal + "@" + email.FromDomain,
|
||||
ToEmail = toEmail,
|
||||
Subject = email.Subject,
|
||||
MessagePreview = email.MessagePreview,
|
||||
CredentialId = credentialInfo.Id,
|
||||
CredentialName = credentialInfo.ServiceName,
|
||||
HasAttachments = email.HasAttachments,
|
||||
};
|
||||
}).ToList();
|
||||
|
||||
// Check for new emails
|
||||
var newEmailIds = new HashSet<int>();
|
||||
foreach (var email in newEmails)
|
||||
{
|
||||
if (!_knownEmailIds.Contains(email.Id))
|
||||
{
|
||||
newEmailIds.Add(email.Id);
|
||||
}
|
||||
}
|
||||
|
||||
// Update the known email IDs
|
||||
foreach (var email in newEmails)
|
||||
{
|
||||
_knownEmailIds.Add(email.Id);
|
||||
}
|
||||
|
||||
// Add new emails to the list and mark them as new
|
||||
if (newEmailIds.Count > 0)
|
||||
{
|
||||
// Add new emails to the beginning of the list
|
||||
var emailsToAdd = newEmails.Where(e => newEmailIds.Contains(e.Id)).ToList();
|
||||
EmailList.InsertRange(0, emailsToAdd);
|
||||
|
||||
// Update total records
|
||||
TotalRecords += emailsToAdd.Count;
|
||||
|
||||
// Mark emails as new
|
||||
NewEmailIds.UnionWith(newEmailIds);
|
||||
|
||||
// Remove new email indicators after 30 seconds
|
||||
_ = Task.Delay(30000).ContinueWith(_ =>
|
||||
{
|
||||
NewEmailIds.ExceptWith(newEmailIds);
|
||||
InvokeAsync(StateHasChanged);
|
||||
});
|
||||
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError(ex, "An error occurred while checking for new emails");
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
await base.OnInitializedAsync();
|
||||
|
||||
// Create a single object reference for JS interop
|
||||
_dotNetRef = DotNetObjectReference.Create(this);
|
||||
await JsInteropService.RegisterVisibilityCallback(_dotNetRef);
|
||||
|
||||
// Only start polling if auto-refresh is enabled and page is visible
|
||||
if (DbService.Settings.AutoEmailRefresh && _isPageVisible)
|
||||
{
|
||||
StartPolling();
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
// Stop polling
|
||||
StopPolling();
|
||||
|
||||
// Unregister the visibility callback using the same reference
|
||||
if (_dotNetRef != null)
|
||||
{
|
||||
await JsInteropService.UnregisterVisibilityCallback(_dotNetRef);
|
||||
_dotNetRef.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
{
|
||||
@@ -211,6 +458,8 @@ else
|
||||
// Reset pagination for fresh load
|
||||
CurrentPage = 1;
|
||||
EmailList.Clear();
|
||||
NewEmailIds.Clear();
|
||||
_knownEmailIds.Clear();
|
||||
|
||||
var emailClaimList = await DbService.GetEmailClaimListAsync();
|
||||
|
||||
@@ -353,6 +602,10 @@ else
|
||||
else
|
||||
{
|
||||
EmailList = newEmails;
|
||||
// Initialize known email IDs for auto-refresh - don't mark existing emails as new
|
||||
_knownEmailIds = new HashSet<int>(newEmails.Select(e => e.Id));
|
||||
// Clear any existing new email indicators since this is the initial load
|
||||
NewEmailIds.Clear();
|
||||
}
|
||||
|
||||
CurrentPage = model.CurrentPage;
|
||||
@@ -373,6 +626,9 @@ else
|
||||
/// </summary>
|
||||
private async Task ShowAliasVaultEmailInModal(int emailId)
|
||||
{
|
||||
// Remove new email mark when email is clicked
|
||||
NewEmailIds.Remove(emailId);
|
||||
|
||||
EmailApiModel? mail = await HttpClient.GetFromJsonAsync<EmailApiModel>($"v1/Email/{emailId}");
|
||||
if (mail != null)
|
||||
{
|
||||
@@ -412,6 +668,9 @@ else
|
||||
/// </summary>
|
||||
private async Task SelectEmailForPreview(int emailId)
|
||||
{
|
||||
// Remove new email mark when email is clicked
|
||||
NewEmailIds.Remove(emailId);
|
||||
|
||||
SelectedEmailId = emailId;
|
||||
await LoadSelectedEmailForPreview(emailId);
|
||||
}
|
||||
@@ -457,6 +716,10 @@ else
|
||||
SelectedEmail = null;
|
||||
}
|
||||
|
||||
// Remove from new email indicators
|
||||
NewEmailIds.Remove(emailId);
|
||||
_knownEmailIds.Remove(emailId);
|
||||
|
||||
// Refresh the email list
|
||||
await RefreshData();
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
|
||||
<div class="flex items-center mb-4">
|
||||
<input @bind="AutoEmailRefresh" @bind:after="UpdateAutoEmailRefresh" id="autoEmailRefresh" type="checkbox" class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600">
|
||||
<label for="autoEmailRefresh" class="ml-2 text-sm font-medium text-gray-900 dark:text-gray-300">Auto email refresh on credential page</label>
|
||||
<label for="autoEmailRefresh" class="ml-2 text-sm font-medium text-gray-900 dark:text-gray-300">Auto refresh emails content when new ones arrive</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -945,10 +945,6 @@ video {
|
||||
height: 2.25rem;
|
||||
}
|
||||
|
||||
.h-\[calc\(100vh-300px\)\] {
|
||||
height: calc(100vh - 300px);
|
||||
}
|
||||
|
||||
.h-full {
|
||||
height: 100%;
|
||||
}
|
||||
@@ -957,26 +953,30 @@ video {
|
||||
height: 1px;
|
||||
}
|
||||
|
||||
.max-h-32 {
|
||||
max-height: 8rem;
|
||||
.h-\[calc\(100vh-300px\)\] {
|
||||
height: calc(100vh - 300px);
|
||||
}
|
||||
|
||||
.max-h-\[90vh\] {
|
||||
max-height: 90vh;
|
||||
}
|
||||
|
||||
.max-h-32 {
|
||||
max-height: 8rem;
|
||||
}
|
||||
|
||||
.min-h-\[250px\] {
|
||||
min-height: 250px;
|
||||
}
|
||||
|
||||
.min-h-\[600px\] {
|
||||
min-height: 600px;
|
||||
}
|
||||
|
||||
.min-h-screen {
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.min-h-\[600px\] {
|
||||
min-height: 600px;
|
||||
}
|
||||
|
||||
.w-1 {
|
||||
width: 0.25rem;
|
||||
}
|
||||
@@ -989,14 +989,6 @@ video {
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
.w-1\/3 {
|
||||
width: 33.333333%;
|
||||
}
|
||||
|
||||
.w-1\/4 {
|
||||
width: 25%;
|
||||
}
|
||||
|
||||
.w-10 {
|
||||
width: 2.5rem;
|
||||
}
|
||||
@@ -1033,10 +1025,6 @@ video {
|
||||
width: 1rem;
|
||||
}
|
||||
|
||||
.w-40 {
|
||||
width: 10rem;
|
||||
}
|
||||
|
||||
.w-48 {
|
||||
width: 12rem;
|
||||
}
|
||||
@@ -1045,6 +1033,10 @@ video {
|
||||
width: 1.25rem;
|
||||
}
|
||||
|
||||
.w-56 {
|
||||
width: 14rem;
|
||||
}
|
||||
|
||||
.w-6 {
|
||||
width: 1.5rem;
|
||||
}
|
||||
@@ -1069,8 +1061,20 @@ video {
|
||||
width: 100vw;
|
||||
}
|
||||
|
||||
.w-56 {
|
||||
width: 14rem;
|
||||
.w-1\/3 {
|
||||
width: 33.333333%;
|
||||
}
|
||||
|
||||
.w-1\/4 {
|
||||
width: 25%;
|
||||
}
|
||||
|
||||
.w-40 {
|
||||
width: 10rem;
|
||||
}
|
||||
|
||||
.w-2 {
|
||||
width: 0.5rem;
|
||||
}
|
||||
|
||||
.min-w-0 {
|
||||
@@ -1279,12 +1283,6 @@ video {
|
||||
margin-left: calc(1.5rem * calc(1 - var(--tw-space-x-reverse)));
|
||||
}
|
||||
|
||||
.space-y-1 > :not([hidden]) ~ :not([hidden]) {
|
||||
--tw-space-y-reverse: 0;
|
||||
margin-top: calc(0.25rem * calc(1 - var(--tw-space-y-reverse)));
|
||||
margin-bottom: calc(0.25rem * var(--tw-space-y-reverse));
|
||||
}
|
||||
|
||||
.space-y-2 > :not([hidden]) ~ :not([hidden]) {
|
||||
--tw-space-y-reverse: 0;
|
||||
margin-top: calc(0.5rem * calc(1 - var(--tw-space-y-reverse)));
|
||||
@@ -1309,6 +1307,12 @@ video {
|
||||
margin-bottom: calc(1.5rem * var(--tw-space-y-reverse));
|
||||
}
|
||||
|
||||
.space-y-1 > :not([hidden]) ~ :not([hidden]) {
|
||||
--tw-space-y-reverse: 0;
|
||||
margin-top: calc(0.25rem * calc(1 - var(--tw-space-y-reverse)));
|
||||
margin-bottom: calc(0.25rem * var(--tw-space-y-reverse));
|
||||
}
|
||||
|
||||
.divide-y > :not([hidden]) ~ :not([hidden]) {
|
||||
--tw-divide-y-reverse: 0;
|
||||
border-top-width: calc(1px * calc(1 - var(--tw-divide-y-reverse)));
|
||||
@@ -1406,14 +1410,14 @@ video {
|
||||
border-width: 1px;
|
||||
}
|
||||
|
||||
.border-0 {
|
||||
border-width: 0px;
|
||||
}
|
||||
|
||||
.border-2 {
|
||||
border-width: 2px;
|
||||
}
|
||||
|
||||
.border-0 {
|
||||
border-width: 0px;
|
||||
}
|
||||
|
||||
.border-b {
|
||||
border-bottom-width: 1px;
|
||||
}
|
||||
@@ -1446,25 +1450,11 @@ video {
|
||||
border-right-width: 0px;
|
||||
}
|
||||
|
||||
.border-dashed {
|
||||
border-style: dashed;
|
||||
}
|
||||
|
||||
.border-amber-400 {
|
||||
--tw-border-opacity: 1;
|
||||
border-color: rgb(251 191 36 / var(--tw-border-opacity));
|
||||
}
|
||||
|
||||
.border-blue-200 {
|
||||
--tw-border-opacity: 1;
|
||||
border-color: rgb(191 219 254 / var(--tw-border-opacity));
|
||||
}
|
||||
|
||||
.border-blue-500 {
|
||||
--tw-border-opacity: 1;
|
||||
border-color: rgb(59 130 246 / var(--tw-border-opacity));
|
||||
}
|
||||
|
||||
.border-blue-700 {
|
||||
--tw-border-opacity: 1;
|
||||
border-color: rgb(29 78 216 / var(--tw-border-opacity));
|
||||
@@ -1485,11 +1475,6 @@ video {
|
||||
border-color: rgb(209 213 219 / var(--tw-border-opacity));
|
||||
}
|
||||
|
||||
.border-gray-400 {
|
||||
--tw-border-opacity: 1;
|
||||
border-color: rgb(156 163 175 / var(--tw-border-opacity));
|
||||
}
|
||||
|
||||
.border-green-500 {
|
||||
--tw-border-opacity: 1;
|
||||
border-color: rgb(34 197 94 / var(--tw-border-opacity));
|
||||
@@ -1530,6 +1515,11 @@ video {
|
||||
border-color: rgb(251 203 116 / var(--tw-border-opacity));
|
||||
}
|
||||
|
||||
.border-yellow-400 {
|
||||
--tw-border-opacity: 1;
|
||||
border-color: rgb(250 204 21 / var(--tw-border-opacity));
|
||||
}
|
||||
|
||||
.bg-amber-100 {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(254 243 199 / var(--tw-bg-opacity));
|
||||
@@ -1704,6 +1694,11 @@ video {
|
||||
background-color: rgb(255 224 150 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.bg-yellow-500 {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(234 179 8 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.bg-opacity-50 {
|
||||
--tw-bg-opacity: 0.5;
|
||||
}
|
||||
@@ -2045,11 +2040,6 @@ video {
|
||||
color: rgb(229 231 235 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.text-gray-300 {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(209 213 219 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.text-gray-400 {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(156 163 175 / var(--tw-text-opacity));
|
||||
@@ -2155,6 +2145,11 @@ video {
|
||||
color: rgb(133 77 14 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.text-gray-300 {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(209 213 219 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.underline {
|
||||
text-decoration-line: underline;
|
||||
}
|
||||
@@ -2167,14 +2162,14 @@ video {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.opacity-25 {
|
||||
opacity: 0.25;
|
||||
}
|
||||
|
||||
.opacity-50 {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.opacity-25 {
|
||||
opacity: 0.25;
|
||||
}
|
||||
|
||||
.opacity-75 {
|
||||
opacity: 0.75;
|
||||
}
|
||||
@@ -2311,11 +2306,6 @@ video {
|
||||
transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
|
||||
}
|
||||
|
||||
.hover\:bg-blue-100:hover {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(219 234 254 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.hover\:bg-blue-600:hover {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(37 99 235 / var(--tw-bg-opacity));
|
||||
@@ -2638,11 +2628,6 @@ video {
|
||||
border-color: rgb(59 130 246 / var(--tw-border-opacity));
|
||||
}
|
||||
|
||||
.dark\:border-blue-800:is(.dark *) {
|
||||
--tw-border-opacity: 1;
|
||||
border-color: rgb(30 64 175 / var(--tw-border-opacity));
|
||||
}
|
||||
|
||||
.dark\:border-gray-400:is(.dark *) {
|
||||
--tw-border-opacity: 1;
|
||||
border-color: rgb(156 163 175 / var(--tw-border-opacity));
|
||||
@@ -2726,14 +2711,6 @@ video {
|
||||
background-color: rgb(30 58 138 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.dark\:bg-blue-900\/20:is(.dark *) {
|
||||
background-color: rgb(30 58 138 / 0.2);
|
||||
}
|
||||
|
||||
.dark\:bg-blue-900\/30:is(.dark *) {
|
||||
background-color: rgb(30 58 138 / 0.3);
|
||||
}
|
||||
|
||||
.dark\:bg-gray-500:is(.dark *) {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(107 114 128 / var(--tw-bg-opacity));
|
||||
@@ -2830,6 +2807,10 @@ video {
|
||||
background-color: rgb(123 74 30 / 0.3);
|
||||
}
|
||||
|
||||
.dark\:bg-yellow-900\/20:is(.dark *) {
|
||||
background-color: rgb(113 63 18 / 0.2);
|
||||
}
|
||||
|
||||
.dark\:bg-opacity-80:is(.dark *) {
|
||||
--tw-bg-opacity: 0.8;
|
||||
}
|
||||
@@ -2992,10 +2973,6 @@ video {
|
||||
background-color: rgb(29 78 216 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.dark\:hover\:bg-blue-900\/30:hover:is(.dark *) {
|
||||
background-color: rgb(30 58 138 / 0.3);
|
||||
}
|
||||
|
||||
.dark\:hover\:bg-gray-500:hover:is(.dark *) {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(107 114 128 / var(--tw-bg-opacity));
|
||||
@@ -3090,11 +3067,6 @@ video {
|
||||
color: rgb(244 149 65 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.dark\:hover\:text-red-300:hover:is(.dark *) {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(252 165 165 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.dark\:hover\:text-red-400:hover:is(.dark *) {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(248 113 113 / var(--tw-text-opacity));
|
||||
@@ -3105,6 +3077,11 @@ video {
|
||||
color: rgb(255 255 255 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.dark\:hover\:text-red-300:hover:is(.dark *) {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(252 165 165 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.dark\:hover\:file\:bg-primary-800\/60:is(.dark *)::file-selector-button:hover {
|
||||
background-color: rgb(154 93 38 / 0.6);
|
||||
}
|
||||
@@ -3423,10 +3400,6 @@ video {
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.lg\:grid-cols-4 {
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.lg\:flex-row {
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user