diff --git a/src/Duempelkas.Desktop/Duempelkas.Desktop.csproj b/src/Duempelkas.Desktop/Duempelkas.Desktop.csproj new file mode 100644 index 0000000..19e429f --- /dev/null +++ b/src/Duempelkas.Desktop/Duempelkas.Desktop.csproj @@ -0,0 +1,27 @@ + + + net10.0 + Exe + enable + enable + + + + + PreserveNewest + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + diff --git a/src/Duempelkas.Desktop/Program.cs b/src/Duempelkas.Desktop/Program.cs new file mode 100644 index 0000000..02c53c6 --- /dev/null +++ b/src/Duempelkas.Desktop/Program.cs @@ -0,0 +1,74 @@ +using Duempelkas.Infrastructure; +using Duempelkas.Infrastructure.Persistence; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Photino.Blazor; + +namespace Duempelkas.Desktop; + +class Program +{ + [STAThread] + static void Main(string[] args) + { + var appBuilder = PhotinoBlazorAppBuilder.CreateDefault(args); + + var dbPath = Path.Combine(AppContext.BaseDirectory, "duempelkas.db"); + appBuilder.Services.AddInfrastructure($"Data Source={dbPath}"); + + appBuilder.RootComponents.Add("app"); + + var app = appBuilder.Build(); + + app.MainWindow.StartUrl = PhotinoWebViewManager.AppBaseUri; + + using (var scope = app.Services.CreateScope()) + { + var db = scope.ServiceProvider.GetRequiredService(); + db.Database.Migrate(); + } + + // Load saved window size or use defaults + var settingsPath = Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), + "Duempelkas", "window.json"); + + int savedWidth = 1280, savedHeight = 768; + if (File.Exists(settingsPath)) + { + try + { + var json = File.ReadAllText(settingsPath); + var doc = System.Text.Json.JsonDocument.Parse(json); + savedWidth = doc.RootElement.GetProperty("width").GetInt32(); + savedHeight = doc.RootElement.GetProperty("height").GetInt32(); + } + catch { /* use defaults on parse error */ } + } + + app.MainWindow + .SetTitle("Dümpelkas – Kassenbuch") + .SetSize(savedWidth, savedHeight) + .SetUseOsDefaultSize(false); + + // Persist window size on resize + app.MainWindow.RegisterSizeChangedHandler((_, size) => + { + try + { + var dir = Path.GetDirectoryName(settingsPath)!; + Directory.CreateDirectory(dir); + File.WriteAllText(settingsPath, + System.Text.Json.JsonSerializer.Serialize(new { width = size.Width, height = size.Height })); + } + catch { /* ignore save errors */ } + }); + + AppDomain.CurrentDomain.UnhandledException += (sender, error) => + { + app.MainWindow.ShowMessage("Schwerwiegender Fehler", error.ExceptionObject.ToString()); + }; + + app.Run(); + } +} diff --git a/src/Duempelkas.Desktop/wwwroot/css/app.css b/src/Duempelkas.Desktop/wwwroot/css/app.css new file mode 100644 index 0000000..0daa23b --- /dev/null +++ b/src/Duempelkas.Desktop/wwwroot/css/app.css @@ -0,0 +1,475 @@ +/* Duempelkas App Styles – Dark Mode */ + +:root { + --color-income: #4ade80; + --color-expense: #f87171; + --color-transfer: #60a5fa; + --color-bg: #1a1a2e; + --color-surface: #16213e; + --color-surface-hover: #1e2d4a; + --color-border: #2a2a4a; + --color-text: #e2e8f0; + --color-text-muted: #94a3b8; + --color-accent: #818cf8; +} + +html, body { + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; + background-color: var(--color-bg); + color: var(--color-text); + margin: 0; + padding: 0; + height: 100%; + overflow: hidden; +} + +/* Blazor error UI – hidden by default, shown by framework JS on unhandled errors */ +#blazor-error-ui { + display: none; + position: fixed; + bottom: 0; + left: 0; + right: 0; + padding: 0.5rem 1rem; + background: rgba(248, 113, 113, 0.95); + color: #fff; + font-size: 0.85rem; + z-index: 9999; + text-align: center; +} + +#blazor-error-ui a { + color: #fff; + text-decoration: underline; + margin-left: 0.5rem; +} + +/* Cards */ +.account-card { + cursor: pointer; + transition: all 0.2s ease; + background-color: var(--color-surface); + border: 1px solid var(--color-border); + border-radius: 0.75rem; +} + +.account-card:hover { + background-color: var(--color-surface-hover); + transform: translateY(-2px); + box-shadow: 0 4px 16px rgba(0,0,0,0.4); + border-color: var(--color-accent); +} + +/* Amounts */ +.amount-positive { + color: var(--color-income); + font-weight: 600; +} + +.amount-negative { + color: var(--color-expense); + font-weight: 600; +} + +/* Entry rows */ +.entry-row-transfer { + background-color: rgba(96, 165, 250, 0.08); +} + +.entry-deleted { + opacity: 0.5; +} + +.entry-deleted td { + text-decoration: line-through; +} + +.entry-deleted td:last-child { + text-decoration: none; +} + +/* Summary boxes */ +.summary-section { + background-color: var(--color-surface); + border: 1px solid var(--color-border); + border-radius: 0.75rem; + padding: 1rem 1.25rem; +} + +.summary-flex { + display: flex; + align-items: center; + gap: 2rem; + flex-wrap: wrap; +} + +/* Dialog */ +.dialog-backdrop { + position: fixed; + top: 0; left: 0; right: 0; bottom: 0; + background-color: rgba(0,0,0,0.6); + z-index: 1040; + display: flex; + align-items: center; + justify-content: center; + backdrop-filter: blur(4px); +} + +.dialog-content { + background: var(--color-surface); + border: 1px solid var(--color-border); + border-radius: 0.75rem; + padding: 1rem 1.25rem; + min-width: 400px; + max-width: 600px; + box-shadow: 0 8px 32px rgba(0,0,0,0.5); +} + +.dialog-content h5 { + margin-top: 0 !important; + margin-bottom: 0.5rem !important; +} + +/* Navbar */ +.app-navbar { + background-color: var(--color-surface); + border-bottom: 1px solid var(--color-border); + padding: 0.5rem 1rem; +} + +.app-navbar .navbar-brand { + color: var(--color-text); + text-decoration: none; +} + +.app-navbar .navbar-brand:hover { + color: var(--color-accent); +} + +.app-navbar .version-label { + color: var(--color-text-muted); + font-size: 0.7rem; + opacity: 0.6; +} + +/* Footer */ +.app-footer { + background-color: var(--color-surface); + border-top: 1px solid var(--color-border); + color: var(--color-text-muted); + padding: 0.4rem 1rem; + font-size: 0.75rem; + text-align: center; +} + +/* Buttons – modern style */ +.btn { + border-radius: 0.5rem; + font-weight: 500; + font-size: 0.85rem; + padding: 0.35rem 0.85rem; + display: inline-flex; + align-items: center; + gap: 0.35rem; + transition: all 0.15s ease; +} + +.btn i { + font-size: 0.9rem; +} + +.btn-primary { + background-color: var(--color-accent); + border-color: var(--color-accent); + color: #fff; +} + +.btn-primary:hover { + background-color: #6366f1; + border-color: #6366f1; +} + +.btn-success { + background-color: #22c55e; + border-color: #22c55e; + color: #fff; +} + +.btn-success:hover { + background-color: #16a34a; + border-color: #16a34a; +} + +.btn-info { + background-color: #3b82f6; + border-color: #3b82f6; + color: #fff; +} + +.btn-info:hover { + background-color: #2563eb; + border-color: #2563eb; +} + +.btn-outline-secondary { + border-color: var(--color-border); + color: var(--color-text-muted); +} + +.btn-outline-secondary:hover { + background-color: var(--color-surface-hover); + border-color: var(--color-text-muted); + color: var(--color-text); +} + +.btn-outline-danger { + border-color: rgba(248, 113, 113, 0.4); + color: var(--color-expense); +} + +.btn-outline-danger:hover { + background-color: rgba(248, 113, 113, 0.15); + border-color: var(--color-expense); + color: var(--color-expense); +} + +.btn-outline-success { + border-color: rgba(74, 222, 128, 0.4); + color: var(--color-income); +} + +.btn-outline-success:hover { + background-color: rgba(74, 222, 128, 0.15); + border-color: var(--color-income); + color: var(--color-income); +} + +.btn-dark { + background-color: #334155; + border-color: #475569; + color: var(--color-text); +} + +.btn-dark:hover { + background-color: #475569; + border-color: #64748b; +} + +/* Table */ +.table { + --bs-table-bg: transparent; + --bs-table-color: var(--color-text); + --bs-table-hover-bg: rgba(255,255,255,0.04); + border-color: var(--color-border); +} + +.table thead th { + border-bottom-color: var(--color-border); + color: var(--color-text-muted); + font-weight: 600; + font-size: 0.8rem; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.table td { + border-bottom-color: var(--color-border); +} + +/* Form controls in dark */ +.form-control, .form-select { + background-color: var(--color-bg); + border-color: var(--color-border); + color: var(--color-text); +} + +.form-control:focus, .form-select:focus { + background-color: var(--color-bg); + border-color: var(--color-accent); + color: var(--color-text); + box-shadow: 0 0 0 0.2rem rgba(129, 140, 248, 0.25); +} + +.form-label { + color: var(--color-text-muted); + font-size: 0.85rem; +} + +/* Edit button pen */ +.btn-edit-pen { + background: none; + border: none; + color: var(--color-text-muted); + padding: 0.1rem 0.3rem; + font-size: 0.9rem; + cursor: pointer; + transition: color 0.15s; +} + +.btn-edit-pen:hover { + color: var(--color-accent); +} + +/* Spinner */ +.spinner-border { + color: var(--color-accent); +} + +/* Text muted override */ +.text-muted { + color: var(--color-text-muted) !important; +} + +/* Scrollbar */ +::-webkit-scrollbar { + width: 8px; +} + +::-webkit-scrollbar-track { + background: var(--color-bg); +} + +::-webkit-scrollbar-thumb { + background: var(--color-border); + border-radius: 4px; +} + +::-webkit-scrollbar-thumb:hover { + background: #3a3a5a; +} + +/* ===== Bootstrap utility class fallbacks ===== + Ensures layout works even when Bootstrap CDN is unavailable */ + +/* Display */ +.d-flex { display: flex !important; } +.d-inline-flex { display: inline-flex !important; } +.d-block { display: block !important; } +.d-none { display: none !important; } + +/* Flex direction */ +.flex-column { flex-direction: column !important; } +.flex-row { flex-direction: row !important; } +.flex-wrap { flex-wrap: wrap !important; } +.flex-nowrap { flex-wrap: nowrap !important; } + +/* Flex sizing */ +.flex-grow-1 { flex-grow: 1 !important; } +.flex-shrink-0 { flex-shrink: 0 !important; } + +/* Justify content */ +.justify-content-start { justify-content: flex-start !important; } +.justify-content-end { justify-content: flex-end !important; } +.justify-content-center { justify-content: center !important; } +.justify-content-between { justify-content: space-between !important; } + +/* Align items */ +.align-items-start { align-items: flex-start !important; } +.align-items-center { align-items: center !important; } +.align-items-end { align-items: flex-end !important; } + +/* Gap */ +.gap-1 { gap: 0.25rem !important; } +.gap-2 { gap: 0.5rem !important; } +.gap-3 { gap: 1rem !important; } +.gap-4 { gap: 1.5rem !important; } + +/* Height / overflow */ +.vh-100 { height: 100vh !important; } +.h-100 { height: 100% !important; } +.overflow-auto { overflow: auto !important; } +.overflow-hidden { overflow: hidden !important; } + +/* Margin */ +.mb-0 { margin-bottom: 0 !important; } +.mb-1 { margin-bottom: 0.25rem !important; } +.mb-2 { margin-bottom: 0.5rem !important; } +.mb-3 { margin-bottom: 1rem !important; } +.mb-4 { margin-bottom: 1.5rem !important; } +.mt-0 { margin-top: 0 !important; } +.mt-1 { margin-top: 0.25rem !important; } +.mt-2 { margin-top: 0.5rem !important; } +.mt-3 { margin-top: 1rem !important; } +.mt-4 { margin-top: 1.5rem !important; } +.me-1 { margin-right: 0.25rem !important; } +.me-2 { margin-right: 0.5rem !important; } +.ms-auto { margin-left: auto !important; } + +/* Padding */ +.p-0 { padding: 0 !important; } +.p-1 { padding: 0.25rem !important; } +.p-2 { padding: 0.5rem !important; } +.p-3 { padding: 1rem !important; } +.px-1 { padding-left: 0.25rem !important; padding-right: 0.25rem !important; } +.px-2 { padding-left: 0.5rem !important; padding-right: 0.5rem !important; } +.px-3 { padding-left: 1rem !important; padding-right: 1rem !important; } +.py-1 { padding-top: 0.25rem !important; padding-bottom: 0.25rem !important; } +.py-2 { padding-top: 0.5rem !important; padding-bottom: 0.5rem !important; } +.py-3 { padding-top: 1rem !important; padding-bottom: 1rem !important; } +.py-4 { padding-top: 1.5rem !important; padding-bottom: 1.5rem !important; } +.py-5 { padding-top: 3rem !important; padding-bottom: 3rem !important; } + +/* Text */ +.text-center { text-align: center !important; } +.text-end { text-align: right !important; } +.text-start { text-align: left !important; } +.fw-bold { font-weight: 700 !important; } +.fw-600 { font-weight: 600 !important; } +.fs-3 { font-size: 1.75rem !important; } +.small { font-size: 0.875rem !important; } + +/* Button sizes */ +.btn-sm { + padding: 0.25rem 0.6rem !important; + font-size: 0.8rem !important; +} + +/* Container */ +.container-fluid { + width: 100%; + padding-left: 1rem; + padding-right: 1rem; +} + +/* Spinner (Bootstrap component fallback) */ +.spinner-border { + display: inline-block; + width: 2rem; + height: 2rem; + border: 0.25em solid var(--color-accent); + border-right-color: transparent; + border-radius: 50%; + animation: spinner-border 0.75s linear infinite; +} +@keyframes spinner-border { + to { transform: rotate(360deg); } +} + +/* ===== Scrollable entry table ===== */ +.entries-scroll-wrapper { + overflow-x: auto; + overflow-y: auto; + flex: 1; + min-height: 0; +} + +/* ===== Modern form inputs ===== */ +.form-control[type="number"], +.dialog-content input[type="number"] { + text-align: right; +} + +.form-control, .form-select { + border-radius: 0.5rem; + transition: border-color 0.15s, box-shadow 0.15s; +} + +.form-label { + font-size: 0.78rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.04em; + color: var(--color-text-muted); + margin-bottom: 0.3rem; +} + diff --git a/src/Duempelkas.Desktop/wwwroot/index.html b/src/Duempelkas.Desktop/wwwroot/index.html new file mode 100644 index 0000000..15e7fcf --- /dev/null +++ b/src/Duempelkas.Desktop/wwwroot/index.html @@ -0,0 +1,27 @@ + + + + + + + Dümpelkas – Kassenbuch + + + + + + Laden... + + + Ein unbehandelter Fehler ist aufgetreten. + Neu laden + × + + + + +