Add Internet Archive URL to Book model and UI

- Added a new column `InternetArchiveUrl` to the `Books` table in the database.
- Updated the `Book` model to include the `InternetArchiveUrl` property with validation attributes.
- Modified the `BooksController` to handle the new `InternetArchiveUrl` property in various endpoints.
- Enhanced the admin UI to allow input and display of the Internet Archive link in `BookDialog`, `BookImportDialog`, and `Books` pages.
- Updated localization files to include translations for "Internet Archive link" and "View on Internet Archive".
This commit is contained in:
2026-05-10 01:47:48 +01:00
parent b6fbc954c6
commit 9481f578af
20 changed files with 10972 additions and 15 deletions

View File

@@ -30,6 +30,14 @@ namespace Marechai.ApiClient.Models
public int? Edition { get; set; }
/// <summary>The id property</summary>
public long? Id { get; set; }
/// <summary>The internet_archive_url property</summary>
#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER
#nullable enable
public string? InternetArchiveUrl { get; set; }
#nullable restore
#else
public string InternetArchiveUrl { get; set; }
#endif
/// <summary>The isbn property</summary>
#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER
#nullable enable
@@ -110,6 +118,7 @@ namespace Marechai.ApiClient.Models
{ "cover_guid", n => { CoverGuid = n.GetGuidValue(); } },
{ "edition", n => { Edition = n.GetIntValue(); } },
{ "id", n => { Id = n.GetLongValue(); } },
{ "internet_archive_url", n => { InternetArchiveUrl = n.GetStringValue(); } },
{ "isbn", n => { Isbn = n.GetStringValue(); } },
{ "native_title", n => { NativeTitle = n.GetStringValue(); } },
{ "original_cover_extension", n => { OriginalCoverExtension = n.GetStringValue(); } },
@@ -134,6 +143,7 @@ namespace Marechai.ApiClient.Models
writer.WriteGuidValue("cover_guid", CoverGuid);
writer.WriteIntValue("edition", Edition);
writer.WriteLongValue("id", Id);
writer.WriteStringValue("internet_archive_url", InternetArchiveUrl);
writer.WriteStringValue("isbn", Isbn);
writer.WriteStringValue("native_title", NativeTitle);
writer.WriteStringValue("original_cover_extension", OriginalCoverExtension);

View File

@@ -1,5 +1,5 @@
{
"descriptionHash": "EF8A471973B5B2FC244AF87CD9FEC3CBCD1EAB14311EC2A99398CC11EBC9DDBED82790ACDCB38998C4559BC5F28D358034B1377F0070988F3D34E623FF99B385",
"descriptionHash": "354E86D017E9237894C1A6384EC2610C9287C8448F403FF3891932735D4B7068F18D65BAC8ADAEE624D7CBEFDDB36B22670384F9176CFE5F1C35CF172CDA6942",
"descriptionLocation": "../../../../../tmp/openapi.json",
"lockFileVersion": "1.0.0",
"kiotaVersion": "1.31.1",

View File

@@ -44,4 +44,6 @@ public class BookDto : DocumentBaseDto
public Guid? CoverGuid { get; set; }
[JsonPropertyName("original_cover_extension")]
public string? OriginalCoverExtension { get; set; }
[JsonPropertyName("internet_archive_url")]
public string? InternetArchiveUrl { get; set; }
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,30 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Marechai.Database.Migrations
{
/// <inheritdoc />
public partial class AddBookInternetArchiveUrl : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "InternetArchiveUrl",
table: "Books",
type: "varchar(2048)",
maxLength: 2048,
nullable: true)
.Annotation("MySql:CharSet", "utf8mb4");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "InternetArchiveUrl",
table: "Books");
}
}
}

View File

@@ -259,6 +259,10 @@ namespace Marechai.Database.Migrations
b.Property<int?>("Edition")
.HasColumnType("int");
b.Property<string>("InternetArchiveUrl")
.HasMaxLength(2048)
.HasColumnType("varchar(2048)");
b.Property<string>("Isbn")
.HasMaxLength(13)
.HasColumnType("varchar(13)");

View File

@@ -39,6 +39,9 @@ public class Book : DocumentBase
public long? SourceId { get; set; }
public Guid? CoverGuid { get; set; }
public string OriginalCoverExtension { get; set; }
[Url]
[StringLength(2048)]
public string InternetArchiveUrl { get; set; }
public virtual Book Previous { get; set; }
public virtual Book Source { get; set; }

