Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions deploy/portainer-stack.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
services:
beacon-server:
image: beacon-server:0.1.11
image: beacon-server:0.1.12
restart: always
environment:
ASPNETCORE_ENVIRONMENT: Production
Expand Down Expand Up @@ -59,7 +59,7 @@ services:
/bin/sh -c '(while sleep 86400; do nginx -s reload; done) & exec nginx -g "daemon off;"'

beacon-backup:
image: beacon-backup:0.1.11
image: beacon-backup:0.1.12
restart: always
environment:
BACKUP_INTERVAL_SECONDS: "86400"
Expand Down
2 changes: 1 addition & 1 deletion src/Beacon.Plugin/Beacon.Plugin.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<!-- Drives the built DLL, the generated manifest, and the folder Dalamud loads it from. -->
<AssemblyName>Beacon</AssemblyName>
<RootNamespace>Beacon</RootNamespace>
<Version>0.1.11.0</Version>
<Version>0.1.12.0</Version>
<!-- The Dalamud SDK does not turn these on, and every service file wants System.Threading.Tasks. -->
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
Expand Down
8 changes: 7 additions & 1 deletion src/Beacon.Plugin/Plugin.cs
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ public sealed class Plugin : IDalamudPlugin

private readonly ProfileEditorWindow profileEditorWindow;

private readonly ImageViewerWindow imageViewerWindow;

