Importer Phase 1D
This commit is contained in:
parent
a39b5cb7d8
commit
ed341587de
@ -13,7 +13,7 @@ public interface IImportRepository
|
||||
|
||||
public sealed class ImportRepository(ISqlConnectionFactory connectionFactory) : IImportRepository
|
||||
{
|
||||
private const string ImportMarker = "Imported via JSON Import Phase 1D";
|
||||
private const string ImportMarker = "Imported via JSON Import Phase 1E";
|
||||
|
||||
public async Task<bool> ProjectTitleExistsAsync(string projectTitle)
|
||||
{
|
||||
@ -46,6 +46,10 @@ public sealed class ImportRepository(ISqlConnectionFactory connectionFactory) :
|
||||
var plotLinesCreated = 0;
|
||||
var plotThreadsCreated = 0;
|
||||
var threadEventsCreated = 0;
|
||||
var storyAssetsCreated = 0;
|
||||
var assetStatesCreated = 0;
|
||||
var assetEventsCreated = 0;
|
||||
var assetCustodyEventsCreated = 0;
|
||||
var skippedFields = GetSkippedOptionalFields(package);
|
||||
var unresolvedWarnings = GetUnresolvedWarnings(package);
|
||||
var sceneMap = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
|
||||
@ -355,6 +359,138 @@ public sealed class ImportRepository(ISqlConnectionFactory connectionFactory) :
|
||||
}
|
||||
}
|
||||
|
||||
var firstSceneId = sceneMap.Values.FirstOrDefault();
|
||||
foreach (var storyAsset in package.StoryAssets)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(storyAsset.Name))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var initialLocationId = ResolveId(locationMap, storyAsset.InitialLocationName);
|
||||
var initialOwnerCharacterId = ResolveId(characterMap, storyAsset.InitialOwnerCharacterName);
|
||||
var storyAssetId = await connection.QuerySingleAsync<int>(
|
||||
"dbo.StoryAsset_Save",
|
||||
new
|
||||
{
|
||||
StoryAssetID = 0,
|
||||
ProjectID = projectId,
|
||||
AssetName = Clean(DisplayName(storyAsset.DisplayName, storyAsset.Name)),
|
||||
AssetKindID = ResolveAssetKindId(lookupIds, storyAsset.Type),
|
||||
Description = CombineText(
|
||||
storyAsset.Description,
|
||||
MetadataLine("Type", storyAsset.Type),
|
||||
initialOwnerCharacterId.HasValue ? null : MetadataLine("Initial owner", storyAsset.InitialOwnerCharacterName),
|
||||
initialLocationId.HasValue ? null : MetadataLine("Initial location", storyAsset.InitialLocationName),
|
||||
storyAsset.Notes,
|
||||
ImportMarker),
|
||||
Importance = ClampImportance(storyAsset.Importance),
|
||||
CurrentStateID = ResolveAssetStateId(lookupIds, storyAsset.States.FirstOrDefault(x => !string.IsNullOrWhiteSpace(x.StateName))?.StateName),
|
||||
CurrentLocationID = initialLocationId,
|
||||
IsResolved = false
|
||||
},
|
||||
transaction,
|
||||
commandType: CommandType.StoredProcedure);
|
||||
storyAssetsCreated++;
|
||||
|
||||
if (initialOwnerCharacterId.HasValue && firstSceneId > 0)
|
||||
{
|
||||
await connection.QuerySingleAsync<int>(
|
||||
"dbo.AssetCustodyEvent_Save",
|
||||
new
|
||||
{
|
||||
AssetCustodyEventID = 0,
|
||||
StoryAssetID = storyAssetId,
|
||||
SceneID = firstSceneId,
|
||||
AssetCustodyEventTypeID = ResolveAssetCustodyEventTypeId(lookupIds, "Has"),
|
||||
Description = CombineText("Initial owner imported from JSON.", ImportMarker),
|
||||
CustodianNames = storyAsset.InitialOwnerCharacterName,
|
||||
CustodyRoleID = ResolveCustodyRoleId(lookupIds, "Has Custody")
|
||||
},
|
||||
transaction,
|
||||
commandType: CommandType.StoredProcedure);
|
||||
assetCustodyEventsCreated++;
|
||||
}
|
||||
|
||||
var importedAssetStateSceneIds = new HashSet<int>();
|
||||
foreach (var state in storyAsset.States)
|
||||
{
|
||||
if (!TryResolveAssetStateScene(sceneMap, state, out var sceneId) || importedAssetStateSceneIds.Contains(sceneId))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
await connection.QuerySingleAsync<int>(
|
||||
"dbo.AssetEvent_Save",
|
||||
new
|
||||
{
|
||||
AssetEventID = 0,
|
||||
StoryAssetID = storyAssetId,
|
||||
SceneID = sceneId,
|
||||
AssetEventTypeID = ResolveAssetEventTypeId(lookupIds, "State Changed"),
|
||||
FromStateID = (int?)null,
|
||||
ToStateID = ResolveAssetStateId(lookupIds, state.StateName),
|
||||
EventTitle = Truncate(Clean(state.StateName).Length == 0 ? "Imported asset state" : state.StateName, 200),
|
||||
EventDescription = CombineText(state.Description, state.Notes, ImportMarker)
|
||||
},
|
||||
transaction,
|
||||
commandType: CommandType.StoredProcedure);
|
||||
importedAssetStateSceneIds.Add(sceneId);
|
||||
assetStatesCreated++;
|
||||
}
|
||||
|
||||
var importedAssetEventSceneIds = new HashSet<int>();
|
||||
foreach (var assetEvent in storyAsset.Events)
|
||||
{
|
||||
if (!TryResolveAssetEventScene(sceneMap, assetEvent, out var sceneId) || importedAssetEventSceneIds.Contains(sceneId))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var eventLocationId = ResolveId(locationMap, assetEvent.LocationName);
|
||||
if (eventLocationId.HasValue)
|
||||
{
|
||||
await connection.QuerySingleAsync<int>(
|
||||
"dbo.SceneAssetLocation_Save",
|
||||
new
|
||||
{
|
||||
SceneAssetLocationID = 0,
|
||||
SceneID = sceneId,
|
||||
StoryAssetID = storyAssetId,
|
||||
LocationID = eventLocationId.Value,
|
||||
Description = CombineText(assetEvent.Summary, assetEvent.Notes, ImportMarker),
|
||||
UpdateCurrentLocation = true
|
||||
},
|
||||
transaction,
|
||||
commandType: CommandType.StoredProcedure);
|
||||
}
|
||||
|
||||
await connection.QuerySingleAsync<int>(
|
||||
"dbo.AssetEvent_Save",
|
||||
new
|
||||
{
|
||||
AssetEventID = 0,
|
||||
StoryAssetID = storyAssetId,
|
||||
SceneID = sceneId,
|
||||
AssetEventTypeID = ResolveAssetEventTypeId(lookupIds, assetEvent.EventType),
|
||||
FromStateID = (int?)null,
|
||||
ToStateID = (int?)null,
|
||||
EventTitle = Truncate(Clean(assetEvent.Title).Length == 0 ? Clean(assetEvent.Summary) : assetEvent.Title, 200) ?? "Imported asset event",
|
||||
EventDescription = CombineText(
|
||||
assetEvent.Summary,
|
||||
MetadataLine("Event type", assetEvent.EventType),
|
||||
MetadataLine("Character", assetEvent.CharacterName),
|
||||
eventLocationId.HasValue ? null : MetadataLine("Location", assetEvent.LocationName),
|
||||
assetEvent.Notes,
|
||||
ImportMarker)
|
||||
},
|
||||
transaction,
|
||||
commandType: CommandType.StoredProcedure);
|
||||
importedAssetEventSceneIds.Add(sceneId);
|
||||
assetEventsCreated++;
|
||||
}
|
||||
}
|
||||
|
||||
transaction.Commit();
|
||||
return new ImportResult
|
||||
{
|
||||
@ -370,6 +506,10 @@ public sealed class ImportRepository(ISqlConnectionFactory connectionFactory) :
|
||||
PlotLinesCreated = plotLinesCreated,
|
||||
PlotThreadsCreated = plotThreadsCreated,
|
||||
ThreadEventsCreated = threadEventsCreated,
|
||||
StoryAssetsCreated = storyAssetsCreated,
|
||||
AssetStatesCreated = assetStatesCreated,
|
||||
AssetEventsCreated = assetEventsCreated,
|
||||
AssetCustodyEventsCreated = assetCustodyEventsCreated,
|
||||
SkippedOptionalFields = skippedFields,
|
||||
UnresolvedWarnings = unresolvedWarnings,
|
||||
Message = "Import complete."
|
||||
@ -415,6 +555,21 @@ public sealed class ImportRepository(ISqlConnectionFactory connectionFactory) :
|
||||
var threadEventTypes = (await connection.QueryAsync<ImportLookupRow>(
|
||||
"SELECT ThreadEventTypeID AS LookupID, TypeName FROM dbo.ThreadEventTypes WHERE IsActive = 1;",
|
||||
transaction: transaction)).ToDictionary(x => x.TypeName, x => x.LookupID, StringComparer.OrdinalIgnoreCase);
|
||||
var assetKinds = (await connection.QueryAsync<ImportLookupRow>(
|
||||
"SELECT AssetKindID AS LookupID, KindName AS TypeName FROM dbo.AssetKinds WHERE IsActive = 1;",
|
||||
transaction: transaction)).ToDictionary(x => x.TypeName, x => x.LookupID, StringComparer.OrdinalIgnoreCase);
|
||||
var assetStates = (await connection.QueryAsync<ImportLookupRow>(
|
||||
"SELECT AssetStateID AS LookupID, StateName AS TypeName FROM dbo.AssetStates WHERE IsActive = 1;",
|
||||
transaction: transaction)).ToDictionary(x => x.TypeName, x => x.LookupID, StringComparer.OrdinalIgnoreCase);
|
||||
var assetEventTypes = (await connection.QueryAsync<ImportLookupRow>(
|
||||
"SELECT AssetEventTypeID AS LookupID, TypeName FROM dbo.AssetEventTypes WHERE IsActive = 1;",
|
||||
transaction: transaction)).ToDictionary(x => x.TypeName, x => x.LookupID, StringComparer.OrdinalIgnoreCase);
|
||||
var assetCustodyEventTypes = (await connection.QueryAsync<ImportLookupRow>(
|
||||
"SELECT AssetCustodyEventTypeID AS LookupID, TypeName FROM dbo.AssetCustodyEventTypes WHERE IsActive = 1;",
|
||||
transaction: transaction)).ToDictionary(x => x.TypeName, x => x.LookupID, StringComparer.OrdinalIgnoreCase);
|
||||
var custodyRoles = (await connection.QueryAsync<ImportLookupRow>(
|
||||
"SELECT CustodyRoleID AS LookupID, RoleName AS TypeName FROM dbo.CustodyRoles WHERE IsActive = 1;",
|
||||
transaction: transaction)).ToDictionary(x => x.TypeName, x => x.LookupID, StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
if (!revisionStatusId.HasValue || !timeModeId.HasValue || !timeConfidenceId.HasValue)
|
||||
{
|
||||
@ -431,7 +586,12 @@ public sealed class ImportRepository(ISqlConnectionFactory connectionFactory) :
|
||||
plotLineTypes,
|
||||
threadTypes,
|
||||
threadStatuses,
|
||||
threadEventTypes);
|
||||
threadEventTypes,
|
||||
assetKinds,
|
||||
assetStates,
|
||||
assetEventTypes,
|
||||
assetCustodyEventTypes,
|
||||
custodyRoles);
|
||||
}
|
||||
|
||||
private static string FormatBookTitle(ImportBookDto book)
|
||||
@ -475,6 +635,11 @@ public sealed class ImportRepository(ISqlConnectionFactory connectionFactory) :
|
||||
skipped.Add("Thread event impact and importance were stored in event descriptions.");
|
||||
}
|
||||
|
||||
if (package.StoryAssets.Any(asset => asset.Events.Any(assetEvent => !string.IsNullOrWhiteSpace(assetEvent.CharacterName))))
|
||||
{
|
||||
skipped.Add("Asset event character names were stored in event descriptions.");
|
||||
}
|
||||
|
||||
return skipped;
|
||||
}
|
||||
|
||||
@ -522,6 +687,48 @@ public sealed class ImportRepository(ISqlConnectionFactory connectionFactory) :
|
||||
warnings.Add($"Unresolved thread event scene reference: {reference}");
|
||||
}
|
||||
|
||||
foreach (var name in package.StoryAssets.Select(asset => asset.InitialOwnerCharacterName?.Trim())
|
||||
.Concat(package.StoryAssets.SelectMany(asset => asset.Events.Select(assetEvent => assetEvent.CharacterName?.Trim())))
|
||||
.Where(name => !string.IsNullOrWhiteSpace(name) && !characterNames.Contains(name))
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
warnings.Add($"Unmatched asset character: {name}");
|
||||
}
|
||||
|
||||
foreach (var name in package.StoryAssets.Select(asset => asset.InitialLocationName?.Trim())
|
||||
.Concat(package.StoryAssets.SelectMany(asset => asset.Events.Select(assetEvent => assetEvent.LocationName?.Trim())))
|
||||
.Where(name => !string.IsNullOrWhiteSpace(name) && !locationNames.Contains(name))
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
warnings.Add($"Unmatched asset location: {name}");
|
||||
}
|
||||
|
||||
foreach (var reference in package.StoryAssets.SelectMany(asset => asset.States.Select((state, index) => new
|
||||
{
|
||||
AssetName = DisplayName(asset.DisplayName, asset.Name),
|
||||
Label = string.IsNullOrWhiteSpace(state.StateName) ? $"State {index + 1}" : state.StateName.Trim(),
|
||||
State = state
|
||||
}))
|
||||
.Where(x => !x.State.BookOrder.HasValue || !x.State.ChapterOrder.HasValue || !x.State.SceneOrder.HasValue || !sceneKeys.Contains(SceneKey(x.State.BookOrder.GetValueOrDefault(), x.State.ChapterOrder.GetValueOrDefault(), x.State.SceneOrder.GetValueOrDefault())))
|
||||
.Select(x => $"{x.AssetName} / state / {x.Label}")
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
warnings.Add($"Unresolved asset state scene reference: {reference}");
|
||||
}
|
||||
|
||||
foreach (var reference in package.StoryAssets.SelectMany(asset => asset.Events.Select((assetEvent, index) => new
|
||||
{
|
||||
AssetName = DisplayName(asset.DisplayName, asset.Name),
|
||||
Label = string.IsNullOrWhiteSpace(assetEvent.Title) ? $"Event {index + 1}" : assetEvent.Title.Trim(),
|
||||
Event = assetEvent
|
||||
}))
|
||||
.Where(x => !x.Event.BookOrder.HasValue || !x.Event.ChapterOrder.HasValue || !x.Event.SceneOrder.HasValue || !sceneKeys.Contains(SceneKey(x.Event.BookOrder.GetValueOrDefault(), x.Event.ChapterOrder.GetValueOrDefault(), x.Event.SceneOrder.GetValueOrDefault())))
|
||||
.Select(x => $"{x.AssetName} / event / {x.Label}")
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
warnings.Add($"Unresolved asset event scene reference: {reference}");
|
||||
}
|
||||
|
||||
return warnings;
|
||||
}
|
||||
|
||||
@ -656,6 +863,62 @@ public sealed class ImportRepository(ISqlConnectionFactory connectionFactory) :
|
||||
: lookupIds.ThreadEventTypeIds.Values.First();
|
||||
}
|
||||
|
||||
private static int ResolveAssetKindId(ImportLookupIds lookupIds, string? type)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(type) && lookupIds.AssetKindIds.TryGetValue(type.Trim(), out var id))
|
||||
{
|
||||
return id;
|
||||
}
|
||||
|
||||
return lookupIds.AssetKindIds.TryGetValue("Object", out var objectId)
|
||||
? objectId
|
||||
: lookupIds.AssetKindIds.Values.First();
|
||||
}
|
||||
|
||||
private static int? ResolveAssetStateId(ImportLookupIds lookupIds, string? state)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(state) && lookupIds.AssetStateIds.TryGetValue(state.Trim(), out var id))
|
||||
{
|
||||
return id;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static int ResolveAssetEventTypeId(ImportLookupIds lookupIds, string? eventType)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(eventType) && lookupIds.AssetEventTypeIds.TryGetValue(eventType.Trim(), out var id))
|
||||
{
|
||||
return id;
|
||||
}
|
||||
|
||||
return lookupIds.AssetEventTypeIds.TryGetValue("Mentioned", out var mentionedId)
|
||||
? mentionedId
|
||||
: lookupIds.AssetEventTypeIds.Values.First();
|
||||
}
|
||||
|
||||
private static int ResolveAssetCustodyEventTypeId(ImportLookupIds lookupIds, string? eventType)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(eventType) && lookupIds.AssetCustodyEventTypeIds.TryGetValue(eventType.Trim(), out var id))
|
||||
{
|
||||
return id;
|
||||
}
|
||||
|
||||
return lookupIds.AssetCustodyEventTypesFallback();
|
||||
}
|
||||
|
||||
private static int ResolveCustodyRoleId(ImportLookupIds lookupIds, string? role)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(role) && lookupIds.CustodyRoleIds.TryGetValue(role.Trim(), out var id))
|
||||
{
|
||||
return id;
|
||||
}
|
||||
|
||||
return lookupIds.CustodyRoleIds.TryGetValue("Holder", out var holderId)
|
||||
? holderId
|
||||
: lookupIds.CustodyRoleIds.Values.First();
|
||||
}
|
||||
|
||||
private static int ClampImportance(int? importance)
|
||||
=> Math.Clamp(importance ?? 5, 1, 10);
|
||||
|
||||
@ -670,6 +933,28 @@ public sealed class ImportRepository(ISqlConnectionFactory connectionFactory) :
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool TryResolveAssetStateScene(IReadOnlyDictionary<string, int> sceneMap, ImportAssetStateDto state, out int sceneId)
|
||||
{
|
||||
if (state.BookOrder.HasValue && state.ChapterOrder.HasValue && state.SceneOrder.HasValue)
|
||||
{
|
||||
return sceneMap.TryGetValue(SceneKey(state.BookOrder.Value, state.ChapterOrder.Value, state.SceneOrder.Value), out sceneId);
|
||||
}
|
||||
|
||||
sceneId = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool TryResolveAssetEventScene(IReadOnlyDictionary<string, int> sceneMap, ImportAssetEventDto assetEvent, out int sceneId)
|
||||
{
|
||||
if (assetEvent.BookOrder.HasValue && assetEvent.ChapterOrder.HasValue && assetEvent.SceneOrder.HasValue)
|
||||
{
|
||||
return sceneMap.TryGetValue(SceneKey(assetEvent.BookOrder.Value, assetEvent.ChapterOrder.Value, assetEvent.SceneOrder.Value), out sceneId);
|
||||
}
|
||||
|
||||
sceneId = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
private static string SceneKey(int bookOrder, int chapterOrder, int sceneOrder)
|
||||
=> $"{bookOrder}/{chapterOrder}/{sceneOrder}";
|
||||
|
||||
@ -724,7 +1009,18 @@ public sealed class ImportRepository(ISqlConnectionFactory connectionFactory) :
|
||||
IReadOnlyDictionary<string, int> PlotLineTypeIds,
|
||||
IReadOnlyDictionary<string, int> ThreadTypeIds,
|
||||
IReadOnlyDictionary<string, int> ThreadStatusIds,
|
||||
IReadOnlyDictionary<string, int> ThreadEventTypeIds);
|
||||
IReadOnlyDictionary<string, int> ThreadEventTypeIds,
|
||||
IReadOnlyDictionary<string, int> AssetKindIds,
|
||||
IReadOnlyDictionary<string, int> AssetStateIds,
|
||||
IReadOnlyDictionary<string, int> AssetEventTypeIds,
|
||||
IReadOnlyDictionary<string, int> AssetCustodyEventTypeIds,
|
||||
IReadOnlyDictionary<string, int> CustodyRoleIds)
|
||||
{
|
||||
public int AssetCustodyEventTypesFallback()
|
||||
=> AssetCustodyEventTypeIds.TryGetValue("Has", out var hasId)
|
||||
? hasId
|
||||
: AssetCustodyEventTypeIds.Values.First();
|
||||
}
|
||||
|
||||
private sealed record ImportAppearance(int CharacterId, string? RoleInScene, int? Importance, string? Notes);
|
||||
|
||||
|
||||
@ -196,8 +196,10 @@ public sealed class ImportPreview
|
||||
public int StoryAssetCount => Package?.StoryAssets.Count ?? 0;
|
||||
public int AssetStateCount => Package?.StoryAssets.Sum(asset => asset.States.Count) ?? 0;
|
||||
public int AssetEventCount => Package?.StoryAssets.Sum(asset => asset.Events.Count) ?? 0;
|
||||
public int AssetCustodyCount => Package?.StoryAssets.Count(asset => !string.IsNullOrWhiteSpace(asset.InitialOwnerCharacterName)) ?? 0;
|
||||
public int ResolvedAssetStateCount => Math.Max(0, AssetStateCount - UnresolvedAssetReferences.Count(x => x.Contains("state", StringComparison.OrdinalIgnoreCase)));
|
||||
public int ResolvedAssetEventCount => Math.Max(0, AssetEventCount - UnresolvedAssetReferences.Count(x => x.Contains("event", StringComparison.OrdinalIgnoreCase)));
|
||||
public int ResolvedAssetCustodyCount => Math.Max(0, AssetCustodyCount - Package?.StoryAssets.Count(asset => !string.IsNullOrWhiteSpace(asset.InitialOwnerCharacterName) && UnmatchedAssetOwnerNames.Contains(asset.InitialOwnerCharacterName.Trim(), StringComparer.OrdinalIgnoreCase)) ?? 0);
|
||||
public int ChapterCount => Package?.Books.Sum(book => book.Chapters.Count) ?? 0;
|
||||
public int SceneCount => Package?.Books.Sum(book => book.Chapters.Sum(chapter => chapter.Scenes.Count)) ?? 0;
|
||||
public int SceneCharacterAppearanceCount => Package?.Books.Sum(book => book.Chapters.Sum(chapter => chapter.Scenes.Sum(scene => scene.SceneCharacters.Count + AutoPovAppearanceCount(scene)))) ?? 0;
|
||||
|
||||
@ -108,6 +108,10 @@ public sealed class ImportService(IImportRepository imports, ILogger<ImportServi
|
||||
builder.AppendLine($"Plot lines: {preview.PlotLineCount}");
|
||||
builder.AppendLine($"Plot threads: {preview.PlotThreadCount}");
|
||||
builder.AppendLine($"Thread events: {preview.ThreadEventCount}");
|
||||
builder.AppendLine($"Story assets: {preview.StoryAssetCount}");
|
||||
builder.AppendLine($"Asset states: {preview.AssetStateCount}");
|
||||
builder.AppendLine($"Asset events: {preview.AssetEventCount}");
|
||||
builder.AppendLine($"Asset custody links: {preview.AssetCustodyCount}");
|
||||
builder.AppendLine($"Books: {preview.BookCount}");
|
||||
builder.AppendLine($"Chapters: {preview.ChapterCount}");
|
||||
builder.AppendLine($"Scenes: {preview.SceneCount}");
|
||||
@ -119,6 +123,9 @@ public sealed class ImportService(IImportRepository imports, ILogger<ImportServi
|
||||
AppendReportSection(builder, "Unmatched location names", preview.UnmatchedLocationNames);
|
||||
AppendReportSection(builder, "Unmatched scene character names", preview.UnmatchedSceneCharacterNames);
|
||||
AppendReportSection(builder, "Unresolved thread event references", preview.UnresolvedThreadEventReferences);
|
||||
AppendReportSection(builder, "Unresolved asset references", preview.UnresolvedAssetReferences);
|
||||
AppendReportSection(builder, "Unmatched asset owners", preview.UnmatchedAssetOwnerNames);
|
||||
AppendReportSection(builder, "Unmatched asset locations", preview.UnmatchedAssetLocationNames);
|
||||
return builder.ToString();
|
||||
}
|
||||
|
||||
@ -174,7 +181,10 @@ public sealed class ImportService(IImportRepository imports, ILogger<ImportServi
|
||||
UnmatchedPovNames = FindUnmatchedPovNames(package),
|
||||
UnmatchedLocationNames = FindUnmatchedLocationNames(package),
|
||||
UnmatchedSceneCharacterNames = FindUnmatchedSceneCharacterNames(package),
|
||||
UnresolvedThreadEventReferences = FindUnresolvedThreadEventReferences(package)
|
||||
UnresolvedThreadEventReferences = FindUnresolvedThreadEventReferences(package),
|
||||
UnresolvedAssetReferences = FindUnresolvedAssetReferences(package),
|
||||
UnmatchedAssetOwnerNames = FindUnmatchedAssetOwnerNames(package),
|
||||
UnmatchedAssetLocationNames = FindUnmatchedAssetLocationNames(package)
|
||||
};
|
||||
}
|
||||
|
||||
@ -196,6 +206,7 @@ public sealed class ImportService(IImportRepository imports, ILogger<ImportServi
|
||||
ValidateCharacters(package, result);
|
||||
ValidateLocations(package, result);
|
||||
ValidatePlotLines(package, result);
|
||||
ValidateStoryAssets(package, result);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(package.PackageVersion))
|
||||
{
|
||||
@ -344,9 +355,115 @@ public sealed class ImportService(IImportRepository imports, ILogger<ImportServi
|
||||
result.Warnings.Add($"Thread event scene reference could not be resolved and will be skipped: {reference}");
|
||||
}
|
||||
|
||||
foreach (var reference in FindUnresolvedAssetReferences(package))
|
||||
{
|
||||
result.Warnings.Add($"Asset state/event scene reference could not be resolved and will be skipped: {reference}");
|
||||
}
|
||||
|
||||
foreach (var name in FindUnmatchedAssetOwnerNames(package))
|
||||
{
|
||||
result.Warnings.Add($"Story asset owner \"{name}\" does not match a top-level imported character.");
|
||||
}
|
||||
|
||||
foreach (var name in FindUnmatchedAssetLocationNames(package))
|
||||
{
|
||||
result.Warnings.Add($"Story asset location \"{name}\" does not match a top-level imported location.");
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static void ValidateStoryAssets(PlotLineImportPackage package, ImportValidationResult result)
|
||||
{
|
||||
foreach (var group in package.StoryAssets
|
||||
.Where(x => !string.IsNullOrWhiteSpace(x.Name))
|
||||
.GroupBy(x => x.Name.Trim(), StringComparer.OrdinalIgnoreCase)
|
||||
.Where(x => x.Count() > 1))
|
||||
{
|
||||
result.Errors.Add($"Story asset \"{group.Key}\" appears more than once in the import package.");
|
||||
}
|
||||
|
||||
for (var assetIndex = 0; assetIndex < package.StoryAssets.Count; assetIndex++)
|
||||
{
|
||||
var asset = package.StoryAssets[assetIndex];
|
||||
var assetLabel = string.IsNullOrWhiteSpace(asset.Name) ? $"Story asset {assetIndex + 1}" : asset.Name.Trim();
|
||||
if (string.IsNullOrWhiteSpace(asset.Name))
|
||||
{
|
||||
result.Errors.Add($"Story asset {assetIndex + 1} is missing a name.");
|
||||
}
|
||||
|
||||
if (DisplayName(asset.DisplayName, asset.Name).Length > TitleMaxLength)
|
||||
{
|
||||
result.Errors.Add($"{assetLabel} name/displayName is longer than {TitleMaxLength} characters.");
|
||||
}
|
||||
|
||||
ValidateLargeText(result, $"{assetLabel} description", asset.Description);
|
||||
ValidateLargeText(result, $"{assetLabel} notes", asset.Notes);
|
||||
|
||||
foreach (var duplicate in asset.States
|
||||
.Where(HasSceneOrderReference)
|
||||
.GroupBy(AssetStateSceneKey)
|
||||
.Where(x => x.Count() > 1))
|
||||
{
|
||||
result.Errors.Add($"{assetLabel} has duplicate states for scene {duplicate.Key}.");
|
||||
}
|
||||
|
||||
foreach (var duplicate in asset.Events
|
||||
.Where(HasSceneOrderReference)
|
||||
.GroupBy(AssetEventSceneKey)
|
||||
.Where(x => x.Count() > 1))
|
||||
{
|
||||
result.Errors.Add($"{assetLabel} has duplicate events for scene {duplicate.Key}.");
|
||||
}
|
||||
|
||||
for (var stateIndex = 0; stateIndex < asset.States.Count; stateIndex++)
|
||||
{
|
||||
var state = asset.States[stateIndex];
|
||||
var stateLabel = string.IsNullOrWhiteSpace(state.StateName) ? $"State {stateIndex + 1}" : state.StateName.Trim();
|
||||
if (string.IsNullOrWhiteSpace(state.StateName) && string.IsNullOrWhiteSpace(state.Description))
|
||||
{
|
||||
result.Errors.Add($"{assetLabel} state {stateIndex + 1} needs stateName or description.");
|
||||
}
|
||||
|
||||
if (!HasSceneOrderReference(state) && string.IsNullOrWhiteSpace(state.SceneRef))
|
||||
{
|
||||
result.Warnings.Add($"{assetLabel} / {stateLabel} is missing bookOrder/chapterOrder/sceneOrder.");
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(state.StateName) && state.StateName.Trim().Length > 100)
|
||||
{
|
||||
result.Warnings.Add($"{assetLabel} / {stateLabel} stateName is longer than 100 characters and may not match an existing asset state.");
|
||||
}
|
||||
|
||||
ValidateLargeText(result, $"{assetLabel} / {stateLabel} description", state.Description);
|
||||
ValidateLargeText(result, $"{assetLabel} / {stateLabel} notes", state.Notes);
|
||||
}
|
||||
|
||||
for (var eventIndex = 0; eventIndex < asset.Events.Count; eventIndex++)
|
||||
{
|
||||
var assetEvent = asset.Events[eventIndex];
|
||||
var eventLabel = string.IsNullOrWhiteSpace(assetEvent.Title) ? $"Event {eventIndex + 1}" : assetEvent.Title.Trim();
|
||||
if (string.IsNullOrWhiteSpace(assetEvent.Title) && string.IsNullOrWhiteSpace(assetEvent.Summary))
|
||||
{
|
||||
result.Errors.Add($"{assetLabel} event {eventIndex + 1} needs a title or summary.");
|
||||
}
|
||||
|
||||
if (!HasSceneOrderReference(assetEvent) && string.IsNullOrWhiteSpace(assetEvent.SceneRef))
|
||||
{
|
||||
result.Warnings.Add($"{assetLabel} / {eventLabel} is missing bookOrder/chapterOrder/sceneOrder.");
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(assetEvent.Title) && assetEvent.Title.Trim().Length > TitleMaxLength)
|
||||
{
|
||||
result.Errors.Add($"{assetLabel} / {eventLabel} title is longer than {TitleMaxLength} characters.");
|
||||
}
|
||||
|
||||
ValidateLargeText(result, $"{assetLabel} / {eventLabel} summary", assetEvent.Summary);
|
||||
ValidateLargeText(result, $"{assetLabel} / {eventLabel} notes", assetEvent.Notes);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void ValidatePlotLines(PlotLineImportPackage package, ImportValidationResult result)
|
||||
{
|
||||
foreach (var group in package.PlotLines
|
||||
@ -602,12 +719,81 @@ public sealed class ImportService(IImportRepository imports, ILogger<ImportServi
|
||||
.ToList();
|
||||
}
|
||||
|
||||
private static IReadOnlyList<string> FindUnresolvedAssetReferences(PlotLineImportPackage package)
|
||||
{
|
||||
var sceneKeys = package.Books
|
||||
.SelectMany(book => book.Chapters.SelectMany(chapter => chapter.Scenes.Select(scene => $"{book.SeriesOrder}/{chapter.Order}/{scene.Order}")))
|
||||
.ToHashSet(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
var stateReferences = package.StoryAssets
|
||||
.SelectMany(asset => asset.States.Select((state, index) => new
|
||||
{
|
||||
AssetName = DisplayName(asset.DisplayName, asset.Name),
|
||||
Label = string.IsNullOrWhiteSpace(state.StateName) ? $"State {index + 1}" : state.StateName.Trim(),
|
||||
State = state
|
||||
}))
|
||||
.Where(x => !HasSceneOrderReference(x.State) || !sceneKeys.Contains(AssetStateSceneKey(x.State)))
|
||||
.Select(x => $"{x.AssetName} / state / {x.Label} -> {AssetStateSceneKey(x.State)}");
|
||||
|
||||
var eventReferences = package.StoryAssets
|
||||
.SelectMany(asset => asset.Events.Select((assetEvent, index) => new
|
||||
{
|
||||
AssetName = DisplayName(asset.DisplayName, asset.Name),
|
||||
Label = string.IsNullOrWhiteSpace(assetEvent.Title) ? $"Event {index + 1}" : assetEvent.Title.Trim(),
|
||||
Event = assetEvent
|
||||
}))
|
||||
.Where(x => !HasSceneOrderReference(x.Event) || !sceneKeys.Contains(AssetEventSceneKey(x.Event)))
|
||||
.Select(x => $"{x.AssetName} / event / {x.Label} -> {AssetEventSceneKey(x.Event)}");
|
||||
|
||||
return stateReferences
|
||||
.Concat(eventReferences)
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.OrderBy(x => x)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
private static IReadOnlyList<string> FindUnmatchedAssetOwnerNames(PlotLineImportPackage package)
|
||||
{
|
||||
var characterNames = CharacterNames(package);
|
||||
return package.StoryAssets
|
||||
.Select(asset => asset.InitialOwnerCharacterName?.Trim())
|
||||
.Concat(package.StoryAssets.SelectMany(asset => asset.Events.Select(assetEvent => assetEvent.CharacterName?.Trim())))
|
||||
.Where(name => !string.IsNullOrWhiteSpace(name) && !characterNames.Contains(name))
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.OrderBy(name => name)
|
||||
.ToList()!;
|
||||
}
|
||||
|
||||
private static IReadOnlyList<string> FindUnmatchedAssetLocationNames(PlotLineImportPackage package)
|
||||
{
|
||||
var locationNames = LocationNames(package);
|
||||
return package.StoryAssets
|
||||
.Select(asset => asset.InitialLocationName?.Trim())
|
||||
.Concat(package.StoryAssets.SelectMany(asset => asset.Events.Select(assetEvent => assetEvent.LocationName?.Trim())))
|
||||
.Where(name => !string.IsNullOrWhiteSpace(name) && !locationNames.Contains(name))
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.OrderBy(name => name)
|
||||
.ToList()!;
|
||||
}
|
||||
|
||||
private static bool HasSceneOrderReference(ImportThreadEventDto threadEvent)
|
||||
=> threadEvent.BookOrder.HasValue && threadEvent.ChapterOrder.HasValue && threadEvent.SceneOrder.HasValue;
|
||||
|
||||
private static string EventSceneKey(ImportThreadEventDto threadEvent)
|
||||
=> $"{threadEvent.BookOrder?.ToString() ?? "?"}/{threadEvent.ChapterOrder?.ToString() ?? "?"}/{threadEvent.SceneOrder?.ToString() ?? "?"}";
|
||||
|
||||
private static bool HasSceneOrderReference(ImportAssetStateDto state)
|
||||
=> state.BookOrder.HasValue && state.ChapterOrder.HasValue && state.SceneOrder.HasValue;
|
||||
|
||||
private static bool HasSceneOrderReference(ImportAssetEventDto assetEvent)
|
||||
=> assetEvent.BookOrder.HasValue && assetEvent.ChapterOrder.HasValue && assetEvent.SceneOrder.HasValue;
|
||||
|
||||
private static string AssetStateSceneKey(ImportAssetStateDto state)
|
||||
=> $"{state.BookOrder?.ToString() ?? "?"}/{state.ChapterOrder?.ToString() ?? "?"}/{state.SceneOrder?.ToString() ?? "?"}";
|
||||
|
||||
private static string AssetEventSceneKey(ImportAssetEventDto assetEvent)
|
||||
=> $"{assetEvent.BookOrder?.ToString() ?? "?"}/{assetEvent.ChapterOrder?.ToString() ?? "?"}/{assetEvent.SceneOrder?.ToString() ?? "?"}";
|
||||
|
||||
private static HashSet<string> CharacterNames(PlotLineImportPackage package)
|
||||
=> package.Characters
|
||||
.Where(x => !string.IsNullOrWhiteSpace(x.Name))
|
||||
|
||||
@ -21,7 +21,7 @@ public sealed class ImportIndexViewModel
|
||||
public static string SampleJson => """
|
||||
{
|
||||
"packageVersion": "1.0",
|
||||
"source": "JSON Import Phase 1D sample",
|
||||
"source": "JSON Import Phase 1E sample",
|
||||
"project": {
|
||||
"title": "The Glass Meridian",
|
||||
"description": "A placeholder two-book fantasy outline.",
|
||||
@ -96,6 +96,58 @@ public sealed class ImportIndexViewModel
|
||||
]
|
||||
}
|
||||
],
|
||||
"storyAssets": [
|
||||
{
|
||||
"name": "Brass key",
|
||||
"displayName": "The Brass Key",
|
||||
"type": "Object",
|
||||
"importance": 9,
|
||||
"description": "A warm brass key with a notch pattern that does not match city locks.",
|
||||
"notes": "Tracks the mystery through book one.",
|
||||
"initialOwnerCharacterName": "Mara",
|
||||
"initialLocationName": "Old Arcade",
|
||||
"states": [
|
||||
{ "bookOrder": 1, "chapterOrder": 1, "sceneOrder": 1, "stateName": "Known", "description": "Mara finds the key in the old arcade.", "notes": "First asset state." },
|
||||
{ "bookOrder": 1, "chapterOrder": 2, "sceneOrder": 1, "stateName": "Changed", "description": "The key reacts to the lantern court map.", "notes": "Shows the key is more than a door tool." }
|
||||
],
|
||||
"events": [
|
||||
{ "bookOrder": 1, "chapterOrder": 1, "sceneOrder": 1, "eventType": "Introduced", "title": "Key found", "summary": "Mara pockets the key after the impossible doorway appears.", "characterName": "Mara", "locationName": "Old Arcade", "notes": "Creates initial custody." },
|
||||
{ "bookOrder": 1, "chapterOrder": 2, "sceneOrder": 1, "eventType": "Used", "title": "Key warms near the map", "summary": "The brass key warms in Mara's hand near the incomplete map.", "characterName": "Mara", "locationName": "Lantern Court", "notes": "Moves the asset to the hidden city." }
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Signal plate",
|
||||
"displayName": "The Signal Plate",
|
||||
"type": "Clue",
|
||||
"importance": 8,
|
||||
"description": "A damaged brass plate that carries a warning from the coast.",
|
||||
"initialOwnerCharacterName": "Tovin",
|
||||
"initialLocationName": "Clockwork Shore",
|
||||
"states": [
|
||||
{ "bookOrder": 2, "chapterOrder": 1, "sceneOrder": 2, "stateName": "Damaged", "description": "The plate is cracked by salt and failed gears." },
|
||||
{ "bookOrder": 2, "chapterOrder": 2, "sceneOrder": 1, "stateName": "Known", "description": "Mara translates the warning." }
|
||||
],
|
||||
"events": [
|
||||
{ "bookOrder": 2, "chapterOrder": 1, "sceneOrder": 2, "eventType": "Discovered", "title": "Plate recovered", "summary": "Tovin pulls the plate free from the broken machine.", "characterName": "Tovin", "locationName": "Clockwork Shore" },
|
||||
{ "bookOrder": 2, "chapterOrder": 2, "sceneOrder": 1, "eventType": "Revealed", "title": "Warning translated", "summary": "The plate names the missing captain and points toward Sera's route.", "characterName": "Mara", "locationName": "Clockwork Shore" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Tide chart",
|
||||
"displayName": "Ilen's Tide Chart",
|
||||
"type": "Document",
|
||||
"importance": 6,
|
||||
"description": "A folded chart showing when the causeway opens.",
|
||||
"initialOwnerCharacterName": "Ilen",
|
||||
"initialLocationName": "Tidal causeway",
|
||||
"states": [
|
||||
{ "bookOrder": 2, "chapterOrder": 2, "sceneOrder": 2, "stateName": "Known", "description": "Ilen confirms the safe crossing window." }
|
||||
],
|
||||
"events": [
|
||||
{ "bookOrder": 2, "chapterOrder": 2, "sceneOrder": 2, "eventType": "Used", "title": "Crossing window found", "summary": "Ilen uses the chart to time the causeway crossing.", "characterName": "Ilen", "locationName": "Tidal causeway" }
|
||||
]
|
||||
}
|
||||
],
|
||||
"books": [
|
||||
{
|
||||
"title": "Book One",
|
||||
|
||||
@ -8,7 +8,7 @@
|
||||
<div>
|
||||
<p class="eyebrow">Tools</p>
|
||||
<h1>JSON Import</h1>
|
||||
<p class="lead-text">Paste or upload a Phase 1D package, preview it, confirm the summary, then import a new project.</p>
|
||||
<p class="lead-text">Paste or upload a Phase 1E package, preview it, confirm the summary, then import a new project.</p>
|
||||
</div>
|
||||
<div class="button-row">
|
||||
<a class="btn btn-outline-primary" asp-action="Sample">Download sample JSON</a>
|
||||
@ -65,11 +65,11 @@
|
||||
</div>
|
||||
<div>
|
||||
<h3>Optional</h3>
|
||||
<p><code>packageVersion</code>, <code>source</code>, top-level <code>characters</code>, top-level <code>locations</code>, top-level <code>plotLines</code>, plot line <code>threads</code>, thread <code>events</code>, POV/location mapping, date/time text, purpose/outcome text, and <code>sceneCharacters</code>.</p>
|
||||
<p><code>packageVersion</code>, <code>source</code>, top-level <code>characters</code>, top-level <code>locations</code>, top-level <code>plotLines</code>, top-level <code>storyAssets</code>, plot line <code>threads</code>, thread <code>events</code>, asset <code>states</code>, asset <code>events</code>, initial asset owner/location mapping, POV/location mapping, date/time text, purpose/outcome text, and <code>sceneCharacters</code>.</p>
|
||||
</div>
|
||||
<div>
|
||||
<h3>Not imported yet</h3>
|
||||
<p>No AI extraction, Word/text parsing, story assets, asset states/custody, dependencies, relationship graphs, merge/update, or export logic. Thread events link to scenes with <code>bookOrder</code>, <code>chapterOrder</code>, and <code>sceneOrder</code>; <code>sceneRef</code> is accepted as future-friendly text only.</p>
|
||||
<p>No AI extraction, Word/text parsing, manuscript parsing, scene dependency import, relationship graphs, merge/update, or export logic. Thread events and asset states/events link to scenes with <code>bookOrder</code>, <code>chapterOrder</code>, and <code>sceneOrder</code>; <code>sceneRef</code> is accepted as future-friendly text only.</p>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
@ -93,6 +93,10 @@
|
||||
<span><strong>@preview.PlotLineCount</strong> plot lines</span>
|
||||
<span><strong>@preview.PlotThreadCount</strong> threads</span>
|
||||
<span><strong>@preview.ResolvedThreadEventCount</strong> events</span>
|
||||
<span><strong>@preview.StoryAssetCount</strong> assets</span>
|
||||
<span><strong>@preview.ResolvedAssetStateCount</strong> asset states</span>
|
||||
<span><strong>@preview.ResolvedAssetEventCount</strong> asset events</span>
|
||||
<span><strong>@preview.ResolvedAssetCustodyCount</strong> custody links</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -131,7 +135,7 @@
|
||||
|
||||
@if (preview.Package is not null)
|
||||
{
|
||||
@if (preview.UnmatchedPovNames.Any() || preview.UnmatchedLocationNames.Any() || preview.UnmatchedSceneCharacterNames.Any() || preview.UnresolvedThreadEventReferences.Any())
|
||||
@if (preview.UnmatchedPovNames.Any() || preview.UnmatchedLocationNames.Any() || preview.UnmatchedSceneCharacterNames.Any() || preview.UnresolvedThreadEventReferences.Any() || preview.UnresolvedAssetReferences.Any() || preview.UnmatchedAssetOwnerNames.Any() || preview.UnmatchedAssetLocationNames.Any())
|
||||
{
|
||||
<div class="import-mapping-summary">
|
||||
@if (preview.UnmatchedPovNames.Any())
|
||||
@ -150,6 +154,18 @@
|
||||
{
|
||||
<span>Unresolved thread events: @string.Join(", ", preview.UnresolvedThreadEventReferences)</span>
|
||||
}
|
||||
@if (preview.UnresolvedAssetReferences.Any())
|
||||
{
|
||||
<span>Unresolved asset references: @string.Join(", ", preview.UnresolvedAssetReferences)</span>
|
||||
}
|
||||
@if (preview.UnmatchedAssetOwnerNames.Any())
|
||||
{
|
||||
<span>Unmatched asset owners/events: @string.Join(", ", preview.UnmatchedAssetOwnerNames)</span>
|
||||
}
|
||||
@if (preview.UnmatchedAssetLocationNames.Any())
|
||||
{
|
||||
<span>Unmatched asset locations: @string.Join(", ", preview.UnmatchedAssetLocationNames)</span>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
@ -213,9 +229,27 @@
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (preview.Package.StoryAssets.Any())
|
||||
{
|
||||
<div class="import-tree mt-3">
|
||||
@foreach (var asset in preview.Package.StoryAssets)
|
||||
{
|
||||
<details class="import-tree-book">
|
||||
<summary>
|
||||
<strong>Asset: @(string.IsNullOrWhiteSpace(asset.DisplayName) ? asset.Name : asset.DisplayName)</strong>
|
||||
<span>@asset.States.Count states / @asset.Events.Count events</span>
|
||||
</summary>
|
||||
<ul>
|
||||
<li>Type: @(asset.Type ?? "Not set") / Initial owner: @(asset.InitialOwnerCharacterName ?? "Not set") / Initial location: @(asset.InitialLocationName ?? "Not set")</li>
|
||||
</ul>
|
||||
</details>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
<section class="import-confirmation mt-3">
|
||||
<h3>Confirm import</h3>
|
||||
<p>This will create <strong>@preview.Package.Project.Title</strong> with <strong>@preview.BookCount</strong> books, <strong>@preview.ChapterCount</strong> chapters, <strong>@preview.SceneCount</strong> scenes, <strong>@preview.CharacterCount</strong> characters, <strong>@preview.LocationCount</strong> locations, <strong>@preview.SceneCharacterAppearanceCount</strong> scene character appearances, <strong>@preview.PlotLineCount</strong> plot lines, <strong>@preview.PlotThreadCount</strong> plot threads, and <strong>@preview.ResolvedThreadEventCount</strong> resolved thread events.</p>
|
||||
<p>This will create <strong>@preview.Package.Project.Title</strong> with <strong>@preview.BookCount</strong> books, <strong>@preview.ChapterCount</strong> chapters, <strong>@preview.SceneCount</strong> scenes, <strong>@preview.CharacterCount</strong> characters, <strong>@preview.LocationCount</strong> locations, <strong>@preview.SceneCharacterAppearanceCount</strong> scene character appearances, <strong>@preview.PlotLineCount</strong> plot lines, <strong>@preview.PlotThreadCount</strong> plot threads, <strong>@preview.ResolvedThreadEventCount</strong> resolved thread events, <strong>@preview.StoryAssetCount</strong> story assets, <strong>@preview.ResolvedAssetStateCount</strong> resolved asset states, <strong>@preview.ResolvedAssetEventCount</strong> resolved asset events, and <strong>@preview.ResolvedAssetCustodyCount</strong> custody links.</p>
|
||||
@if (preview.HasDuplicateProjectTitle)
|
||||
{
|
||||
<p class="muted">Because this title already exists, the created project will be named <strong>@preview.DuplicateImportProjectTitle</strong> if you continue.</p>
|
||||
|
||||
@ -26,6 +26,10 @@
|
||||
<span><strong>@Model.PlotLinesCreated</strong> plot lines created</span>
|
||||
<span><strong>@Model.PlotThreadsCreated</strong> plot threads created</span>
|
||||
<span><strong>@Model.ThreadEventsCreated</strong> thread events created</span>
|
||||
<span><strong>@Model.StoryAssetsCreated</strong> story assets created</span>
|
||||
<span><strong>@Model.AssetStatesCreated</strong> asset states created</span>
|
||||
<span><strong>@Model.AssetEventsCreated</strong> asset events created</span>
|
||||
<span><strong>@Model.AssetCustodyEventsCreated</strong> custody records created</span>
|
||||
</div>
|
||||
|
||||
@if (Model.SkippedOptionalFields.Any())
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user