View File

@@ -154,7 +154,8 @@ public class BooksController(
Pages = b.Pages,
Country = b.Country.Name,
CoverGuid = b.CoverGuid,
OriginalCoverExtension = b.OriginalCoverExtension
OriginalCoverExtension = b.OriginalCoverExtension,
InternetArchiveUrl = b.InternetArchiveUrl
})
.ToListAsync(cancellationToken);
}
@@ -200,7 +201,8 @@ public class BooksController(
Pages = b.Pages,
Country = b.Country.Name,
CoverGuid = b.CoverGuid,
OriginalCoverExtension = b.OriginalCoverExtension
OriginalCoverExtension = b.OriginalCoverExtension,
InternetArchiveUrl = b.InternetArchiveUrl
})
.ToListAsync(cancellationToken);
}
@@ -245,7 +247,8 @@ public class BooksController(
SourceId = b.SourceId,
Country = b.Country.Name,
CoverGuid = b.CoverGuid,
OriginalCoverExtension = b.OriginalCoverExtension
OriginalCoverExtension = b.OriginalCoverExtension,
InternetArchiveUrl = b.InternetArchiveUrl
})
.ToListAsync(cancellationToken);
}
@@ -271,7 +274,8 @@ public class BooksController(
SourceId = b.SourceId,
Country = b.Country.Name,
CoverGuid = b.CoverGuid,
OriginalCoverExtension = b.OriginalCoverExtension
OriginalCoverExtension = b.OriginalCoverExtension,
InternetArchiveUrl = b.InternetArchiveUrl
})
.FirstOrDefaultAsync();
@@ -326,6 +330,7 @@ public class BooksController(
CountryName = b.Country.Name,
b.CoverGuid,
b.OriginalCoverExtension,
b.InternetArchiveUrl,
PreviousTitle = b.PreviousId.HasValue ? b.Previous.Title : null,
SourceTitle = b.SourceId.HasValue ? b.Source.Title : null
})
@@ -430,7 +435,8 @@ public class BooksController(
SourceId = head.SourceId,
Country = head.CountryName,
CoverGuid = head.CoverGuid,
OriginalCoverExtension = head.OriginalCoverExtension
OriginalCoverExtension = head.OriginalCoverExtension,
InternetArchiveUrl = head.InternetArchiveUrl
};
List<PersonByBookDto> people = peopleTask.Result.OrderBy(p => p.FullName).ThenBy(p => p.Role).ToList();
@@ -475,6 +481,7 @@ public class BooksController(
model.Edition = dto.Edition;
model.PreviousId = dto.PreviousId;
model.SourceId = dto.SourceId;
model.InternetArchiveUrl = dto.InternetArchiveUrl;
await context.News.AddAsync(new News
{
@@ -512,7 +519,8 @@ public class BooksController(
Pages = dto.Pages,
Edition = dto.Edition,
PreviousId = dto.PreviousId,
SourceId = dto.SourceId
SourceId = dto.SourceId,
InternetArchiveUrl = dto.InternetArchiveUrl
};
await context.Books.AddAsync(model);

View File

@@ -85,6 +85,14 @@
Variant="Variant.Outlined"
Clearable="true"
Class="mb-4"/>
<MudTextField @bind-Value="InternetArchiveUrl"
Label="@L["Internet Archive link"]"
Variant="Variant.Outlined"
MaxLength="2048"
InputType="InputType.Url"
Placeholder="https://archive.org/details/..."
Class="mb-4"/>
</MudTabPanel>
@* Tab 2 - Synopsis (only for existing books) *@
@@ -469,6 +477,7 @@
[Parameter] public long? PreviousId { get; set; }
[Parameter] public long? SourceId { get; set; }
[Parameter] public bool HasCover { get; set; }
[Parameter] public string InternetArchiveUrl { get; set; }
[Inject] BooksService BooksService { get; set; } = null!;
[Inject] TranslationService TranslationService { get; set; } = null!;
@@ -988,7 +997,8 @@
Published = Published,
PublishedPrecision = PublishedPrecision,
PreviousId = _selectedPreviousBook?.Id,
SourceId = _selectedSourceBook?.Id
SourceId = _selectedSourceBook?.Id,
InternetArchiveUrl = string.IsNullOrWhiteSpace(InternetArchiveUrl) ? null : InternetArchiveUrl.Trim()
}));
}
}

