150 lines
5.0 KiB
Plaintext
150 lines
5.0 KiB
Plaintext
@inherits LocalizableComponentBase
|
|||
|
|
|
||
|
|
@* The persona's follows, lists, blocks, mutes, blocked servers and bookmarks, exported and imported as the CSV files
|
||
|
|
Mastodon writes, which most fediverse servers read too: a persona moves here from another server, or away, with what
|
||
|
|
it gathered there. An import acts one row at a time, through PrivaPub, and can be stopped. *@
|
||
|
|
<div class="flex flex-col gap-5 w-full">
|
||
|
|
<section class="flex flex-col gap-2" data-export>
|
||
|
|
<h3 class="font-semibold text-sm">@Localizer["Export"]</h3>
|
||
|
|
<p class="text-sm opacity-70">@Localizer["Files in Mastodon's format, which most fediverse servers import."]</p>
|
||
|
|
<div class="flex flex-wrap gap-2">
|
||
|
|
@foreach (var data in Kinds)
|
||
|
|
{
|
||
|
|
<DButton Size="DSize.Sm" Disabled="@(IsDefaultDisabled || exporting is not null || importing is not null)" OnClick="() => Export(data)"
|
||
|
|
data-export-kind="@data">
|
||
|
|
@if (exporting == data)
|
||
|
|
{
|
||
|
|
<DLoading Size="DSize.Xs" />
|
||
|
|
}
|
||
|
|
else
|
||
|
|
{
|
||
|
|
<i class="ion-md-download"></i>
|
||
|
|
}
|
||
|
|
<span>@Name(data)</span>
|
||
|
|
</DButton>
|
||
|
|
}
|
||
|
|
</div>
|
||
|
|
</section>
|
||
|
|
|
||
|
|
<section class="flex flex-col gap-2" data-import>
|
||
|
|
<h3 class="font-semibold text-sm">@Localizer["Import"]</h3>
|
||
|
|
<div class="flex flex-wrap gap-2 items-end">
|
||
|
|
<label class="flex flex-col gap-1 text-sm">
|
||
|
|
<span>@Localizer["What the file holds"]</span>
|
||
|
|
<DSelect TValue="PortableData" Size="DSize.Sm" @bind-Value="importKind" disabled="@(importing is not null)" data-import-kind>
|
||
|
|
@foreach (var data in Kinds)
|
||
|
|
{
|
||
|
|
<option value="@data">@Name(data)</option>
|
||
|
|
}
|
||
|
|
</DSelect>
|
||
|
|
</label>
|
||
|
|
<DFileInput Size="DSize.Sm" Accept=".csv,text/csv" OnChange="Pick" disabled="@(importing is not null)" data-import-file />
|
||
|
|
@if (importing is null)
|
||
|
|
{
|
||
|
|
<DButton Size="DSize.Sm" Color="DColor.Primary" Disabled="@(IsDefaultDisabled || file is null || exporting is not null)" OnClick="Import"
|
||
|
|
data-import-start>
|
||
|
|
<i class="ion-md-cloud-upload"></i><span>@Localizer["Import"]</span>
|
||
|
|
</DButton>
|
||
|
|
}
|
||
|
|
else
|
||
|
|
{
|
||
|
|
<DButton Size="DSize.Sm" Color="DColor.Warning" OnClick="Stop">@Localizer["Stop"]</DButton>
|
||
|
|
}
|
||
|
|
</div>
|
||
|
|
@if (report is not null)
|
||
|
|
{
|
||
|
|
<div class="flex flex-col gap-1" data-import-report>
|
||
|
|
<DProgress class="w-full max-w-md" Value="@(report.Done + report.Failed.Count)" Max="@Math.Max(1, report.Total)" />
|
||
|
|
<span class="text-sm tabular-nums">
|
||
|
|
@Localizer["{0} of {1} done, {2} could not be.", report.Done, report.Total, report.Failed.Count]
|
||
|
|
</span>
|
||
|
|
<ErrorViewer AdditionalErrors="report.Failed" />
|
||
|
|
</div>
|
||
|
|
}
|
||
|
|
</section>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
@code {
|
||
|
|
[Inject] PortabilityService PortabilityService { get; set; }
|
||
|
|
[Inject] IBlazorDownloadFileService DownloadFile { get; set; }
|
||
|
|
[Inject] ILoggingService Logger { get; set; }
|
||
|
|
|
||
|
|
// an export file of 5000 rows stays well under this
|
||
|
|
const long MaximumFileSize = 4 * 1024 * 1024;
|
||
|
|
static readonly PortableData[] Kinds = Enum.GetValues<PortableData>();
|
||
|
|
|
||
|
|
PortableData? exporting;
|
||
|
|
PortableData? importing;
|
||
|
|
PortableData importKind = PortableData.Follows;
|
||
|
|
string file;
|
||
|
|
ImportReport report;
|
||
|
|
CancellationTokenSource stopping;
|
||
|
|
|
||
|
|
protected override void OnInitialized() => IsLoading = false;
|
||
|
|
|
||
|
|
string Name(PortableData data) => data switch
|
||
|
|
{
|
||
|
|
PortableData.Follows => Localizer["Follows"],
|
||
|
|
PortableData.Lists => Localizer["Lists"],
|
||
|
|
PortableData.Blocks => Localizer["Blocked accounts"],
|
||
|
|
PortableData.Mutes => Localizer["Muted accounts"],
|
||
|
|
PortableData.ServerBlocks => Localizer["Blocked servers"],
|
||
|
|
_ => Localizer["Bookmarks"]
|
||
|
|
};
|
||
|
|
|
||
|
|
async Task Export(PortableData data)
|
||
|
|
{
|
||
|
|
exporting = data;
|
||
|
|
var bytes = await PortabilityService.Export(data, CascadingState.PersonaId);
|
||
|
|
exporting = null;
|
||
|
|
await DownloadFile.DownloadFileAsync(PortabilityService.FileName(data), bytes, "text/csv");
|
||
|
|
}
|
||
|
|
|
||
|
|
async Task Pick(InputFileChangeEventArgs eventArgs)
|
||
|
|
{
|
||
|
|
file = null;
|
||
|
|
report = null;
|
||
|
|
if (eventArgs.File.Size > MaximumFileSize)
|
||
|
|
{
|
||
|
|
Toast.AddWarning(Localizer["Supplied file \"{0}\" with size {1} exceeds the maximum of {2}.", eventArgs.File.Name,
|
||
|
|
eventArgs.File.Size.GetFileSize(Localizer), MaximumFileSize.GetFileSize(Localizer)]);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
try
|
||
|
|
{
|
||
|
|
using var reader = new StreamReader(eventArgs.File.OpenReadStream(MaximumFileSize));
|
||
|
|
file = await reader.ReadToEndAsync();
|
||
|
|
}
|
||
|
|
catch (Exception ex)
|
||
|
|
{
|
||
|
|
await Logger.ProcessError(ex, $"{nameof(DataTransfer)}.{nameof(Pick)}:{eventArgs.File.Name}");
|
||
|
|
Toast.AddWarning(Localizer["The file \"{0}\" could not be read.", eventArgs.File.Name]);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
async Task Import()
|
||
|
|
{
|
||
|
|
importing = importKind;
|
||
|
|
stopping = new();
|
||
|
|
report = new();
|
||
|
|
var progress = new Progress<ImportReport>(step =>
|
||
|
|
{
|
||
|
|
report = step;
|
||
|
|
StateHasChanged();
|
||
|
|
});
|
||
|
|
report = await PortabilityService.Import(importKind, file, progress, stopping.Token);
|
||
|
|
importing = null;
|
||
|
|
stopping.Dispose();
|
||
|
|
stopping = null;
|
||
|
|
Toast.AddResponseSuccess(Localizer["{0} of {1} done, {2} could not be.", report.Done, report.Total, report.Failed.Count]);
|
||
|
|
}
|
||
|
|
|
||
|
|
void Stop() => stopping?.Cancel();
|
||
|
|
|
||
|
|
public override async ValueTask DisposeAsync()
|
||
|
|
{
|
||
|
|
stopping?.Cancel();
|
||
|
|
await base.DisposeAsync();
|
||
|
|
}
|
||
|
|
}
|