feat(ui): wire file and database nest storage
This commit is contained in:
@@ -0,0 +1,45 @@
|
|||||||
|
using System.Linq;
|
||||||
|
|
||||||
|
namespace OpenNest.Data;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Builds the shareable <see cref="NestRecord"/> metadata from a live <see cref="Nest"/>.
|
||||||
|
/// Counts are computed here (not carried on <see cref="Nest"/> itself) so every caller
|
||||||
|
/// that saves to the nest server gets the same definition of "plate" and "part" counts.
|
||||||
|
/// </summary>
|
||||||
|
public static class NestRecordFactory
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Builds a record for uploading <paramref name="nest"/>. <paramref name="id"/> is the
|
||||||
|
/// client-tracked id: <see cref="System.Guid.Empty"/> for a first save (the server
|
||||||
|
/// assigns one), or the previously returned id to update an existing record.
|
||||||
|
/// <paramref name="fileSize"/> is the size of the serialized .nest archive being
|
||||||
|
/// uploaded alongside this record; the server also recomputes it independently.
|
||||||
|
/// </summary>
|
||||||
|
public static NestRecord FromNest(Nest nest, System.Guid id, long fileSize)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(nest);
|
||||||
|
|
||||||
|
return new NestRecord
|
||||||
|
{
|
||||||
|
Id = id,
|
||||||
|
Name = nest.Name ?? "",
|
||||||
|
Customer = nest.Customer ?? "",
|
||||||
|
DateCreated = nest.DateCreated,
|
||||||
|
DateModified = nest.DateLastModified,
|
||||||
|
Material = nest.Material?.Name ?? "",
|
||||||
|
Thickness = nest.Thickness,
|
||||||
|
Status = nest.Status,
|
||||||
|
PlateCount = nest.Plates.Count,
|
||||||
|
PartCount = nest.Plates.Sum(CountNonCutoffParts),
|
||||||
|
// Nest has no separate "comments" field; Notes is the closest analog
|
||||||
|
// shown in the nest info dialog, so it round-trips as Comments here.
|
||||||
|
Comments = nest.Notes ?? "",
|
||||||
|
MadeBy = nest.MadeBy ?? "",
|
||||||
|
FileSize = fileSize,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int CountNonCutoffParts(Plate plate) =>
|
||||||
|
plate.Parts.Count(part => !part.BaseDrawing.IsCutOff);
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
namespace OpenNest.Data;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Tracks a document's database identity, bound to a particular server address.
|
||||||
|
/// A failed upload leaves that identity untouched. Switching servers creates a new
|
||||||
|
/// record rather than accidentally updating an unrelated record with the same id.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class NestSaveSession
|
||||||
|
{
|
||||||
|
public Guid RemoteId { get; private set; }
|
||||||
|
public string ServerUrl { get; private set; } = "";
|
||||||
|
|
||||||
|
public void Bind(Guid id, string serverUrl)
|
||||||
|
{
|
||||||
|
if (id == Guid.Empty)
|
||||||
|
throw new ArgumentException("A saved nest needs a non-empty id.", nameof(id));
|
||||||
|
RemoteId = id;
|
||||||
|
ServerUrl = Normalize(serverUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<NestRecord> SaveAsync(
|
||||||
|
INestRepository repository, string serverUrl, Nest nest, byte[] archive,
|
||||||
|
bool saveCopy = false, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(repository);
|
||||||
|
ArgumentNullException.ThrowIfNull(nest);
|
||||||
|
ArgumentNullException.ThrowIfNull(archive);
|
||||||
|
|
||||||
|
var normalizedUrl = Normalize(serverUrl);
|
||||||
|
if (normalizedUrl.Length == 0)
|
||||||
|
throw new ArgumentException("A database server URL is required.", nameof(serverUrl));
|
||||||
|
|
||||||
|
var id = !saveCopy && ServerUrl == normalizedUrl ? RemoteId : Guid.Empty;
|
||||||
|
var record = NestRecordFactory.FromNest(nest, id, archive.LongLength);
|
||||||
|
var saved = id == Guid.Empty
|
||||||
|
? await repository.UploadAsync(archive, record, cancellationToken)
|
||||||
|
: await repository.UpdateFileAsync(id, archive, record, cancellationToken);
|
||||||
|
if (saved.Id == Guid.Empty || (id != Guid.Empty && saved.Id != id))
|
||||||
|
throw new InvalidDataException("Server returned an invalid nest id.");
|
||||||
|
|
||||||
|
Bind(saved.Id, normalizedUrl);
|
||||||
|
return saved;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string Normalize(string? url) => (url ?? "").Trim().TrimEnd('/');
|
||||||
|
}
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
using OpenNest.Data;
|
||||||
|
|
||||||
|
using Vector = OpenNest.Geometry.Vector;
|
||||||
|
|
||||||
|
namespace OpenNest.Tests.Data;
|
||||||
|
|
||||||
|
public class NestRecordFactoryTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void FromNest_CountsPlatesAndNonCutoffParts()
|
||||||
|
{
|
||||||
|
var nest = new Nest("Job 42")
|
||||||
|
{
|
||||||
|
Customer = "Cozy Cat",
|
||||||
|
Material = new Material("Aluminum"),
|
||||||
|
Thickness = 0.125,
|
||||||
|
Status = NestStatus.ToBeCut,
|
||||||
|
MadeBy = "AJ",
|
||||||
|
Notes = "rush order",
|
||||||
|
DateCreated = new DateTime(2026, 1, 1),
|
||||||
|
DateLastModified = new DateTime(2026, 2, 2),
|
||||||
|
};
|
||||||
|
|
||||||
|
var plate1 = nest.CreatePlate();
|
||||||
|
var drawingA = new Drawing("A");
|
||||||
|
var drawingB = new Drawing("B");
|
||||||
|
nest.Drawings.Add(drawingA);
|
||||||
|
nest.Drawings.Add(drawingB);
|
||||||
|
plate1.Parts.Add(new Part(drawingA));
|
||||||
|
plate1.Parts.Add(new Part(drawingB));
|
||||||
|
|
||||||
|
var plate2 = nest.CreatePlate();
|
||||||
|
var drawingC = new Drawing("C");
|
||||||
|
nest.Drawings.Add(drawingC);
|
||||||
|
plate2.Parts.Add(new Part(drawingC));
|
||||||
|
var cutoff = new CutOff(new Vector(0, 0), CutOffAxis.Vertical);
|
||||||
|
plate2.CutOffs.Add(cutoff);
|
||||||
|
plate2.Parts.Add(new Part(cutoff.Drawing));
|
||||||
|
|
||||||
|
var record = NestRecordFactory.FromNest(nest, Guid.Empty, fileSize: 1024);
|
||||||
|
|
||||||
|
Assert.Equal(Guid.Empty, record.Id);
|
||||||
|
Assert.Equal("Job 42", record.Name);
|
||||||
|
Assert.Equal("Cozy Cat", record.Customer);
|
||||||
|
Assert.Equal("Aluminum", record.Material);
|
||||||
|
Assert.Equal(0.125, record.Thickness);
|
||||||
|
Assert.Equal(NestStatus.ToBeCut, record.Status);
|
||||||
|
Assert.Equal("AJ", record.MadeBy);
|
||||||
|
Assert.Equal("rush order", record.Comments);
|
||||||
|
Assert.Equal(new DateTime(2026, 1, 1), record.DateCreated);
|
||||||
|
Assert.Equal(new DateTime(2026, 2, 2), record.DateModified);
|
||||||
|
Assert.Equal(2, record.PlateCount);
|
||||||
|
// 2 real parts on plate1 + 1 real part on plate2; the cutoff part is excluded.
|
||||||
|
Assert.Equal(3, record.PartCount);
|
||||||
|
Assert.Equal(1024, record.FileSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void FromNest_EmptyNest_ZeroCounts()
|
||||||
|
{
|
||||||
|
var nest = new Nest("Empty");
|
||||||
|
|
||||||
|
var record = NestRecordFactory.FromNest(nest, Guid.NewGuid(), fileSize: 0);
|
||||||
|
|
||||||
|
Assert.Equal(0, record.PlateCount);
|
||||||
|
Assert.Equal(0, record.PartCount);
|
||||||
|
Assert.Equal("", record.Material);
|
||||||
|
Assert.Equal("", record.Comments);
|
||||||
|
Assert.Equal("", record.MadeBy);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void FromNest_PreservesGivenId()
|
||||||
|
{
|
||||||
|
var id = Guid.NewGuid();
|
||||||
|
var nest = new Nest("Keep Id");
|
||||||
|
|
||||||
|
var record = NestRecordFactory.FromNest(nest, id, fileSize: 5);
|
||||||
|
|
||||||
|
Assert.Equal(id, record.Id);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
using OpenNest.Data;
|
||||||
|
|
||||||
|
namespace OpenNest.Tests.Data;
|
||||||
|
|
||||||
|
public class NestSaveSessionTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public async Task Save_CreatesThenUpdatesAndSaveCopyCreatesNewRecord()
|
||||||
|
{
|
||||||
|
var repository = new FakeRepository();
|
||||||
|
var session = new NestSaveSession();
|
||||||
|
var nest = new Nest("Shop job");
|
||||||
|
var bytes = new byte[] { 1, 2, 3 };
|
||||||
|
|
||||||
|
await session.SaveAsync(repository, "http://shop:8090/", nest, bytes);
|
||||||
|
var firstId = session.RemoteId;
|
||||||
|
Assert.NotEqual(Guid.Empty, firstId);
|
||||||
|
Assert.Equal(3, repository.LastRecord!.FileSize);
|
||||||
|
Assert.Equal("Shop job", repository.LastRecord.Name);
|
||||||
|
Assert.Equal(1, repository.Uploads);
|
||||||
|
|
||||||
|
await session.SaveAsync(repository, "http://shop:8090", nest, bytes);
|
||||||
|
Assert.Equal(firstId, repository.UpdatedId);
|
||||||
|
Assert.Equal(1, repository.Updates);
|
||||||
|
|
||||||
|
await session.SaveAsync(repository, "http://shop:8090", nest, bytes, saveCopy: true);
|
||||||
|
Assert.NotEqual(firstId, session.RemoteId);
|
||||||
|
Assert.Equal(2, repository.Uploads);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task FailedSaveAndServerSwitch_NeverOverwritePriorIdentity()
|
||||||
|
{
|
||||||
|
var repository = new FakeRepository();
|
||||||
|
var session = new NestSaveSession();
|
||||||
|
var oldId = Guid.NewGuid();
|
||||||
|
session.Bind(oldId, "http://old:8090");
|
||||||
|
repository.Fail = true;
|
||||||
|
|
||||||
|
await Assert.ThrowsAsync<IOException>(() =>
|
||||||
|
session.SaveAsync(repository, "http://new:8090", new Nest("Job"), [1]));
|
||||||
|
Assert.Equal(oldId, session.RemoteId);
|
||||||
|
Assert.Equal("http://old:8090", session.ServerUrl);
|
||||||
|
Assert.Equal(Guid.Empty, repository.LastRecord!.Id);
|
||||||
|
Assert.Equal(0, repository.Updates);
|
||||||
|
|
||||||
|
repository.Fail = false;
|
||||||
|
await session.SaveAsync(repository, "http://new:8090", new Nest("Job"), [1]);
|
||||||
|
Assert.NotEqual(oldId, session.RemoteId);
|
||||||
|
Assert.Equal("http://new:8090", session.ServerUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class FakeRepository : INestRepository
|
||||||
|
{
|
||||||
|
public int Uploads { get; private set; }
|
||||||
|
public int Updates { get; private set; }
|
||||||
|
public Guid UpdatedId { get; private set; }
|
||||||
|
public NestRecord? LastRecord { get; private set; }
|
||||||
|
public bool Fail { get; set; }
|
||||||
|
|
||||||
|
public Task<NestRecord> UploadAsync(byte[] file, NestRecord record, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
Uploads++;
|
||||||
|
LastRecord = record;
|
||||||
|
if (Fail) throw new IOException("Offline");
|
||||||
|
record.Id = Guid.NewGuid();
|
||||||
|
return Task.FromResult(record);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<NestRecord> UpdateFileAsync(Guid id, byte[] file, NestRecord record, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
Updates++;
|
||||||
|
UpdatedId = id;
|
||||||
|
LastRecord = record;
|
||||||
|
if (Fail) throw new IOException("Offline");
|
||||||
|
return Task.FromResult(record);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<IReadOnlyList<NestRecord>> ListAsync(CancellationToken ct = default) => throw new NotImplementedException();
|
||||||
|
public Task<NestRecord?> GetMetadataAsync(Guid id, CancellationToken ct = default) => throw new NotImplementedException();
|
||||||
|
public Task<byte[]?> GetFileAsync(Guid id, CancellationToken ct = default) => throw new NotImplementedException();
|
||||||
|
public Task<NestRecord> UpdateMetadataAsync(Guid id, NestRecord record, CancellationToken ct = default) => throw new NotImplementedException();
|
||||||
|
public Task DeleteAsync(Guid id, CancellationToken ct = default) => throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,8 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using OpenNest.Data;
|
||||||
using OpenNest.IO;
|
using OpenNest.IO;
|
||||||
|
|
||||||
namespace OpenNest
|
namespace OpenNest
|
||||||
@@ -12,6 +15,12 @@ namespace OpenNest
|
|||||||
|
|
||||||
public string LastSavePath { get; private set; }
|
public string LastSavePath { get; private set; }
|
||||||
|
|
||||||
|
private readonly NestSaveSession remoteSession = new();
|
||||||
|
|
||||||
|
public Guid RemoteId => remoteSession.RemoteId;
|
||||||
|
|
||||||
|
public void BindRemote(Guid id, string serverUrl) => remoteSession.Bind(id, serverUrl);
|
||||||
|
|
||||||
public string Name => Nest?.Name;
|
public string Name => Nest?.Name;
|
||||||
|
|
||||||
public bool HasSavePath => File.Exists(LastSavePath);
|
public bool HasSavePath => File.Exists(LastSavePath);
|
||||||
@@ -26,5 +35,25 @@ namespace OpenNest
|
|||||||
var writer = new NestWriter(Nest);
|
var writer = new NestWriter(Nest);
|
||||||
writer.Write(path);
|
writer.Write(path);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Serializes the current nest to a .nest archive in memory.</summary>
|
||||||
|
public byte[] Serialize()
|
||||||
|
{
|
||||||
|
using var stream = new MemoryStream();
|
||||||
|
new NestWriter(Nest).Write(stream);
|
||||||
|
return stream.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Uploads this nest, preserving its server identity only on success.</summary>
|
||||||
|
public async Task<NestRecord> SaveToDatabaseAsync(
|
||||||
|
INestRepository repository, string serverUrl, bool saveCopy = false,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var bytes = Serialize();
|
||||||
|
var saved = await remoteSession.SaveAsync(
|
||||||
|
repository, serverUrl, Nest, bytes, saveCopy, cancellationToken);
|
||||||
|
LastSaveDate = DateTime.Now;
|
||||||
|
return saved;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+152
-23
@@ -30,6 +30,10 @@ namespace OpenNest.Forms
|
|||||||
private bool clickUpdateLocation;
|
private bool clickUpdateLocation;
|
||||||
private bool nestingInProgress;
|
private bool nestingInProgress;
|
||||||
private CancellationTokenSource nestingCts;
|
private CancellationTokenSource nestingCts;
|
||||||
|
private bool databaseSaveInProgress;
|
||||||
|
private bool databaseOpenInProgress;
|
||||||
|
private readonly ToolStripMenuItem storageModeMenu = new("Storage Mode...");
|
||||||
|
private readonly ToolStripMenuItem exportNestMenu = new("Export .nest...");
|
||||||
|
|
||||||
private const float ZoomInFactor = 1.5f;
|
private const float ZoomInFactor = 1.5f;
|
||||||
private const float ZoomOutFactor = 1.0f / ZoomInFactor;
|
private const float ZoomOutFactor = 1.0f / ZoomInFactor;
|
||||||
@@ -48,6 +52,10 @@ namespace OpenNest.Forms
|
|||||||
public MainForm()
|
public MainForm()
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
|
storageModeMenu.Click += StorageMode_Click;
|
||||||
|
exportNestMenu.Click += ExportNest_Click;
|
||||||
|
mnuFile.DropDownItems.Insert(mnuFile.DropDownItems.IndexOf(mnuFileExport), exportNestMenu);
|
||||||
|
mnuFile.DropDownItems.Insert(mnuFile.DropDownItems.IndexOf(mnuFileExit), storageModeMenu);
|
||||||
UpdateOverlapMenu();
|
UpdateOverlapMenu();
|
||||||
LoadSettings();
|
LoadSettings();
|
||||||
|
|
||||||
@@ -150,9 +158,12 @@ namespace OpenNest.Forms
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void LoadNest(Nest nest, FormWindowState windowState = FormWindowState.Maximized)
|
private void LoadNest(Nest nest, FormWindowState windowState = FormWindowState.Maximized,
|
||||||
|
Guid remoteId = default, string serverUrl = null)
|
||||||
{
|
{
|
||||||
var editForm = new EditNestForm(nest);
|
var editForm = new EditNestForm(nest);
|
||||||
|
if (remoteId != Guid.Empty)
|
||||||
|
editForm.Document.BindRemote(remoteId, serverUrl);
|
||||||
editForm.MdiParent = this;
|
editForm.MdiParent = this;
|
||||||
editForm.PlateChanged += (sender, e) =>
|
editForm.PlateChanged += (sender, e) =>
|
||||||
{
|
{
|
||||||
@@ -198,10 +209,11 @@ namespace OpenNest.Forms
|
|||||||
var hasValue = activeForm != null;
|
var hasValue = activeForm != null;
|
||||||
|
|
||||||
btnZoomToFit.Enabled = hasValue;
|
btnZoomToFit.Enabled = hasValue;
|
||||||
mnuFileSave.Enabled = hasValue;
|
mnuFileSave.Enabled = hasValue && !databaseSaveInProgress;
|
||||||
btnSave.Enabled = hasValue;
|
btnSave.Enabled = hasValue && !databaseSaveInProgress;
|
||||||
btnSaveAs.Enabled = hasValue;
|
btnSaveAs.Enabled = hasValue && !databaseSaveInProgress;
|
||||||
mnuFileSaveAs.Enabled = hasValue;
|
mnuFileSaveAs.Enabled = hasValue && !databaseSaveInProgress;
|
||||||
|
exportNestMenu.Enabled = hasValue;
|
||||||
mnuFileExport.Enabled = hasValue;
|
mnuFileExport.Enabled = hasValue;
|
||||||
mnuFileExportAll.Enabled = hasValue;
|
mnuFileExportAll.Enabled = hasValue;
|
||||||
btnZoomOut.Enabled = hasValue;
|
btnZoomOut.Enabled = hasValue;
|
||||||
@@ -563,21 +575,57 @@ namespace OpenNest.Forms
|
|||||||
LoadNest(nest, windowState);
|
LoadNest(nest, windowState);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void Open_Click(object sender, EventArgs e)
|
private async void Open_Click(object sender, EventArgs e)
|
||||||
{
|
{
|
||||||
var dlg = new OpenFileDialog();
|
if (NestStorage.Settings.Mode == NestStorageMode.File)
|
||||||
dlg.Filter = NestFormat.FileFilter;
|
|
||||||
dlg.Multiselect = true;
|
|
||||||
|
|
||||||
if (dlg.ShowDialog() == DialogResult.OK)
|
|
||||||
{
|
{
|
||||||
var reader = new NestReader(dlg.FileName);
|
using var dlg = new OpenFileDialog { Filter = NestFormat.FileFilter, Multiselect = true };
|
||||||
var nest = reader.Read();
|
if (dlg.ShowDialog(this) == DialogResult.OK)
|
||||||
LoadNest(nest);
|
{
|
||||||
if (reader.Warnings.Count > 0)
|
var reader = new NestReader(dlg.FileName);
|
||||||
MessageBox.Show(this, string.Join(Environment.NewLine, reader.Warnings),
|
var nest = reader.Read();
|
||||||
"Nest Load Warnings", MessageBoxButtons.OK, MessageBoxIcon.Warning);
|
LoadNest(nest);
|
||||||
|
ShowNestWarnings(reader);
|
||||||
|
}
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var repository = NestStorage.CreateRepository();
|
||||||
|
using var dlg = new SavedNestsForm(repository);
|
||||||
|
if (dlg.ShowDialog(this) != DialogResult.OK)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var serverUrl = NestStorage.Settings.ServerUrl;
|
||||||
|
databaseOpenInProgress = true;
|
||||||
|
storageModeMenu.Enabled = false;
|
||||||
|
var file = await repository.GetFileAsync(dlg.SelectedId);
|
||||||
|
if (file == null)
|
||||||
|
throw new FileNotFoundException("This nest no longer exists on the server.");
|
||||||
|
using var stream = new MemoryStream(file);
|
||||||
|
var reader = new NestReader(stream);
|
||||||
|
var nest = reader.Read();
|
||||||
|
LoadNest(nest, remoteId: dlg.SelectedId, serverUrl: serverUrl);
|
||||||
|
ShowNestWarnings(reader);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
ShowStorageError("open", ex);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
databaseOpenInProgress = false;
|
||||||
|
if (!IsDisposed)
|
||||||
|
storageModeMenu.Enabled = !databaseSaveInProgress;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ShowNestWarnings(NestReader reader)
|
||||||
|
{
|
||||||
|
if (reader.Warnings.Count > 0)
|
||||||
|
MessageBox.Show(this, string.Join(Environment.NewLine, reader.Warnings),
|
||||||
|
"Nest Load Warnings", MessageBoxButtons.OK, MessageBoxIcon.Warning);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ImportBom_Click(object sender, EventArgs e)
|
private void ImportBom_Click(object sender, EventArgs e)
|
||||||
@@ -587,18 +635,99 @@ namespace OpenNest.Forms
|
|||||||
form.ShowDialog(this);
|
form.ShowDialog(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void Save_Click(object sender, EventArgs e)
|
private async void Save_Click(object sender, EventArgs e)
|
||||||
{
|
{
|
||||||
if (activeForm != null)
|
var form = activeForm;
|
||||||
activeForm.Save();
|
if (form == null || databaseSaveInProgress)
|
||||||
|
return;
|
||||||
|
if (NestStorage.Settings.Mode == NestStorageMode.File)
|
||||||
|
form.Save();
|
||||||
|
else
|
||||||
|
await SaveDatabaseAsync(form, saveCopy: false);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void SaveAs_Click(object sender, EventArgs e)
|
private async void SaveAs_Click(object sender, EventArgs e)
|
||||||
{
|
{
|
||||||
if (activeForm != null)
|
var form = activeForm;
|
||||||
activeForm.SaveAs();
|
if (form == null || databaseSaveInProgress)
|
||||||
|
return;
|
||||||
|
if (NestStorage.Settings.Mode == NestStorageMode.File)
|
||||||
|
form.SaveAs();
|
||||||
|
else
|
||||||
|
await SaveDatabaseAsync(form, saveCopy: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task SaveDatabaseAsync(EditNestForm form, bool saveCopy)
|
||||||
|
{
|
||||||
|
databaseSaveInProgress = true;
|
||||||
|
storageModeMenu.Enabled = false;
|
||||||
|
EnableCheck();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var serverUrl = NestStorage.Settings.ServerUrl;
|
||||||
|
var saved = await form.Document.SaveToDatabaseAsync(
|
||||||
|
NestStorage.CreateRepository(), serverUrl, saveCopy);
|
||||||
|
if (!form.IsDisposed)
|
||||||
|
form.Text = form.Document.Name;
|
||||||
|
if (!IsDisposed)
|
||||||
|
statusLabel1.Text = $"Saved {saved.Name} to nest database";
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
if (!IsDisposed)
|
||||||
|
ShowStorageError("save", ex);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
databaseSaveInProgress = false;
|
||||||
|
if (!IsDisposed)
|
||||||
|
{
|
||||||
|
storageModeMenu.Enabled = !databaseOpenInProgress;
|
||||||
|
EnableCheck();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void StorageMode_Click(object sender, EventArgs e)
|
||||||
|
{
|
||||||
|
using var dlg = new StorageModeForm();
|
||||||
|
if (dlg.ShowDialog(this) != DialogResult.OK)
|
||||||
|
return;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
NestStorage.Save(dlg.SelectedMode, dlg.ServerUrl);
|
||||||
|
statusLabel1.Text = $"Nest storage: {dlg.SelectedMode}";
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
ShowStorageError("change storage mode", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ExportNest_Click(object sender, EventArgs e)
|
||||||
|
{
|
||||||
|
var form = activeForm;
|
||||||
|
if (form == null)
|
||||||
|
return;
|
||||||
|
using var dlg = new SaveFileDialog { Filter = NestFormat.FileFilter, FileName = form.Nest.Name };
|
||||||
|
if (dlg.ShowDialog(this) != DialogResult.OK)
|
||||||
|
return;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Export never changes the document's save path, remote id, or nest name.
|
||||||
|
new NestWriter(form.Nest).Write(dlg.FileName);
|
||||||
|
statusLabel1.Text = $"Exported .nest to {dlg.FileName}";
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
ShowStorageError("export", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ShowStorageError(string action, Exception ex) =>
|
||||||
|
MessageBox.Show(this, $"Could not {action} nest: {ex.Message}",
|
||||||
|
"Nest Storage", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||||||
|
|
||||||
private void Export_Click(object sender, EventArgs e)
|
private void Export_Click(object sender, EventArgs e)
|
||||||
{
|
{
|
||||||
if (activeForm == null)
|
if (activeForm == null)
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
using System;
|
||||||
|
using OpenNest.Data;
|
||||||
|
|
||||||
|
namespace OpenNest.Forms
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// App-scoped nest storage-mode selection (File vs Database) and the shared
|
||||||
|
/// <see cref="RemoteNestRepository"/> for the configured server. Settings persist
|
||||||
|
/// to %APPDATA%\OpenNest\storage.json via <see cref="NestStorageSettings"/>.
|
||||||
|
/// </summary>
|
||||||
|
public static class NestStorage
|
||||||
|
{
|
||||||
|
private static NestStorageSettings settings =
|
||||||
|
NestStorageSettings.Load(NestStorageSettings.DefaultPath);
|
||||||
|
|
||||||
|
private static RemoteNestRepository cachedRepository;
|
||||||
|
private static string cachedServerUrl;
|
||||||
|
|
||||||
|
/// <summary>Current settings, loaded once at process start.</summary>
|
||||||
|
public static NestStorageSettings Settings => settings;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Persists new settings and invalidates the cached repository so the next
|
||||||
|
/// <see cref="CreateRepository"/> call picks up a changed server URL.
|
||||||
|
/// If persistence fails the current mode remains unchanged.
|
||||||
|
/// </summary>
|
||||||
|
public static void Save(NestStorageMode mode, string serverUrl)
|
||||||
|
{
|
||||||
|
var next = new NestStorageSettings { Mode = mode, ServerUrl = serverUrl };
|
||||||
|
next.Save(NestStorageSettings.DefaultPath);
|
||||||
|
settings = next;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The shared repository for the current server URL, recreated only when the
|
||||||
|
/// URL changes. Throws <see cref="ArgumentException"/> if the configured URL
|
||||||
|
/// is not a valid absolute http/https address.
|
||||||
|
/// </summary>
|
||||||
|
public static INestRepository CreateRepository()
|
||||||
|
{
|
||||||
|
if (cachedRepository == null || cachedServerUrl != settings.ServerUrl)
|
||||||
|
{
|
||||||
|
cachedRepository?.Dispose();
|
||||||
|
cachedRepository = new RemoteNestRepository(settings.ServerUrl);
|
||||||
|
cachedServerUrl = settings.ServerUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
return cachedRepository;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,257 @@
|
|||||||
|
using System;
|
||||||
|
using System.Data;
|
||||||
|
using System.Drawing;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using System.Windows.Forms;
|
||||||
|
using OpenNest.Data;
|
||||||
|
|
||||||
|
namespace OpenNest.Forms;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// File > Open (Database mode): lists nests from the shared nest server with
|
||||||
|
/// filterable/sortable metadata columns, and lets the operator open or delete one.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class SavedNestsForm : Form
|
||||||
|
{
|
||||||
|
private readonly INestRepository repository;
|
||||||
|
private readonly DataGridView grid;
|
||||||
|
private readonly TextBox searchBox;
|
||||||
|
private readonly Button openButton;
|
||||||
|
private readonly Button deleteButton;
|
||||||
|
private readonly Button refreshButton;
|
||||||
|
private NestRecord[] records = Array.Empty<NestRecord>();
|
||||||
|
|
||||||
|
/// <summary>Set to the chosen record's id when the dialog closes with OK.</summary>
|
||||||
|
public Guid SelectedId { get; private set; }
|
||||||
|
|
||||||
|
public SavedNestsForm(INestRepository repository)
|
||||||
|
{
|
||||||
|
this.repository = repository;
|
||||||
|
|
||||||
|
Text = "Open Nest — Database";
|
||||||
|
StartPosition = FormStartPosition.CenterParent;
|
||||||
|
ClientSize = new Size(880, 480);
|
||||||
|
MinimizeBox = false;
|
||||||
|
MaximizeBox = false;
|
||||||
|
ShowInTaskbar = false;
|
||||||
|
|
||||||
|
var topPanel = new Panel { Dock = DockStyle.Top, Height = 34, Padding = new Padding(6) };
|
||||||
|
var searchLabel = new Label { Text = "Filter:", AutoSize = true, Location = new Point(6, 10) };
|
||||||
|
searchBox = new TextBox { Location = new Point(50, 6), Width = 300 };
|
||||||
|
searchBox.TextChanged += (_, _) => ApplyFilter();
|
||||||
|
refreshButton = new Button { Text = "Refresh", AutoSize = true, Location = new Point(360, 5) };
|
||||||
|
refreshButton.Click += async (_, _) => await LoadAsync();
|
||||||
|
topPanel.Controls.Add(searchLabel);
|
||||||
|
topPanel.Controls.Add(searchBox);
|
||||||
|
topPanel.Controls.Add(refreshButton);
|
||||||
|
|
||||||
|
grid = new DataGridView
|
||||||
|
{
|
||||||
|
Dock = DockStyle.Fill,
|
||||||
|
ReadOnly = true,
|
||||||
|
AllowUserToAddRows = false,
|
||||||
|
AllowUserToDeleteRows = false,
|
||||||
|
SelectionMode = DataGridViewSelectionMode.FullRowSelect,
|
||||||
|
MultiSelect = false,
|
||||||
|
AutoSizeColumnsMode = DataGridViewAutoSizeColumnsMode.DisplayedCells,
|
||||||
|
};
|
||||||
|
grid.CellDoubleClick += (_, e) =>
|
||||||
|
{
|
||||||
|
if (e.RowIndex >= 0)
|
||||||
|
Open_Click(this, EventArgs.Empty);
|
||||||
|
};
|
||||||
|
|
||||||
|
var buttons = new FlowLayoutPanel
|
||||||
|
{
|
||||||
|
Dock = DockStyle.Bottom,
|
||||||
|
Height = 44,
|
||||||
|
FlowDirection = FlowDirection.RightToLeft,
|
||||||
|
Padding = new Padding(6),
|
||||||
|
};
|
||||||
|
var cancel = new Button { Text = "Cancel", AutoSize = true, DialogResult = DialogResult.Cancel };
|
||||||
|
deleteButton = new Button { Text = "Delete", AutoSize = true, Enabled = false };
|
||||||
|
deleteButton.Click += Delete_Click;
|
||||||
|
openButton = new Button { Text = "Open", AutoSize = true, Enabled = false };
|
||||||
|
openButton.Click += Open_Click;
|
||||||
|
buttons.Controls.Add(cancel);
|
||||||
|
buttons.Controls.Add(openButton);
|
||||||
|
buttons.Controls.Add(deleteButton);
|
||||||
|
|
||||||
|
grid.SelectionChanged += (_, _) =>
|
||||||
|
{
|
||||||
|
var hasSelection = grid.SelectedRows.Count > 0;
|
||||||
|
openButton.Enabled = hasSelection;
|
||||||
|
deleteButton.Enabled = hasSelection;
|
||||||
|
};
|
||||||
|
|
||||||
|
Controls.Add(grid);
|
||||||
|
Controls.Add(topPanel);
|
||||||
|
Controls.Add(buttons);
|
||||||
|
CancelButton = cancel;
|
||||||
|
|
||||||
|
Shown += async (_, _) => await LoadAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task LoadAsync()
|
||||||
|
{
|
||||||
|
Cursor = Cursors.WaitCursor;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var list = await repository.ListAsync();
|
||||||
|
if (IsDisposed)
|
||||||
|
return;
|
||||||
|
records = list.ToArray();
|
||||||
|
ApplyFilter();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
if (!IsDisposed)
|
||||||
|
MessageBox.Show(
|
||||||
|
this,
|
||||||
|
$"Could not load nests from the server: {ex.Message}",
|
||||||
|
"Open Nest — Database",
|
||||||
|
MessageBoxButtons.OK,
|
||||||
|
MessageBoxIcon.Error
|
||||||
|
);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (!IsDisposed)
|
||||||
|
Cursor = Cursors.Default;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ApplyFilter()
|
||||||
|
{
|
||||||
|
var filter = searchBox.Text.Trim();
|
||||||
|
var filtered = string.IsNullOrEmpty(filter)
|
||||||
|
? records
|
||||||
|
: records
|
||||||
|
.Where(r =>
|
||||||
|
Contains(r.Name, filter)
|
||||||
|
|| Contains(r.Customer, filter)
|
||||||
|
|| Contains(r.Material, filter)
|
||||||
|
|| Contains(r.MadeBy, filter)
|
||||||
|
|| Contains(r.Comments, filter)
|
||||||
|
|| Contains(r.Status.ToString(), filter)
|
||||||
|
|| Contains(FormatStatus(r.Status), filter)
|
||||||
|
|| Contains(r.DateCreated.ToString("g"), filter)
|
||||||
|
|| Contains(r.DateModified.ToString("g"), filter)
|
||||||
|
|| Contains(r.SavedAt.ToString("g"), filter)
|
||||||
|
|| Contains(r.Thickness.ToString(), filter)
|
||||||
|
|| Contains(r.PlateCount.ToString(), filter)
|
||||||
|
|| Contains(r.PartCount.ToString(), filter)
|
||||||
|
|| Contains(r.FileSize.ToString(), filter)
|
||||||
|
)
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
Populate(filtered);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool Contains(string value, string filter) =>
|
||||||
|
!string.IsNullOrEmpty(value) && value.Contains(filter, StringComparison.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
private void Populate(NestRecord[] rows)
|
||||||
|
{
|
||||||
|
var table = new DataTable();
|
||||||
|
table.Columns.Add("Name", typeof(string));
|
||||||
|
table.Columns.Add("Customer", typeof(string));
|
||||||
|
table.Columns.Add("Status", typeof(string));
|
||||||
|
table.Columns.Add("Material", typeof(string));
|
||||||
|
table.Columns.Add("Date Created", typeof(DateTime));
|
||||||
|
table.Columns.Add("Date Modified", typeof(DateTime));
|
||||||
|
table.Columns.Add("Thickness", typeof(double));
|
||||||
|
table.Columns.Add("Plates", typeof(int));
|
||||||
|
table.Columns.Add("Parts", typeof(int));
|
||||||
|
table.Columns.Add("Made By", typeof(string));
|
||||||
|
table.Columns.Add("Comments", typeof(string));
|
||||||
|
table.Columns.Add("File Size", typeof(long));
|
||||||
|
table.Columns.Add("Saved", typeof(DateTime));
|
||||||
|
table.Columns.Add("Id", typeof(Guid));
|
||||||
|
|
||||||
|
foreach (var record in rows.OrderByDescending(r => r.SavedAt))
|
||||||
|
{
|
||||||
|
table.Rows.Add(
|
||||||
|
record.Name,
|
||||||
|
record.Customer,
|
||||||
|
FormatStatus(record.Status),
|
||||||
|
record.Material,
|
||||||
|
record.DateCreated,
|
||||||
|
record.DateModified,
|
||||||
|
record.Thickness,
|
||||||
|
record.PlateCount,
|
||||||
|
record.PartCount,
|
||||||
|
record.MadeBy,
|
||||||
|
record.Comments,
|
||||||
|
record.FileSize,
|
||||||
|
record.SavedAt,
|
||||||
|
record.Id
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
grid.DataSource = table;
|
||||||
|
if (grid.Columns.Contains("Id"))
|
||||||
|
grid.Columns["Id"].Visible = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string FormatStatus(NestStatus status) => status switch
|
||||||
|
{
|
||||||
|
NestStatus.Quote => "Quote",
|
||||||
|
NestStatus.ToBeCut => "To Be Cut",
|
||||||
|
NestStatus.HasBeenCut => "Has Been Cut",
|
||||||
|
_ => status.ToString(),
|
||||||
|
};
|
||||||
|
|
||||||
|
private void Open_Click(object sender, EventArgs e)
|
||||||
|
{
|
||||||
|
if (grid.SelectedRows.Count == 0)
|
||||||
|
return;
|
||||||
|
SelectedId = (Guid)grid.SelectedRows[0].Cells["Id"].Value;
|
||||||
|
DialogResult = DialogResult.OK;
|
||||||
|
Close();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void Delete_Click(object sender, EventArgs e)
|
||||||
|
{
|
||||||
|
if (grid.SelectedRows.Count == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var id = (Guid)grid.SelectedRows[0].Cells["Id"].Value;
|
||||||
|
var name = grid.SelectedRows[0].Cells["Name"].Value?.ToString() ?? "";
|
||||||
|
|
||||||
|
var confirm = MessageBox.Show(
|
||||||
|
this,
|
||||||
|
$"Delete '{name}' from the server? This cannot be undone.",
|
||||||
|
"Delete Nest",
|
||||||
|
MessageBoxButtons.YesNo,
|
||||||
|
MessageBoxIcon.Warning
|
||||||
|
);
|
||||||
|
if (confirm != DialogResult.Yes)
|
||||||
|
return;
|
||||||
|
|
||||||
|
deleteButton.Enabled = false;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await repository.DeleteAsync(id);
|
||||||
|
if (!IsDisposed)
|
||||||
|
await LoadAsync();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
if (!IsDisposed)
|
||||||
|
MessageBox.Show(
|
||||||
|
this,
|
||||||
|
$"Could not delete the nest: {ex.Message}",
|
||||||
|
"Delete Nest",
|
||||||
|
MessageBoxButtons.OK,
|
||||||
|
MessageBoxIcon.Error
|
||||||
|
);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (!IsDisposed)
|
||||||
|
deleteButton.Enabled = grid.SelectedRows.Count > 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,140 @@
|
|||||||
|
using System;
|
||||||
|
using System.Drawing;
|
||||||
|
using System.Windows.Forms;
|
||||||
|
using OpenNest.Data;
|
||||||
|
|
||||||
|
namespace OpenNest.Forms;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// File > Storage Mode...: choose File mode (local .nest files, the default and
|
||||||
|
/// current behavior) or Database mode (Save/Open go through the shared nest server).
|
||||||
|
/// </summary>
|
||||||
|
public sealed class StorageModeForm : Form
|
||||||
|
{
|
||||||
|
private readonly RadioButton fileModeRadio;
|
||||||
|
private readonly RadioButton databaseModeRadio;
|
||||||
|
private readonly TextBox serverUrlBox;
|
||||||
|
private readonly Label serverUrlLabel;
|
||||||
|
|
||||||
|
public StorageModeForm()
|
||||||
|
{
|
||||||
|
Text = "Storage Mode";
|
||||||
|
StartPosition = FormStartPosition.CenterParent;
|
||||||
|
ClientSize = new Size(420, 190);
|
||||||
|
MinimizeBox = false;
|
||||||
|
MaximizeBox = false;
|
||||||
|
ShowInTaskbar = false;
|
||||||
|
FormBorderStyle = FormBorderStyle.FixedDialog;
|
||||||
|
|
||||||
|
var info = new Label
|
||||||
|
{
|
||||||
|
Dock = DockStyle.Top,
|
||||||
|
Height = 40,
|
||||||
|
Padding = new Padding(10, 10, 10, 0),
|
||||||
|
Text = "Database mode shares saved nests with every shop PC through a central server.",
|
||||||
|
};
|
||||||
|
|
||||||
|
fileModeRadio = new RadioButton
|
||||||
|
{
|
||||||
|
Text = "File — save/open local .nest files (default)",
|
||||||
|
AutoSize = true,
|
||||||
|
Location = new Point(14, 54),
|
||||||
|
Checked = true,
|
||||||
|
};
|
||||||
|
databaseModeRadio = new RadioButton
|
||||||
|
{
|
||||||
|
Text = "Database — save/open through a shared nest server",
|
||||||
|
AutoSize = true,
|
||||||
|
Location = new Point(14, 80),
|
||||||
|
};
|
||||||
|
|
||||||
|
serverUrlLabel = new Label
|
||||||
|
{
|
||||||
|
Text = "Server URL:",
|
||||||
|
AutoSize = true,
|
||||||
|
Location = new Point(32, 112),
|
||||||
|
Enabled = false,
|
||||||
|
};
|
||||||
|
serverUrlBox = new TextBox
|
||||||
|
{
|
||||||
|
Location = new Point(110, 108),
|
||||||
|
Width = 280,
|
||||||
|
PlaceholderText = "http://barge.lan:8090",
|
||||||
|
Enabled = false,
|
||||||
|
};
|
||||||
|
|
||||||
|
fileModeRadio.CheckedChanged += (_, _) => UpdateServerUrlEnabled();
|
||||||
|
databaseModeRadio.CheckedChanged += (_, _) => UpdateServerUrlEnabled();
|
||||||
|
|
||||||
|
var buttons = new FlowLayoutPanel
|
||||||
|
{
|
||||||
|
Dock = DockStyle.Bottom,
|
||||||
|
Height = 44,
|
||||||
|
FlowDirection = FlowDirection.RightToLeft,
|
||||||
|
Padding = new Padding(6),
|
||||||
|
};
|
||||||
|
var cancel = new Button { Text = "Cancel", AutoSize = true, DialogResult = DialogResult.Cancel };
|
||||||
|
var ok = new Button { Text = "OK", AutoSize = true };
|
||||||
|
ok.Click += Ok_Click;
|
||||||
|
buttons.Controls.Add(cancel);
|
||||||
|
buttons.Controls.Add(ok);
|
||||||
|
|
||||||
|
Controls.Add(databaseModeRadio);
|
||||||
|
Controls.Add(fileModeRadio);
|
||||||
|
Controls.Add(serverUrlBox);
|
||||||
|
Controls.Add(serverUrlLabel);
|
||||||
|
Controls.Add(info);
|
||||||
|
Controls.Add(buttons);
|
||||||
|
|
||||||
|
AcceptButton = ok;
|
||||||
|
CancelButton = cancel;
|
||||||
|
|
||||||
|
LoadCurrentSettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
public NestStorageMode SelectedMode =>
|
||||||
|
databaseModeRadio.Checked ? NestStorageMode.Database : NestStorageMode.File;
|
||||||
|
|
||||||
|
public string ServerUrl => serverUrlBox.Text.Trim();
|
||||||
|
|
||||||
|
private void LoadCurrentSettings()
|
||||||
|
{
|
||||||
|
var settings = NestStorage.Settings;
|
||||||
|
databaseModeRadio.Checked = settings.Mode == NestStorageMode.Database;
|
||||||
|
fileModeRadio.Checked = !databaseModeRadio.Checked;
|
||||||
|
serverUrlBox.Text = settings.ServerUrl;
|
||||||
|
UpdateServerUrlEnabled();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateServerUrlEnabled()
|
||||||
|
{
|
||||||
|
serverUrlBox.Enabled = databaseModeRadio.Checked;
|
||||||
|
serverUrlLabel.Enabled = databaseModeRadio.Checked;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Ok_Click(object sender, EventArgs e)
|
||||||
|
{
|
||||||
|
if (databaseModeRadio.Checked)
|
||||||
|
{
|
||||||
|
var url = ServerUrl;
|
||||||
|
var isValidUrl =
|
||||||
|
Uri.TryCreate(url, UriKind.Absolute, out var uri)
|
||||||
|
&& (uri.Scheme == Uri.UriSchemeHttp || uri.Scheme == Uri.UriSchemeHttps);
|
||||||
|
|
||||||
|
if (!isValidUrl)
|
||||||
|
{
|
||||||
|
MessageBox.Show(
|
||||||
|
this,
|
||||||
|
"Enter a valid server URL, e.g. http://barge.lan:8090",
|
||||||
|
"Storage Mode",
|
||||||
|
MessageBoxButtons.OK,
|
||||||
|
MessageBoxIcon.Warning
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DialogResult = DialogResult.OK;
|
||||||
|
Close();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,10 +4,15 @@ The desktop app can save nests two ways:
|
|||||||
|
|
||||||
- **File mode** (default, unchanged behavior): Save/Save As write a `.nest` ZIP
|
- **File mode** (default, unchanged behavior): Save/Save As write a `.nest` ZIP
|
||||||
archive to disk via the normal file dialog. See [nest-file-format.md](nest-file-format.md).
|
archive to disk via the normal file dialog. See [nest-file-format.md](nest-file-format.md).
|
||||||
- **Database mode**: Save uploads the nest to a central `OpenNest.Server`
|
- **Database mode**: Open lists server records with filterable metadata and
|
||||||
instance shared by every shop PC, instead of writing a local file. A separate
|
sortable columns; Save creates a record on first save and updates that same
|
||||||
**File > Export .nest...** command is always available (in both modes) for
|
record afterward. Save As creates a new record (a copy). A failed save keeps
|
||||||
producing a local file to share or back up.
|
the previous record association; switching server URLs creates a record on the
|
||||||
|
new server rather than updating an id from the previous one. Delete in the
|
||||||
|
saved-nest list permanently removes the selected server record after confirmation.
|
||||||
|
A separate **File > Export .nest...** command is always available (in both
|
||||||
|
modes) for producing a local file to share or back up. Export does not change
|
||||||
|
the document's file save path or database record association.
|
||||||
|
|
||||||
The mode and server address are stored per-PC at `%APPDATA%\OpenNest\storage.json`
|
The mode and server address are stored per-PC at `%APPDATA%\OpenNest\storage.json`
|
||||||
(`OpenNest.Data.NestStorageSettings`), defaulting to File mode so existing
|
(`OpenNest.Data.NestStorageSettings`), defaulting to File mode so existing
|
||||||
|
|||||||
Reference in New Issue
Block a user