View File

@@ -15,4 +15,5 @@ public sealed class BookDialogResult
public int PublishedPrecision { get; set; }
public long? PreviousId { get; set; }
public long? SourceId { get; set; }
public string InternetArchiveUrl { get; set; }
}

View File

@@ -168,6 +168,20 @@
</div>
</CellTemplate>
</TemplateColumn>
<TemplateColumn Title="@L["Internet Archive link"]">
<CellTemplate>
@if(!string.IsNullOrWhiteSpace(context.Item.InternetArchiveUrl))
{
<MudTooltip Text="@context.Item.InternetArchiveUrl">
<MudIconButton Icon="@Icons.Material.Filled.OpenInNew"
Size="Size.Small"
Color="Color.Primary"
Href="@context.Item.InternetArchiveUrl"
Target="_blank"/>
</MudTooltip>
}
</CellTemplate>
</TemplateColumn>
</Columns>
</MudDataGrid>
</MudStack>
@@ -305,7 +319,7 @@
static readonly string[] CsvHeaders =
[
"Title", "NativeTitle", "SortTitle", "ISBN", "Edition", "Pages",
"PublishedYear", "PublishedMonth", "PublishedDay", "Country"
"PublishedYear", "PublishedMonth", "PublishedDay", "Country", "InternetArchiveUrl"
];
static readonly Dictionary<string, string> CountryAliases = new(StringComparer.OrdinalIgnoreCase)
@@ -430,7 +444,8 @@
NativeTitle = GetValueOrNull(row, "NativeTitle"),
SortTitle = GetValueOrNull(row, "SortTitle"),
Isbn = GetValueOrNull(row, "ISBN"),
CountryInput = GetValueOrNull(row, "Country")
CountryInput = GetValueOrNull(row, "Country"),
InternetArchiveUrl = GetValueOrNull(row, "InternetArchiveUrl")
};
if(TryParseInt(row, "Edition", out int edition))
@@ -562,7 +577,8 @@
Pages = row.Pages,
CountryId = row.SelectedCountry?.Id,
Published = row.PublishedDate.HasValue ? new DateTimeOffset(row.PublishedDate.Value) : null,
PublishedPrecision = row.PublishedPrecision
PublishedPrecision = row.PublishedPrecision,
InternetArchiveUrl = row.InternetArchiveUrl
};
(long? id, string error) = await BooksService.CreateAsync(dto);
@@ -604,7 +620,8 @@
["PublishedYear"] = row.PublishedYear?.ToString() ?? "",
["PublishedMonth"] = row.PublishedMonth?.ToString() ?? "",
["PublishedDay"] = row.PublishedDay?.ToString() ?? "",
["Country"] = row.CountryInput ?? ""
["Country"] = row.CountryInput ?? "",
["InternetArchiveUrl"] = row.InternetArchiveUrl ?? ""
};
rows.Add(dict);

View File

@@ -41,6 +41,7 @@ public sealed class BookImportRow
public int? PublishedMonth { get; set; }
public int? PublishedDay { get; set; }
public string CountryInput { get; set; }
public string InternetArchiveUrl { get; set; }
public List<Iso31661NumericDto> MatchedCountries { get; set; } = [];
public Iso31661NumericDto SelectedCountry { get; set; }

View File

@@ -52,6 +52,20 @@
</CellTemplate>
</PropertyColumn>
<PropertyColumn Property="x => x.Country" Title="@L["Country"]"/>
<TemplateColumn Title="@L["Internet Archive link"]" Sortable="false" Filterable="false">
<CellTemplate>
@if(!string.IsNullOrWhiteSpace(context.Item.InternetArchiveUrl))
{
<MudTooltip Text="@context.Item.InternetArchiveUrl">
<MudIconButton Icon="@Icons.Material.Filled.OpenInNew"
Size="Size.Small"
Color="Color.Primary"
Href="@context.Item.InternetArchiveUrl"
Target="_blank"/>
</MudTooltip>
}
</CellTemplate>
</TemplateColumn>
<TemplateColumn Title="@L["Actions"]" Sortable="false" Filterable="false">
<CellTemplate>
<MudStack Row="true" Spacing="1">

