diff --git a/Rozpisky/App.xaml.cs b/Rozpisky/App.xaml.cs
index 38960aa..4e7e9b9 100644
--- a/Rozpisky/App.xaml.cs
+++ b/Rozpisky/App.xaml.cs
@@ -1,4 +1,4 @@
-using System.IO;
+using System.IO;
using System.Text;
using System.Windows;
using System.Windows.Threading;
@@ -7,8 +7,9 @@ namespace Rozpisky
{
///
/// Globální záchytná síť pro neošetřené výjimky – aplikace nesmí spadnout bez stopy.
- /// Každá výjimka se zapíše do error.log vedle .exe (stejný portable princip jako
- /// cad.json/layout.json) a uživateli se ukáže MessageBox. Výjimky na UI
+ /// Každá výjimka se zapíše do error.log v uživatelské složce
+ /// (, stejně jako cad.json/layout.json)
+ /// a uživateli se ukáže MessageBox. Výjimky na UI
/// (Dispatcher) vlákně se označí jako zpracované, takže aplikace běží dál – rozdělaný
/// projekt zůstane otevřený a jde uložit.
///
@@ -18,6 +19,10 @@ namespace Rozpisky
{
base.OnStartup(e);
+ // Data starších verzí ležela vedle .exe – přenést do uživatelské složky dřív,
+ // než si na ně sáhne kterýkoli store (ten už čte jen nové umístění).
+ Data.Migrace.PrenesStaraData();
+
// Úklid diagnostického logu tisku, ať po měsících nenaroste do enormních rozměrů.
Cad.CadTiskService.UklidLog();
@@ -46,14 +51,14 @@ namespace Rozpisky
{
string radek = $"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] {zdroj}: {ex?.ToString() ?? "(neznámá výjimka)"}"
+ Environment.NewLine + Environment.NewLine;
- File.AppendAllText(Path.Combine(AppContext.BaseDirectory, "error.log"), radek, Encoding.UTF8);
+ File.AppendAllText(Data.UzivatelskaData.Cesta("error.log"), radek, Encoding.UTF8);
}
- catch { /* read-only složka apod. – MessageBox se ukáže i tak */ }
+ catch { /* nezapisovatelná složka apod. – MessageBox se ukáže i tak */ }
}
private static void UkazChybu(Exception ex) => MessageBox.Show(
"Nastala neočekávaná chyba, aplikace ale běží dál – doporučujeme uložit projekt.\n\n" +
- $"{ex.Message}\n\nPodrobnosti jsou v souboru error.log vedle aplikace.",
+ $"{ex.Message}\n\nPodrobnosti jsou v souboru error.log ve složce\n{Data.UzivatelskaData.Slozka}",
"Neočekávaná chyba", MessageBoxButton.OK, MessageBoxImage.Error);
}
}
diff --git a/Rozpisky/Cad/CadEnginesStore.cs b/Rozpisky/Cad/CadEnginesStore.cs
index bac688f..b42f2c5 100644
--- a/Rozpisky/Cad/CadEnginesStore.cs
+++ b/Rozpisky/Cad/CadEnginesStore.cs
@@ -1,4 +1,4 @@
-using System.IO;
+using System.IO;
using System.Text.Encodings.Web;
using System.Text.Json;
using Rozpisky.Data;
@@ -7,9 +7,9 @@ namespace Rozpisky.Cad;
///
/// Načítání/ukládání seznamu podporovaných CAD systémů + naposledy vybraného enginu do JSON souboru
-/// vedle .exe (portable). Stejný princip jako /
-/// : CAD je vlastnost stroje (ne zakázky), takže výběr žije mimo
-/// projekt a je globální pro celou instalaci.
+/// v uživatelské složce (). CAD je vlastnost stroje
+/// (ne zakázky), takže výběr žije mimo projekt a je společný všem projektům – a mimo instalační
+/// složku, aby o něj uživatel při přeinstalaci nepřišel.
///
public static class CadEnginesStore
{
@@ -21,8 +21,8 @@ public static class CadEnginesStore
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
};
- /// Výchozí umístění: soubor vedle .exe (portable).
- public static string DefaultPath => Path.Combine(AppContext.BaseDirectory, SouborNazev);
+ /// Výchozí umístění: uživatelská složka (viz ).
+ public static string DefaultPath => UzivatelskaData.Cesta(SouborNazev);
/// Uložený výběr CADu: buď předdefinovaný engine (), nebo
/// ruční režim „Manuálně" ( + ). Režim a ruční
diff --git a/Rozpisky/Cad/CadTiskService.cs b/Rozpisky/Cad/CadTiskService.cs
index 20c40b1..001a1ce 100644
--- a/Rozpisky/Cad/CadTiskService.cs
+++ b/Rozpisky/Cad/CadTiskService.cs
@@ -1,4 +1,4 @@
-using System.Globalization;
+using System.Globalization;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
@@ -196,14 +196,15 @@ public static class CadTiskService
{
try
{
- var cesta = Path.Combine(AppContext.BaseDirectory, "tisk_diag.log");
+ var cesta = Data.UzivatelskaData.Cesta("tisk_diag.log");
if (File.Exists(cesta) && new FileInfo(cesta).Length > MaxLogBajtu)
File.Delete(cesta);
}
catch { /* read-only složka apod. – úklid nesmí přerušit start */ }
}
- /// Diagnostický „chlebníček“ – zapíše krok tisku do tisk_diag.log vedle .exe.
+ /// Diagnostický „chlebníček“ – zapíše krok tisku do tisk_diag.log
+ /// v uživatelské složce.
/// Slouží k lokalizaci tvrdého pádu CADu (nativní chyba v PlotToFile), který nezanechá
/// stopu v error.log ani v log okně (obojí zmizí s procesem). Zapisuje se synchronně (flush ihned),
/// takže poslední řádek přežije i pád procesu. Diagnostika sama nesmí nic shodit.
@@ -211,7 +212,7 @@ public static class CadTiskService
{
try
{
- File.AppendAllText(Path.Combine(AppContext.BaseDirectory, "tisk_diag.log"),
+ File.AppendAllText(Data.UzivatelskaData.Cesta("tisk_diag.log"),
$"[{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}] {krok}{Environment.NewLine}");
}
catch { /* read-only složka apod. – nesmí přerušit tisk */ }
diff --git a/Rozpisky/Cad/LayoutConfigStore.cs b/Rozpisky/Cad/LayoutConfigStore.cs
index 106bd1f..693b88f 100644
--- a/Rozpisky/Cad/LayoutConfigStore.cs
+++ b/Rozpisky/Cad/LayoutConfigStore.cs
@@ -1,4 +1,4 @@
-using System.IO;
+using System.IO;
using System.Text.Encodings.Web;
using System.Text.Json;
using Rozpisky.Data;
@@ -6,8 +6,8 @@ using Rozpisky.Data;
namespace Rozpisky.Cad;
///
-/// Načítání/ukládání do JSON souboru vedle .exe (portable). Stejný
-/// princip jako – konfigurace layoutů/tisku je vlastnost stroje/instalace
+/// Načítání/ukládání do JSON souboru v uživatelské složce
+/// (). Stejný princip jako – konfigurace layoutů/tisku je vlastnost stroje/instalace
/// (bloky a hladiny šablony, dostupné tiskárny), needituje se přes UI, jen ručně v souboru.
///
public static class LayoutConfigStore
@@ -20,8 +20,8 @@ public static class LayoutConfigStore
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
};
- /// Výchozí umístění: soubor vedle .exe (portable).
- public static string DefaultPath => Path.Combine(AppContext.BaseDirectory, SouborNazev);
+ /// Výchozí umístění: uživatelská složka (viz ).
+ public static string DefaultPath => UzivatelskaData.Cesta(SouborNazev);
/// Načte konfiguraci. Když soubor chybí/je poškozený, vrátí vestavěné výchozí hodnoty
/// (a rovnou je uloží, aby šel soubor dál ručně upravovat).
diff --git a/Rozpisky/Data/JsonConfigFile.cs b/Rozpisky/Data/JsonConfigFile.cs
index a7b977b..c649c16 100644
--- a/Rozpisky/Data/JsonConfigFile.cs
+++ b/Rozpisky/Data/JsonConfigFile.cs
@@ -1,12 +1,12 @@
-using System.IO;
+using System.IO;
using System.Text;
using System.Windows;
namespace Rozpisky.Data;
///
-/// Společné zacházení s JSON konfiguračními soubory vedle .exe (ciselniky.json,
-/// cad.json, layout.json, mapovani.json). Řeší dvě slabiny přímého
+/// Společné zacházení s JSON konfiguračními soubory (ciselniky.user.json, cad.json,
+/// layout.json v uživatelské složce, mapovani.json vedle .exe). Řeší dvě slabiny přímého
/// File.WriteAllText: pád uprostřed zápisu nesmí poškodit původní soubor (atomický zápis
/// přes .tmp) a poškozený soubor se nesmí tiše zahodit – ručně editovaný config s překlepem by se
/// jinak bez varování přepsal výchozími hodnotami a uživatel by přišel o vlastní data.
@@ -41,7 +41,7 @@ public static class JsonConfigFile
(zaloha is null
? "Zálohu původního souboru se nepodařilo vytvořit."
: $"Původní obsah je zálohovaný v:\n{zaloha}") +
- "\n\nPodrobnosti jsou v souboru error.log vedle aplikace.";
+ $"\n\nPodrobnosti jsou v souboru error.log ve složce\n{UzivatelskaData.Slozka}";
// BeginInvoke: hláška se ukáže až po rozběhnutí UI smyčky (config se čte před oknem).
Application.Current?.Dispatcher.BeginInvoke(() =>
MessageBox.Show(zprava, "Poškozený konfigurační soubor",
@@ -54,7 +54,7 @@ public static class JsonConfigFile
{
try
{
- File.AppendAllText(Path.Combine(AppContext.BaseDirectory, "error.log"),
+ File.AppendAllText(UzivatelskaData.Cesta("error.log"),
$"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] Config: {text}{Environment.NewLine}{Environment.NewLine}",
Encoding.UTF8);
}
diff --git a/Rozpisky/Data/Migrace.cs b/Rozpisky/Data/Migrace.cs
new file mode 100644
index 0000000..470fb1c
--- /dev/null
+++ b/Rozpisky/Data/Migrace.cs
@@ -0,0 +1,60 @@
+using System.IO;
+
+namespace Rozpisky.Data;
+
+///
+/// Jednorázový přesun dat starších verzí, které zapisovaly vedle .exe, do uživatelské složky
+/// (). Bez toho by uživatel po aktualizaci našel prázdné číselníky
+/// a přenastavený CAD. Volá se jednou při startu, ještě než si na data sáhne kterýkoli store.
+///
+/// Zásada: nikdy nic nemazat a nikdy nepřepsat novější data. Soubor se přenese jen tehdy, když
+/// v cíli ještě není; originál zůstane ležet jako *.migrovano (jediná kopie uživatelových dat).
+/// Selhání nesmí shodit start – horší než nepřenesená data je aplikace, která nenaběhne.
+///
+///
+public static class Migrace
+{
+ /// Přípona, kterou dostane původní soubor po přenesení.
+ public const string PriponaHotovo = ".migrovano";
+
+ /// Přenese cad.json a layout.json ze složky aplikace do uživatelské.
+ /// Číselníky mají vlastní cestu (rozpad na vrstvy) – viz .
+ public static void PrenesStaraData()
+ {
+ Prenes(CadEnginesStoreNazev);
+ Prenes(LayoutConfigStoreNazev);
+ }
+
+ private const string CadEnginesStoreNazev = "cad.json";
+ private const string LayoutConfigStoreNazev = "layout.json";
+
+ /// Přesune soubor ze složky aplikace do uživatelské, pokud tam ještě není.
+ /// V portable režimu jsou obě složky totožné a neudělá se nic.
+ private static void Prenes(string nazevSouboru)
+ {
+ try
+ {
+ var zdroj = Path.Combine(AppContext.BaseDirectory, nazevSouboru);
+ var cil = UzivatelskaData.Cesta(nazevSouboru);
+ if (string.Equals(zdroj, cil, StringComparison.OrdinalIgnoreCase)) return;
+ if (!File.Exists(zdroj) || File.Exists(cil)) return;
+
+ File.Copy(zdroj, cil);
+ OznacJakoPrenesene(zdroj);
+ }
+ catch (Exception ex) when (ex is IOException or UnauthorizedAccessException)
+ {
+ // Nezapisovatelná instalační složka apod. – data zůstanou tam, kde jsou, a zkusí se
+ // přenést zase příště. Start aplikace to zastavit nesmí.
+ }
+ }
+
+ /// Přejmenuje původní soubor na *.migrovano, aby se přenos neopakoval a aby
+ /// starší verze programu (kdyby ji uživatel vrátil) nezmátla už neaktuální data. Nedaří-li se
+ /// to (soubor jen pro čtení), nevadí – kopie v cíli už existuje a další běh přenos přeskočí.
+ internal static void OznacJakoPrenesene(string cesta)
+ {
+ try { File.Move(cesta, cesta + PriponaHotovo, overwrite: true); }
+ catch (Exception ex) when (ex is IOException or UnauthorizedAccessException) { }
+ }
+}
diff --git a/Rozpisky/Data/UzivatelskaData.cs b/Rozpisky/Data/UzivatelskaData.cs
new file mode 100644
index 0000000..39ce29d
--- /dev/null
+++ b/Rozpisky/Data/UzivatelskaData.cs
@@ -0,0 +1,53 @@
+using System.IO;
+
+namespace Rozpisky.Data;
+
+///
+/// Jediné místo pro data, která píše uživatel (číselníky, výběr CADu, konfigurace layoutů,
+/// logy). Leží v profilu uživatele (%APPDATA%\Rozpisky), ne vedle .exe – přeinstalace novou
+/// verzí ani .exe v Program Files (jen pro čtení) je tak nemůžou zničit.
+///
+/// Podklady dodávané s programem (šablona, mustr, loga, výchozí číselníky, mapování) zůstávají
+/// vedle .exe a čtou se přes – ty se naopak při každé instalaci
+/// obnovují. Hranice mezi tím, co se instaluje, a tím, co patří uživateli, vede přesně tudy.
+///
+///
+/// Portable režim: leží-li vedle .exe soubor portable.txt, vrátí se složka aplikace.
+/// Slouží k běhu z USB a k vývoji, kdy je pohodlnější mít všechno v jedné složce s buildem.
+///
+///
+public static class UzivatelskaData
+{
+ /// Značka portable režimu vedle .exe (obsah souboru se neřeší, stačí že existuje).
+ public const string PortableZnacka = "portable.txt";
+
+ private const string NazevSlozky = "Rozpisky";
+
+ private static string? _slozka;
+
+ /// Složka pro zapisovaná data. Počítá se jednou za běh a při prvním použití se vytvoří.
+ /// Když se vytvořit nepodaří (výjimečné – profil uživatele je zapisovatelný), spadne se zpátky
+ /// na složku aplikace, ať se program aspoň spustí.
+ public static string Slozka => _slozka ??= UrciSlozku();
+
+ /// Plná cesta k souboru se zapisovanými daty.
+ public static string Cesta(string nazevSouboru) => Path.Combine(Slozka, nazevSouboru);
+
+ private static string UrciSlozku()
+ {
+ var vedleExe = AppContext.BaseDirectory;
+ if (File.Exists(Path.Combine(vedleExe, PortableZnacka))) return vedleExe;
+
+ try
+ {
+ var slozka = Path.Combine(
+ Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), NazevSlozky);
+ Directory.CreateDirectory(slozka);
+ return slozka;
+ }
+ catch (Exception ex) when (ex is IOException or UnauthorizedAccessException)
+ {
+ return vedleExe;
+ }
+ }
+}