public Plugin(IDalamudPluginInterface pluginInterface)
{
Svc.Initialise(pluginInterface);
Expand All @@ -75,6 +77,8 @@ public Plugin(IDalamudPluginInterface pluginInterface)
stagehand = new StagehandIpc();
stages = new StageDressing(config, api, stagehand, location);

imageViewerWindow = new ImageViewerWindow(images);

editorWindow = new BeaconEditorWindow(config, atlas, location, screenshots, images, notifications, stages);
settingsWindow = new SettingsWindow(config, api, atlas, hub, travel, stages);
atlasWindow = new AtlasWindow(
Expand All @@ -90,7 +94,7 @@ public Plugin(IDalamudPluginInterface pluginInterface)
OpenChronicle,
OpenCharacterCard);

profileEditorWindow = new ProfileEditorWindow(config, profiles, atlas, location, screenshots, images, notifications);
profileEditorWindow = new ProfileEditorWindow(config, profiles, atlas, location, screenshots, images, notifications, imageViewerWindow);
chronicleWindow = new ChronicleWindow(
config,
profiles,
Expand All @@ -99,6 +103,7 @@ public Plugin(IDalamudPluginInterface pluginInterface)
location,
images,
notifications,
imageViewerWindow,
profileEditorWindow.Open);

overlay = new BeaconOverlay(config, atlas, travel, location, () => atlasWindow.IsOpen);
Expand All @@ -108,6 +113,7 @@ public Plugin(IDalamudPluginInterface pluginInterface)
windows.AddWindow(settingsWindow);
windows.AddWindow(chronicleWindow);
windows.AddWindow(profileEditorWindow);
windows.AddWindow(imageViewerWindow);
windows.AddWindow(overlay);

// Restore the filters the player left the atlas on, so it opens where they left off.
Expand Down
153 changes: 71 additions & 82 deletions src/Beacon.Plugin/UI/ProfileCard.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,38 +23,59 @@ public sealed class ProfileCard(ImageCache images)
/// <summary>Set while the reader has asked for the long prose.</summary>
private bool showLongProse;

/// <summary>The gallery the reader has opened, if any.</summary>
public Guid? ViewingGallery { get; set; }
/// <summary>
/// Called when the reader clicks a likeness, with the image to show full size.
///
/// A callback rather than state of its own: the card draws inside a child window, and anything
/// opened from in there has to be drawn by whoever owns the window, not by the card.
/// </summary>
public Action<Guid>? OpenImage { get; set; }

/// <summary>Called when the reader asks for the whole gallery.</summary>
public Action<ProfileDto>? OpenGallery { get; set; }

/// <summary>
/// A portrait chosen but not yet uploaded, so the editor's preview shows the picture being
/// considered rather than the one it is about to replace.
/// </summary>
public IDalamudTextureWrap? PendingPortrait { get; set; }

/// <summary>The card most recently drawn, so the gallery popup has something to show.</summary>
private ProfileDto? shown;

/// <summary>Draws the card face.</summary>
public void Draw(ProfileDto profile, float scale)
public void Draw(ProfileDto profile, float scale) => DrawCardContents(profile, scale);

/// <summary>Indents the cursor so something of this width sits in the middle of the card.</summary>
private static void Centre(float itemWidth, float available)
{
shown = profile;
DrawCardContents(profile, scale);
var indent = (available - itemWidth) / 2f;
if (indent > 0f)
ImGui.SetCursorPosX(ImGui.GetCursorPosX() + indent);
}

private static void CentreText(string text, float available) => Centre(ImGui.CalcTextSize(text).X, available);

private void DrawCardContents(ProfileDto profile, float scale)
{
var portraitSize = 104f * scale;
var width = ImGui.GetContentRegionAvail().X;

// The likeness leads, centred, at a size worth looking at. Everything else sits beneath it and
// scrolls -- a face is what somebody decides on, and it was previously a thumbnail beside a
// column of text with half the card left empty.
var portraitSize = Math.Clamp(width * 0.52f, 220f * scale, 460f * scale);

Centre(portraitSize, width);
DrawPortrait(profile, portraitSize, scale);

ImGui.SameLine(0, 14f * scale);
ImGui.BeginGroup();
ImGui.Spacing();

CentreText(profile.Identity.Name, width);
Ornament.Text(Theme.Ink, profile.Identity.Name);

if (!string.IsNullOrWhiteSpace(profile.Identity.Title))
Ornament.Text(Theme.Wax, $"“{profile.Identity.Title}”");
{
var title = $"“{profile.Identity.Title}”";
CentreText(title, width);
Ornament.Text(Theme.Wax, title);
}

var details = new List<string>();
if (!string.IsNullOrWhiteSpace(profile.Identity.Lineage))
Expand All @@ -64,11 +85,23 @@ private void DrawCardContents(ProfileDto profile, float scale)
details.Add($"Age {age}");

if (details.Count > 0)
Ornament.Text(Theme.InkSoft, string.Join(" · ", details));
{
var line = string.Join(" · ", details);
CentreText(line, width);
Ornament.Text(Theme.InkSoft, line);
}

if (profile.Identity.Archetype.Count > 0)
{
ImGui.Spacing();

// Measure the row of chips so it can be centred as a block rather than left-hung.
var chipsWidth = 0f;
foreach (var word in profile.Identity.Archetype)
chipsWidth += ImGui.CalcTextSize(word).X + (18f * scale);

Centre(chipsWidth, width);

foreach (var word in profile.Identity.Archetype)
{
Ornament.Tag(word, Theme.ParchmentRule, Theme.Ink);
Expand All @@ -81,10 +114,14 @@ private void DrawCardContents(ProfileDto profile, float scale)
if (!string.IsNullOrWhiteSpace(profile.Identity.Quote))
{
ImGui.Spacing();
Ornament.TextWrapped(Theme.InkSoft, $"“{profile.Identity.Quote}”");
}
var quote = $"“{profile.Identity.Quote}”";

ImGui.EndGroup();
// Only centre a quote that fits on one line; a wrapped one reads better ranged left.
if (ImGui.CalcTextSize(quote).X < width)
CentreText(quote, width);

Ornament.TextWrapped(Theme.InkSoft, quote);
}

Ornament.FleuronDivider(Theme.ParchmentRule, 4f);

Expand Down Expand Up @@ -246,12 +283,28 @@ private void DrawPortrait(ProfileDto profile, float size, float scale)

draw.AddRect(origin, box, Theme.Brass.Packed(), 0f, ImDrawFlags.None, 1.4f * scale);

ImGui.Dummy(new Vector2(size, size * 1.25f));
// Clicking the likeness opens it at a size you can actually look at.
if (portrait is not null && profile.HasPortrait)
{
ImGui.InvisibleButton($"##portrait{profile.Id}", new Vector2(size, size * 1.25f));

if (ImGui.IsItemHovered())
{
ImGui.SetMouseCursor(ImGuiMouseCursor.Hand);
draw.AddRect(origin, box, Theme.Gold.Packed(), 0f, ImDrawFlags.None, 2f * scale);
Ornament.Tooltip("Click to see it full size.");
}

if (ImGui.IsItemClicked())
OpenImage?.Invoke(profile.PortraitImageId!.Value);
}
else
ImGui.Dummy(new Vector2(size, size * 1.25f));

if (profile.Gallery.Count > 1)
{
if (ImGui.SmallButton($"Gallery ({profile.Gallery.Count})##gal{profile.Id}"))
ViewingGallery = profile.Id;
OpenGallery?.Invoke(profile);
}

ImGui.EndGroup();
Expand Down Expand Up @@ -381,68 +434,4 @@ private void DrawLinks(ProfileDto profile)
ImGui.Spacing();
}

/// <summary>The gallery, shown over the card when the reader opens it.</summary>
public void DrawGalleryPopup()
{
if (ViewingGallery is not { } id)
return;

var profile = shown;
if (profile is null || profile.Id != id)
{
ViewingGallery = null;
return;
}

ImGui.OpenPopup("Gallery###BeaconGallery");

var open = true;
ImGui.SetNextWindowSizeConstraints(new Vector2(420, 300), new Vector2(1400, 1000));

if (!ImGui.BeginPopupModal("Gallery###BeaconGallery", ref open, ImGuiWindowFlags.None))
{
ViewingGallery = null;
return;
}

if (!open)
{
ViewingGallery = null;
ImGui.CloseCurrentPopup();
ImGui.EndPopup();
return;
}

var scale = ImGuiHelpers.GlobalScale;
var width = ImGui.GetContentRegionAvail().X;

foreach (var image in profile.Gallery)
{
Ornament.Text(Theme.BrassBright, ProfileLabels.Describe(image.Category));

if (images.Get(image.ImageId, thumb: false) is { } texture)
{
var drawWidth = Math.Min(width, 520f * scale);
var drawHeight = drawWidth * texture.Height / Math.Max(1f, texture.Width);
ImGui.Image(texture.Handle, new Vector2(drawWidth, drawHeight));
}
else
{
Ornament.Text(Theme.MutedDeep, "Loading...");
}

if (!string.IsNullOrWhiteSpace(image.Caption))
Ornament.TextWrapped(Theme.Muted, image.Caption!);

Ornament.FleuronDivider(Theme.BrassDim);
}

if (ImGui.Button("Close"))
{
ViewingGallery = null;
ImGui.CloseCurrentPopup();
}

ImGui.EndPopup();
}
}
11 changes: 9 additions & 2 deletions src/Beacon.Plugin/Windows/ChronicleWindow.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ public sealed class ChronicleWindow : Window
private string searchText = string.Empty;

/// <summary>The card face, shared with the editor's preview so the two cannot drift.</summary>
private readonly ImageViewerWindow viewer;

private readonly ProfileCard card;

public ChronicleWindow(
Expand All @@ -47,6 +49,7 @@ public ChronicleWindow(
LocationService location,
ImageCache images,
NotificationService notifications,
ImageViewerWindow viewer,
Action<ProfileDto?> openEditor)
: base("The Chronicle###BeaconChronicle")
{
Expand All @@ -57,7 +60,12 @@ public ChronicleWindow(
this.location = location;
this.images = images;
this.notifications = notifications;
card = new ProfileCard(images);
this.viewer = viewer;
card = new ProfileCard(images)
{
OpenImage = id => viewer.Show(id, "Likeness"),
OpenGallery = viewer.ShowGallery,
};
this.openEditor = openEditor;

SizeConstraints = new WindowSizeConstraints
Expand Down Expand Up @@ -101,7 +109,6 @@ public override void Draw()
ImGui.EndChild();

DrawStatusBar();
card.DrawGalleryPopup();
}

// --- Filters ---------------------------------------------------------
Expand Down
Loading
Loading