View File

@@ -61,7 +61,8 @@ public partial class Books
PublishedPrecision = data.PublishedPrecision,
Published = data.Published.HasValue ? new DateTimeOffset(data.Published.Value, TimeSpan.Zero) : null,
PreviousId = data.PreviousId,
SourceId = data.SourceId
SourceId = data.SourceId,
InternetArchiveUrl = data.InternetArchiveUrl
};
(long? id, string errorMessage) = await BooksService.CreateAsync(dto);
@@ -99,7 +100,8 @@ public partial class Books
{ x => x.PublishedPrecision, fullBook.PublishedPrecision ?? 0 },
{ x => x.PreviousId, fullBook.PreviousId },
{ x => x.SourceId, fullBook.SourceId },
{ x => x.HasCover, fullBook.CoverGuid is not null }
{ x => x.HasCover, fullBook.CoverGuid is not null },
{ x => x.InternetArchiveUrl, fullBook.InternetArchiveUrl }
};
IDialogReference dialog = await DialogService.ShowAsync<BookDialog>(L["Edit Book"], parameters,
@@ -127,7 +129,8 @@ public partial class Books
CountryId = data.CountryId,
Published = data.Published.HasValue ? new DateTimeOffset(data.Published.Value, TimeSpan.Zero) : null,
PreviousId = data.PreviousId,
SourceId = data.SourceId
SourceId = data.SourceId,
InternetArchiveUrl = data.InternetArchiveUrl
};
(bool succeeded, string errorMessage) = await BooksService.UpdateAsync(book.Id ?? 0, dto);

View File

@@ -150,6 +150,20 @@
</td>
</tr>
}
@if(!string.IsNullOrEmpty(_book.InternetArchiveUrl))
{
<tr>
<td style="width: 33%; text-align: right;"><strong>@L["Internet Archive link"]</strong></td>
<td>
<MudLink Href="@_book.InternetArchiveUrl" Target="_blank" Underline="Underline.Always">
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="1">
<MudIcon Icon="@Icons.Material.Filled.OpenInNew" Size="Size.Small"/>
<span>@L["View on Internet Archive"]</span>
</MudStack>
</MudLink>
</td>
</tr>
}
</tbody>
</MudSimpleTable>
</MudCardContent>

View File

@@ -445,4 +445,10 @@
<data name="There are no companies in the database." xml:space="preserve">
<value>Es gibt keine Unternehmen in der Datenbank.</value>
</data>
<data name="Internet Archive link" xml:space="preserve">
<value>Internet Archive Link</value>
</data>
<data name="View on Internet Archive" xml:space="preserve">
<value>Auf Internet Archive ansehen</value>
</data>
</root>

View File

@@ -408,4 +408,10 @@
<data name="There are no companies in the database." xml:space="preserve">
<value>There are no companies in the database.</value>
</data>
<data name="Internet Archive link" xml:space="preserve">
<value>Internet Archive link</value>
</data>
<data name="View on Internet Archive" xml:space="preserve">
<value>View on Internet Archive</value>
</data>
</root>

View File

@@ -354,4 +354,10 @@
<data name="There are no companies in the database." xml:space="preserve">
<value>No hay compañías en la base de datos.</value>
</data>
<data name="Internet Archive link" xml:space="preserve">
<value>Enlace a Internet Archive</value>
</data>
<data name="View on Internet Archive" xml:space="preserve">
<value>Ver en Internet Archive</value>
</data>
</root>

View File

@@ -445,4 +445,10 @@
<data name="There are no companies in the database." xml:space="preserve">
<value>Il n'y a pas d'entreprises dans la base de données.</value>
</data>
<data name="Internet Archive link" xml:space="preserve">
<value>Lien Internet Archive</value>
</data>
<data name="View on Internet Archive" xml:space="preserve">
<value>Voir sur Internet Archive</value>
</data>
</root>

View File

@@ -445,4 +445,10 @@
<data name="There are no companies in the database." xml:space="preserve">
<value>Non ci sono aziende nel database.</value>
</data>
<data name="Internet Archive link" xml:space="preserve">
<value>Collegamento Internet Archive</value>
</data>
<data name="View on Internet Archive" xml:space="preserve">
<value>Visualizza su Internet Archive</value>
</data>
</root>