price per item is printed
This commit is contained in:
@@ -46,3 +46,5 @@ app.*.map.json
|
||||
|
||||
# Widget Preview related
|
||||
.widget_preview/
|
||||
# Exclusive lock used by scripts/build_release.ps1.
|
||||
.release-build.lock
|
||||
|
||||
@@ -2,6 +2,42 @@
|
||||
|
||||
A new Flutter project.
|
||||
|
||||
## Kleine Fenster
|
||||
|
||||
Windows und Linux verwenden eine minimale Inhaltsgröße von 320 × 480 logischen
|
||||
Pixeln (ohne Fensterrahmen). In schmalen Ansichten stehen die Hauptbereiche
|
||||
untereinander. Unter 320 Pixeln Browserbreite lässt sich die App horizontal
|
||||
scrollen; die Seite und Formulareingaben bleiben beim Verkleinern erhalten.
|
||||
|
||||
## Release-Builds mit automatischer Versionierung
|
||||
|
||||
Im Projektordner mit PowerShell ausführen:
|
||||
|
||||
```powershell
|
||||
.\build_release.cmd
|
||||
```
|
||||
|
||||
Erstellt standardmäßig eine Android-APK unter
|
||||
`build/app/outputs/flutter-apk/app-release.apk`. Vor dem Build werden Patchversion
|
||||
und Buildnummer erhöht, z. B. `1.0.0+1` → `1.0.1+2`. Ein erfolgreicher Build behält
|
||||
diese Version in `pubspec.yaml`; bei einem Fehler wird die bisherige Version
|
||||
wiederhergestellt. Die installierte Version und Buildnummer stehen im Hamburger-Menü.
|
||||
|
||||
Weitere Ziele und zusätzliche Flutter-Argumente:
|
||||
|
||||
```powershell
|
||||
.\build_release.cmd -Target windows
|
||||
.\build_release.cmd -Target appbundle
|
||||
.\build_release.cmd -FlutterArguments --split-per-abi
|
||||
```
|
||||
|
||||
Für automatische Versionierung immer dieses Skript verwenden. Direkte Aufrufe
|
||||
von `flutter build` und Debug-Builds erhöhen die Version nicht. Änderungen an
|
||||
`pubspec.yaml` und gegebenenfalls `pubspec.lock` nach dem Release ins Repository übernehmen.
|
||||
|
||||
Der CMD-Starter erlaubt PowerShell-Skripte nur für diesen einen Prozess; die
|
||||
systemweite Ausführungsrichtlinie bleibt unverändert.
|
||||
|
||||
## Getting Started
|
||||
|
||||
This project is a starting point for a Flutter application.
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
@echo off
|
||||
powershell.exe -NoProfile -ExecutionPolicy Bypass -File "%~dp0scripts\build_release.ps1" %*
|
||||
exit /b %errorlevel%
|
||||
@@ -77,6 +77,9 @@ class AnwendungController extends GetxController {
|
||||
entry.toJson(),
|
||||
]);
|
||||
anwendungen.assignAll([entry]);
|
||||
if (Get.isRegistered<HomeController>()) {
|
||||
Get.find<HomeController>().loadProfile();
|
||||
}
|
||||
hasError.value = false;
|
||||
message.value = 'Verarbeitungsprofil gespeichert.';
|
||||
} catch (_) {
|
||||
|
||||
@@ -31,6 +31,7 @@ class BauteilController extends GetxController {
|
||||
final minutenController = TextEditingController(text: '0');
|
||||
final gewichtController = TextEditingController();
|
||||
final abfallController = TextEditingController(text: '0');
|
||||
final gesamtpreisController = TextEditingController();
|
||||
final _verbrauchControllers = <String, TextEditingController>{};
|
||||
final filamente = <KaufFilamentModel>[].obs;
|
||||
final profile = <AnwendungModel>[].obs;
|
||||
@@ -224,6 +225,7 @@ class BauteilController extends GetxController {
|
||||
druckdauerMinuten: duration,
|
||||
gewichtGramm: InputValidators.number(gewichtController.text)!,
|
||||
abfallGramm: InputValidators.number(abfallController.text)!,
|
||||
gesamtpreis: InputValidators.number(gesamtpreisController.text),
|
||||
fotoBase64: foto.value == null ? null : base64Encode(foto.value!),
|
||||
filamente: selectedFilamente.entries
|
||||
.map(
|
||||
@@ -268,6 +270,8 @@ class BauteilController extends GetxController {
|
||||
minutenController.text = '${entry.druckdauerMinuten % 60}';
|
||||
gewichtController.text = DisplayFormats.number(entry.gewichtGramm);
|
||||
abfallController.text = DisplayFormats.number(entry.abfallGramm);
|
||||
gesamtpreisController.text =
|
||||
entry.gesamtpreis?.toString().replaceAll('.', ',') ?? '';
|
||||
foto.value = entry.fotoBytes;
|
||||
for (final item in entry.filamente) {
|
||||
verbrauchController(item.filamentId).text = item.verbrauchGramm > 0
|
||||
@@ -342,6 +346,7 @@ class BauteilController extends GetxController {
|
||||
minutenController.text = '0';
|
||||
gewichtController.clear();
|
||||
abfallController.text = '0';
|
||||
gesamtpreisController.clear();
|
||||
selectedFilamente.clear();
|
||||
for (final controller in _verbrauchControllers.values) {
|
||||
controller.clear();
|
||||
@@ -359,6 +364,7 @@ class BauteilController extends GetxController {
|
||||
minutenController.dispose();
|
||||
gewichtController.dispose();
|
||||
abfallController.dispose();
|
||||
gesamtpreisController.dispose();
|
||||
for (final controller in _verbrauchControllers.values) {
|
||||
controller.dispose();
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import 'package:uuid/uuid.dart';
|
||||
|
||||
import '../models/kauf_filament_model.dart';
|
||||
import '../models/bauteil_model.dart';
|
||||
import '../models/anwendung_model.dart';
|
||||
import '../helper/storage_keys.dart';
|
||||
import '../helper/filament_bestand.dart';
|
||||
import '../helper/display_formats.dart';
|
||||
@@ -18,6 +19,16 @@ class HomeController extends GetxController {
|
||||
|
||||
final filamente = <KaufFilamentModel>[].obs;
|
||||
final verbrauch = <String, double>{}.obs;
|
||||
final filamentIdsMitProfil = <String>{}.obs;
|
||||
|
||||
void loadProfile() {
|
||||
filamentIdsMitProfil.assignAll(
|
||||
storage
|
||||
.getModelList(StorageKeys.anwendungen)
|
||||
.map(AnwendungModel.fromJson)
|
||||
.map((profile) => profile.filamentId),
|
||||
);
|
||||
}
|
||||
|
||||
void loadVerbrauch() {
|
||||
verbrauch.assignAll(
|
||||
@@ -133,6 +144,7 @@ class HomeController extends GetxController {
|
||||
|
||||
void loadFilamente() {
|
||||
loadVerbrauch();
|
||||
loadProfile();
|
||||
final data = storage.getModelList(storageKey);
|
||||
|
||||
filamente.assignAll(data.map(KaufFilamentModel.fromJson));
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
class DisplayFormats {
|
||||
static String currency(num value) =>
|
||||
NumberFormat.currency(locale: 'de_AT', symbol: '€').format(value);
|
||||
static String number(num value) =>
|
||||
NumberFormat('0.##', 'de_AT').format(value);
|
||||
static String date(DateTime value) =>
|
||||
|
||||
@@ -9,6 +9,7 @@ import 'package:get_storage/get_storage.dart';
|
||||
|
||||
import 'services/get_storage_services.dart';
|
||||
import 'theme/app_theme.dart';
|
||||
import 'widgets/responsive_viewport.dart';
|
||||
|
||||
void main() async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
@@ -26,6 +27,8 @@ class MyApp extends StatelessWidget {
|
||||
title: 'Flutter Filament Verwaltung',
|
||||
debugShowCheckedModeBanner: false,
|
||||
theme: AppTheme.light,
|
||||
builder: (context, child) =>
|
||||
ResponsiveViewport(child: child ?? const SizedBox.shrink()),
|
||||
initialBinding: SampleBindings(),
|
||||
initialRoute: HomeView.routeName,
|
||||
getPages: SampleRouts.samplePages,
|
||||
|
||||
@@ -71,6 +71,7 @@ class BackupModel {
|
||||
item.druckdauerMinuten <= 0 ||
|
||||
!_positive(item.gewichtGramm) ||
|
||||
!_nonNegative(item.abfallGramm) ||
|
||||
(item.gesamtpreis != null && !_nonNegative(item.gesamtpreis!)) ||
|
||||
item.filamente.isEmpty ||
|
||||
item.filamente.any(
|
||||
(entry) =>
|
||||
|
||||
@@ -34,6 +34,7 @@ class BauteilModel {
|
||||
final int druckdauerMinuten;
|
||||
final double gewichtGramm;
|
||||
final double abfallGramm;
|
||||
final double? gesamtpreis;
|
||||
final List<BauteilFilamentModel> filamente;
|
||||
final String? fotoBase64;
|
||||
|
||||
@@ -46,6 +47,7 @@ class BauteilModel {
|
||||
required this.gewichtGramm,
|
||||
required this.abfallGramm,
|
||||
required this.filamente,
|
||||
this.gesamtpreis,
|
||||
this.fotoBase64,
|
||||
});
|
||||
|
||||
@@ -71,6 +73,7 @@ class BauteilModel {
|
||||
druckdauerMinuten: druckdauerMinuten,
|
||||
gewichtGramm: gewichtGramm,
|
||||
abfallGramm: abfallGramm,
|
||||
gesamtpreis: gesamtpreis,
|
||||
filamente: filamente,
|
||||
fotoBase64: fotoBase64,
|
||||
);
|
||||
@@ -83,6 +86,7 @@ class BauteilModel {
|
||||
'druckdauerMinuten': druckdauerMinuten,
|
||||
'gewichtGramm': gewichtGramm,
|
||||
'abfallGramm': abfallGramm,
|
||||
'gesamtpreis': gesamtpreis,
|
||||
'filamente': filamente.map((item) => item.toJson()).toList(),
|
||||
'fotoBase64': fotoBase64,
|
||||
};
|
||||
@@ -95,6 +99,7 @@ class BauteilModel {
|
||||
druckdauerMinuten: json['druckdauerMinuten'] as int,
|
||||
gewichtGramm: (json['gewichtGramm'] as num).toDouble(),
|
||||
abfallGramm: (json['abfallGramm'] as num).toDouble(),
|
||||
gesamtpreis: (json['gesamtpreis'] as num?)?.toDouble(),
|
||||
fotoBase64: json['fotoBase64'] as String?,
|
||||
filamente: (json['filamente'] as List)
|
||||
.map(
|
||||
|
||||
@@ -6,6 +6,7 @@ import '../models/kauf_filament_model.dart';
|
||||
import '../theme/app_theme.dart';
|
||||
import '../widgets/filament_form.dart';
|
||||
import '../widgets/filament_inventory.dart';
|
||||
import '../widgets/app_version.dart';
|
||||
import 'anwendung_view.dart';
|
||||
import 'bauteil_view.dart';
|
||||
import 'export_view.dart';
|
||||
@@ -27,25 +28,37 @@ class HomeView extends GetView<HomeController> {
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
],
|
||||
title: const Row(
|
||||
title: LayoutBuilder(
|
||||
builder: (context, constraints) => Row(
|
||||
children: [
|
||||
Icon(Icons.all_inbox_rounded, color: AppTheme.teal, size: 28),
|
||||
SizedBox(width: 12),
|
||||
Flexible(
|
||||
if (constraints.maxWidth >= 260) ...[
|
||||
const Icon(
|
||||
Icons.all_inbox_rounded,
|
||||
color: AppTheme.teal,
|
||||
size: 28,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
],
|
||||
const Flexible(
|
||||
child: Text(
|
||||
'Filamentverwaltung',
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(fontSize: 21, fontWeight: FontWeight.w700),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
drawer: Drawer(
|
||||
child: SafeArea(
|
||||
child: ListView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
children: [
|
||||
const ListTile(title: Text('Filamentverwaltung')),
|
||||
const AppVersion(),
|
||||
const Divider(),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.import_export),
|
||||
title: const Text('Daten exportieren / importieren'),
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:package_info_plus/package_info_plus.dart';
|
||||
|
||||
class AppVersion extends StatefulWidget {
|
||||
const AppVersion({super.key});
|
||||
|
||||
@override
|
||||
State<AppVersion> createState() => _AppVersionState();
|
||||
}
|
||||
|
||||
class _AppVersionState extends State<AppVersion> {
|
||||
late final Future<PackageInfo> _info = PackageInfo.fromPlatform();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => FutureBuilder<PackageInfo>(
|
||||
future: _info,
|
||||
builder: (context, snapshot) {
|
||||
final info = snapshot.data;
|
||||
return ListTile(
|
||||
leading: const Icon(Icons.info_outline),
|
||||
title: Text(info == null ? 'Version' : 'Version ${info.version}'),
|
||||
subtitle: Text(
|
||||
info == null
|
||||
? (snapshot.hasError ? 'Nicht verfügbar' : 'Wird geladen …')
|
||||
: 'Build ${info.buildNumber}',
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -102,6 +102,10 @@ class BauteilCard extends StatelessWidget {
|
||||
runSpacing: 8,
|
||||
children: [
|
||||
_detail(DisplayFormats.duration(bauteil.druckdauerMinuten)),
|
||||
if (bauteil.gesamtpreis != null)
|
||||
_detail(
|
||||
'Gesamtpreis ${DisplayFormats.currency(bauteil.gesamtpreis!)}',
|
||||
),
|
||||
_detail(
|
||||
'Bauteil ${DisplayFormats.number(bauteil.gewichtGramm)} g',
|
||||
),
|
||||
|
||||
@@ -64,6 +64,13 @@ class BauteilForm extends StatelessWidget {
|
||||
unit: 'g',
|
||||
validator: InputValidators.nonNegative,
|
||||
),
|
||||
EntryNumberField(
|
||||
controller: controller.gesamtpreisController,
|
||||
label: 'Gesamtpreis (optional)',
|
||||
unit: '€',
|
||||
validator: (value) =>
|
||||
InputValidators.nonNegative(value, optional: true),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
@@ -9,14 +9,10 @@ class BauteilPhotoPicker extends StatelessWidget {
|
||||
final BauteilController controller;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Obx(
|
||||
() => Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
BauteilPhoto(bytes: controller.foto.value),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
Widget build(BuildContext context) => LayoutBuilder(
|
||||
builder: (context, constraints) => Obx(() {
|
||||
final photo = BauteilPhoto(bytes: controller.foto.value);
|
||||
final controls = Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
@@ -30,8 +26,7 @@ class BauteilPhotoPicker extends StatelessWidget {
|
||||
children: [
|
||||
OutlinedButton.icon(
|
||||
onPressed:
|
||||
controller.isPickingPhoto.value ||
|
||||
controller.isSaving.value
|
||||
controller.isPickingPhoto.value || controller.isSaving.value
|
||||
? null
|
||||
: controller.pickPhoto,
|
||||
icon: const Icon(Icons.add_photo_alternate_outlined),
|
||||
@@ -51,9 +46,21 @@ class BauteilPhotoPicker extends StatelessWidget {
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
if (constraints.maxWidth < 400) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [photo, const SizedBox(height: 12), controls],
|
||||
);
|
||||
}
|
||||
return Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
photo,
|
||||
const SizedBox(width: 16),
|
||||
Expanded(child: controls),
|
||||
],
|
||||
),
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -12,12 +12,14 @@ class FilamentCard extends StatelessWidget {
|
||||
required this.onTap,
|
||||
required this.onAction,
|
||||
this.verbrauchGramm = 0,
|
||||
this.hatProfil = false,
|
||||
});
|
||||
|
||||
final KaufFilamentModel filament;
|
||||
final VoidCallback onTap;
|
||||
final ValueChanged<String> onAction;
|
||||
final double verbrauchGramm;
|
||||
final bool hatProfil;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -155,6 +157,11 @@ class FilamentCard extends StatelessWidget {
|
||||
icon: Icons.layers_outlined,
|
||||
),
|
||||
_DetailPill(label: weight, icon: Icons.scale_outlined),
|
||||
if (hatProfil)
|
||||
const _DetailPill(
|
||||
label: 'Profil erstellt',
|
||||
icon: Icons.check_circle_outline,
|
||||
),
|
||||
],
|
||||
),
|
||||
const Padding(
|
||||
|
||||
@@ -32,6 +32,7 @@ class FilamentInventory extends StatelessWidget {
|
||||
sliver: Obx(() {
|
||||
final filamente = controller.filamente.toList();
|
||||
final verbrauch = Map<String, double>.from(controller.verbrauch);
|
||||
final filamentIdsMitProfil = controller.filamentIdsMitProfil.toSet();
|
||||
final total = filamente.fold<double>(
|
||||
0,
|
||||
(sum, item) => sum + item.preis,
|
||||
@@ -144,6 +145,9 @@ class FilamentInventory extends StatelessWidget {
|
||||
key: ValueKey(filament.filamentId),
|
||||
filament: filament,
|
||||
verbrauchGramm: verbrauch[filament.filamentId] ?? 0,
|
||||
hatProfil: filamentIdsMitProfil.contains(
|
||||
filament.filamentId,
|
||||
),
|
||||
onTap: () => onOpen(filament),
|
||||
onAction: (action) => onAction(action, filament),
|
||||
);
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// Keeps the navigator (including dialogs) usable in very narrow browser tabs.
|
||||
/// The native Windows/Linux runners use the same minimum client width.
|
||||
class ResponsiveViewport extends StatelessWidget {
|
||||
const ResponsiveViewport({super.key, required this.child});
|
||||
|
||||
static const minimumWidth = 320.0;
|
||||
final Widget child;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
if (constraints.maxWidth >= minimumWidth) return child;
|
||||
final media = MediaQuery.of(context);
|
||||
return SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
primary: false,
|
||||
child: SizedBox(
|
||||
width: minimumWidth,
|
||||
height: constraints.maxHeight,
|
||||
child: MediaQuery(
|
||||
data: media.copyWith(size: Size(minimumWidth, media.size.height)),
|
||||
child: child,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -59,6 +59,8 @@ static void my_application_activate(GApplication* application) {
|
||||
project, self->dart_entrypoint_arguments);
|
||||
|
||||
FlView* view = fl_view_new(project);
|
||||
// Match ResponsiveViewport.minimumWidth; GTK adds window decorations.
|
||||
gtk_widget_set_size_request(GTK_WIDGET(view), 320, 480);
|
||||
GdkRGBA background_color;
|
||||
// Background defaults to black, override it here if necessary, e.g. #00000000
|
||||
// for transparent.
|
||||
|
||||
@@ -7,8 +7,10 @@ import Foundation
|
||||
|
||||
import file_picker_darwin
|
||||
import file_selector_macos
|
||||
import package_info_plus
|
||||
|
||||
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||
FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin"))
|
||||
FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin"))
|
||||
FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin"))
|
||||
}
|
||||
|
||||
@@ -493,6 +493,22 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.0"
|
||||
package_info_plus:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: package_info_plus
|
||||
sha256: "73ef2cd5c11a7548f9b7f6fc2a4f6113de2e64aa2a70de63feede95548822908"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "10.2.2"
|
||||
package_info_plus_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: package_info_plus_platform_interface
|
||||
sha256: db762cb2f4f25ee60fb6359773861b0f199e00b90d237bd85a76a1e806b46ef4
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.1.0"
|
||||
path:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
||||
+2
-1
@@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
|
||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||
# In Windows, build-name is used as the major, minor, and patch parts
|
||||
# of the product and file versions while build-number is used as the build suffix.
|
||||
version: 1.0.0+1
|
||||
version: 1.0.2+3
|
||||
|
||||
environment:
|
||||
sdk: ^3.13.0
|
||||
@@ -41,6 +41,7 @@ dependencies:
|
||||
uuid: ^4.6.0
|
||||
file_selector: ^1.1.0
|
||||
file_picker: ^13.3.0
|
||||
package_info_plus: ^10.2.2
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
param(
|
||||
[ValidateSet('apk', 'appbundle', 'windows', 'web', 'ios', 'ipa', 'macos', 'linux')]
|
||||
[string]$Target = 'apk',
|
||||
[string]$FlutterCommand = 'flutter',
|
||||
[string[]]$FlutterArguments = @()
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$projectRoot = Split-Path -Parent $PSScriptRoot
|
||||
$pubspecPath = Join-Path $projectRoot 'pubspec.yaml'
|
||||
$lockPath = Join-Path $projectRoot '.release-build.lock'
|
||||
$buildLock = $null
|
||||
$originalBytes = $null
|
||||
$updated = $false
|
||||
$succeeded = $false
|
||||
|
||||
try {
|
||||
# Hold an exclusive lock; the OS releases it even if PowerShell is closed.
|
||||
$buildLock = [System.IO.File]::Open(
|
||||
$lockPath, [System.IO.FileMode]::OpenOrCreate,
|
||||
[System.IO.FileAccess]::ReadWrite, [System.IO.FileShare]::None
|
||||
)
|
||||
foreach ($argument in $FlutterArguments) {
|
||||
if ($argument -match '^--(build-name|build-number|debug|profile|config-only)(=|$)') {
|
||||
throw "Argument $argument ist im versionierten Release-Build nicht erlaubt."
|
||||
}
|
||||
}
|
||||
$originalBytes = [System.IO.File]::ReadAllBytes($pubspecPath)
|
||||
$source = [System.Text.Encoding]::UTF8.GetString($originalBytes)
|
||||
$versionMatch = [regex]::Match($source, '(?m)^version:\s*(\d+)\.(\d+)\.(\d+)\+(\d+)[ \t]*\r?$')
|
||||
if (-not $versionMatch.Success) {
|
||||
throw 'In pubspec.yaml wird eine Version wie 1.0.0+1 erwartet.'
|
||||
}
|
||||
$patch = [long]::Parse($versionMatch.Groups[3].Value) + 1
|
||||
$build = [long]::Parse($versionMatch.Groups[4].Value) + 1
|
||||
$nextVersion = '{0}.{1}.{2}+{3}' -f $versionMatch.Groups[1].Value, $versionMatch.Groups[2].Value, $patch, $build
|
||||
$replacement = "version: $nextVersion"
|
||||
if ($versionMatch.Value.EndsWith("`r")) { $replacement += "`r" }
|
||||
$nextSource = $source.Remove($versionMatch.Index, $versionMatch.Length).Insert($versionMatch.Index, $replacement)
|
||||
[System.IO.File]::WriteAllText($pubspecPath, $nextSource, [System.Text.UTF8Encoding]::new($false))
|
||||
$updated = $true
|
||||
|
||||
Push-Location $projectRoot
|
||||
try {
|
||||
Write-Host "Release $nextVersion ($Target) wird erstellt ..."
|
||||
$global:LASTEXITCODE = 0
|
||||
& $FlutterCommand build $Target --release @FlutterArguments
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "Flutter-Build fehlgeschlagen (Exitcode $LASTEXITCODE)."
|
||||
}
|
||||
$succeeded = $true
|
||||
Write-Host "Release $nextVersion erfolgreich. Die Version wurde in pubspec.yaml gespeichert."
|
||||
} finally {
|
||||
Pop-Location
|
||||
}
|
||||
} finally {
|
||||
if ($updated -and -not $succeeded) {
|
||||
[System.IO.File]::WriteAllBytes($pubspecPath, $originalBytes)
|
||||
Write-Host 'Build nicht erfolgreich: Bisherige Version wiederhergestellt.'
|
||||
}
|
||||
if ($null -ne $buildLock) { $buildLock.Dispose() }
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$projectRoot = Split-Path -Parent $PSScriptRoot
|
||||
$testRoot = Join-Path $projectRoot ('build/release-script-test-' + [guid]::NewGuid().ToString('N'))
|
||||
$testScripts = Join-Path $testRoot 'scripts'
|
||||
New-Item -ItemType Directory -Path $testScripts -Force | Out-Null
|
||||
Copy-Item -LiteralPath (Join-Path $PSScriptRoot 'build_release.ps1') -Destination $testScripts
|
||||
$pubspecPath = Join-Path $testRoot 'pubspec.yaml'
|
||||
$original = "name: test_app`r`nversion: 1.2.9+41`r`n"
|
||||
[System.IO.File]::WriteAllText($pubspecPath, $original)
|
||||
$fakeFlutter = Join-Path $testRoot 'fake_flutter.ps1'
|
||||
@'
|
||||
param([Parameter(ValueFromRemainingArguments = $true)][string[]]$BuildArguments)
|
||||
if (($BuildArguments -join ' ') -notmatch '^build (apk|windows) --release') {
|
||||
throw 'Unexpected Flutter arguments.'
|
||||
}
|
||||
Copy-Item -LiteralPath './pubspec.yaml' -Destination './built-version.yaml' -Force
|
||||
if (Test-Path './fail-build') { $global:LASTEXITCODE = 1 } else { $global:LASTEXITCODE = 0 }
|
||||
'@ | Set-Content -LiteralPath $fakeFlutter
|
||||
$releaseScript = Join-Path $testScripts 'build_release.ps1'
|
||||
|
||||
function Assert-Version([string]$Expected) {
|
||||
$pubspec = [System.IO.File]::ReadAllText($pubspecPath)
|
||||
if ($pubspec -ne "name: test_app`r`nversion: $Expected`r`n") {
|
||||
throw "Unexpected pubspec: $pubspec"
|
||||
}
|
||||
}
|
||||
|
||||
& $releaseScript -FlutterCommand $fakeFlutter
|
||||
Assert-Version '1.2.10+42'
|
||||
if ([System.IO.File]::ReadAllText((Join-Path $testRoot 'built-version.yaml')) -notmatch '1\.2\.10\+42') {
|
||||
throw 'The build did not receive the new version.'
|
||||
}
|
||||
& $releaseScript -Target windows -FlutterCommand $fakeFlutter
|
||||
Assert-Version '1.2.11+43'
|
||||
|
||||
New-Item -ItemType File -Path (Join-Path $testRoot 'fail-build') | Out-Null
|
||||
$failureCaught = $false
|
||||
try { & $releaseScript -FlutterCommand $fakeFlutter } catch { $failureCaught = $true }
|
||||
if (-not $failureCaught) { throw 'Build failure was not reported.' }
|
||||
Assert-Version '1.2.11+43'
|
||||
|
||||
$invalidCaught = $false
|
||||
try {
|
||||
& $releaseScript -FlutterCommand $fakeFlutter -FlutterArguments '--build-number=99'
|
||||
} catch { $invalidCaught = $true }
|
||||
if (-not $invalidCaught) { throw 'Manual version override was accepted.' }
|
||||
Assert-Version '1.2.11+43'
|
||||
|
||||
$lock = [System.IO.File]::Open(
|
||||
(Join-Path $testRoot '.release-build.lock'),
|
||||
[System.IO.FileMode]::OpenOrCreate, [System.IO.FileAccess]::ReadWrite,
|
||||
[System.IO.FileShare]::None
|
||||
)
|
||||
$lockCaught = $false
|
||||
try {
|
||||
try { & $releaseScript -FlutterCommand $fakeFlutter } catch { $lockCaught = $true }
|
||||
} finally { $lock.Dispose() }
|
||||
if (-not $lockCaught) { throw 'Concurrent release was not blocked.' }
|
||||
Assert-Version '1.2.11+43'
|
||||
Write-Host 'All release-script checks passed (success, repeat build, rollback, overrides, lock).'
|
||||
@@ -236,6 +236,53 @@ void main() {
|
||||
expect(find.textContaining('nicht gefunden'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('Inventory marks saved profiles and updates after creating one', (
|
||||
tester,
|
||||
) async {
|
||||
tester.view.physicalSize = const Size(1280, 1400);
|
||||
tester.view.devicePixelRatio = 1;
|
||||
addTearDown(tester.view.resetPhysicalSize);
|
||||
addTearDown(tester.view.resetDevicePixelRatio);
|
||||
await tester.pumpWidget(
|
||||
GetMaterialApp(
|
||||
theme: AppTheme.light,
|
||||
initialRoute: HomeView.routeName,
|
||||
getPages: SampleRouts.samplePages,
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
final home = Get.find<HomeController>();
|
||||
Finder badgeFor(String id) => find.descendant(
|
||||
of: find.byKey(ValueKey(id)),
|
||||
matching: find.text('Profil erstellt'),
|
||||
);
|
||||
expect(badgeFor('a'), findsNothing);
|
||||
expect(badgeFor('b'), findsOneWidget);
|
||||
|
||||
await tester.tap(find.byKey(const ValueKey('a')));
|
||||
await tester.pumpAndSettle();
|
||||
final profileController = Get.find<AnwendungController>();
|
||||
profileController.druckController.text = '220';
|
||||
profileController.druckbettController.text = '60';
|
||||
storage.failWrites = true;
|
||||
await profileController.saveAnwendung();
|
||||
expect(home.filamentIdsMitProfil, isNot(contains('a')));
|
||||
|
||||
storage.failWrites = false;
|
||||
await profileController.saveAnwendung();
|
||||
Get.back<void>();
|
||||
await tester.pumpAndSettle();
|
||||
expect(badgeFor('a'), findsOneWidget);
|
||||
expect(badgeFor('b'), findsOneWidget);
|
||||
|
||||
// Reloads after imports must also clear badges for removed profiles.
|
||||
await storage.saveModelList(AnwendungController.storageKey, []);
|
||||
home.loadFilamente();
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('Profil erstellt'), findsNothing);
|
||||
expect(tester.takeException(), isNull);
|
||||
});
|
||||
|
||||
testWidgets(
|
||||
'Multicolor part persists both filaments and matching processing profile',
|
||||
(tester) async {
|
||||
|
||||
@@ -7,6 +7,7 @@ import 'package:get/get.dart';
|
||||
|
||||
import 'package:ertl_filament_verwaltung/controller/bauteil_controller.dart';
|
||||
import 'package:ertl_filament_verwaltung/controller/home_controller.dart';
|
||||
import 'package:ertl_filament_verwaltung/helper/display_formats.dart';
|
||||
import 'package:ertl_filament_verwaltung/models/bauteil_model.dart';
|
||||
import 'package:ertl_filament_verwaltung/models/kauf_filament_model.dart';
|
||||
import 'package:ertl_filament_verwaltung/pages/bauteil_view.dart';
|
||||
@@ -143,6 +144,7 @@ void main() {
|
||||
});
|
||||
expect(legacy.filamente.single.verbrauchGramm, 0);
|
||||
expect(legacy.fotoBytes, isNull);
|
||||
expect(legacy.gesamtpreis, isNull);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -151,6 +153,7 @@ void main() {
|
||||
(tester) async {
|
||||
await show(tester);
|
||||
fill();
|
||||
controller.gesamtpreisController.text = '12,50';
|
||||
photos.result = png;
|
||||
await controller.pickPhoto();
|
||||
await controller.saveBauteil();
|
||||
@@ -158,9 +161,19 @@ void main() {
|
||||
expect(remaining('b'), 960);
|
||||
final originalId = controller.bauteile.single.bauteilId;
|
||||
final originalDate = controller.bauteile.single.erstelltAm;
|
||||
expect(controller.bauteile.single.gesamtpreis, 12.5);
|
||||
expect(
|
||||
storage
|
||||
.getModelList(BauteilController.storageKey)
|
||||
.single['gesamtpreis'],
|
||||
12.5,
|
||||
);
|
||||
expect(controller.gesamtpreisController.text, isEmpty);
|
||||
|
||||
await edit(tester, controller.bauteile.single);
|
||||
expect(controller.foto.value, png);
|
||||
expect(controller.gesamtpreisController.text, '12,5');
|
||||
controller.gesamtpreisController.text = '18,90';
|
||||
expect(controller.verfuegbarGramm('a'), 1000);
|
||||
controller.verbrauchController('a').text = '20';
|
||||
controller.verbrauchController('b').text = '80';
|
||||
@@ -180,6 +193,10 @@ void main() {
|
||||
controller.bauteile.every((item) => item.fotoBase64 != null),
|
||||
isTrue,
|
||||
);
|
||||
expect(
|
||||
controller.bauteile.every((item) => item.gesamtpreis == 18.9),
|
||||
isTrue,
|
||||
);
|
||||
expect(remaining('a'), 960);
|
||||
expect(remaining('b'), 840);
|
||||
await controller.deleteBauteil(originalId);
|
||||
@@ -190,6 +207,7 @@ void main() {
|
||||
expect(remaining('a'), 980);
|
||||
final reloaded = Get.put(BauteilController(), tag: 'reload');
|
||||
expect(reloaded.bauteile.single.fotoBytes, png);
|
||||
expect(reloaded.bauteile.single.gesamtpreis, 18.9);
|
||||
expect(reloaded.bauteile.single.filamente.last.verbrauchGramm, 80);
|
||||
// Purchases retain the original weight; the persisted ledger is deducted.
|
||||
expect(
|
||||
@@ -218,6 +236,32 @@ void main() {
|
||||
},
|
||||
);
|
||||
|
||||
testWidgets(
|
||||
'Total price rejects invalid amounts, accepts zero and can be cleared',
|
||||
(tester) async {
|
||||
await show(tester);
|
||||
fill();
|
||||
for (final value in ['-1', 'NaN', 'Infinity', 'abc']) {
|
||||
controller.gesamtpreisController.text = value;
|
||||
await controller.saveBauteil();
|
||||
expect(storage.getModelList(BauteilController.storageKey), isEmpty);
|
||||
}
|
||||
controller.gesamtpreisController.text = '0';
|
||||
await controller.saveBauteil();
|
||||
expect(controller.bauteile.single.gesamtpreis, 0);
|
||||
await edit(tester, controller.bauteile.single);
|
||||
controller.gesamtpreisController.clear();
|
||||
await controller.saveBauteil();
|
||||
expect(controller.bauteile.single.gesamtpreis, isNull);
|
||||
expect(
|
||||
storage
|
||||
.getModelList(BauteilController.storageKey)
|
||||
.single['gesamtpreis'],
|
||||
isNull,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
testWidgets(
|
||||
'Editing credits only the edited booking; duplicates need new stock',
|
||||
(tester) async {
|
||||
@@ -310,6 +354,7 @@ void main() {
|
||||
(tester) async {
|
||||
await show(tester, narrow: true);
|
||||
fill();
|
||||
controller.gesamtpreisController.text = '12,50';
|
||||
photos.result = png;
|
||||
await controller.pickPhoto();
|
||||
await controller.saveBauteil();
|
||||
@@ -320,6 +365,10 @@ void main() {
|
||||
scrollable: find.byType(Scrollable).first,
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
expect(
|
||||
find.text('Gesamtpreis ${DisplayFormats.currency(12.5)}'),
|
||||
findsOneWidget,
|
||||
);
|
||||
expect(
|
||||
find.descendant(
|
||||
of: find.byType(BauteilCard),
|
||||
|
||||
@@ -88,6 +88,7 @@ void main() {
|
||||
druckdauerMinuten: 60,
|
||||
gewichtGramm: 40,
|
||||
abfallGramm: 10,
|
||||
gesamtpreis: 12.5,
|
||||
filamente: [
|
||||
const BauteilFilamentModel(
|
||||
filamentId: 'f1',
|
||||
@@ -129,6 +130,7 @@ void main() {
|
||||
await controller.importData();
|
||||
expect(storage.tables, fixture());
|
||||
expect(home.materialien, ['PA12', 'PLA']);
|
||||
expect(home.filamentIdsMitProfil, contains('f1'));
|
||||
expect(home.restgewicht(home.filamente.single), 950);
|
||||
expect(controller.hasError.value, isFalse);
|
||||
},
|
||||
@@ -174,6 +176,23 @@ void main() {
|
||||
expect(storage.tables, fixture());
|
||||
});
|
||||
|
||||
test('Backup imports accept legacy parts without price and reject negative prices', () {
|
||||
final legacy = fixture();
|
||||
legacy[StorageKeys.bauteile]!.single.remove('gesamtpreis');
|
||||
final decoded = BackupModel.decode(BackupModel(legacy).encode());
|
||||
expect(
|
||||
BauteilModel.fromJson(decoded.data[StorageKeys.bauteile]!.single)
|
||||
.gesamtpreis,
|
||||
isNull,
|
||||
);
|
||||
final invalid = fixture();
|
||||
invalid[StorageKeys.bauteile]!.single['gesamtpreis'] = -1;
|
||||
expect(
|
||||
() => BackupModel.decode(BackupModel(invalid).encode()),
|
||||
throwsFormatException,
|
||||
);
|
||||
});
|
||||
|
||||
test('A failed import restores previously written tables', () async {
|
||||
storage.tables.addAll(fixture());
|
||||
files.source = BackupModel({for (final key in BackupModel.keys) key: []})
|
||||
|
||||
@@ -0,0 +1,136 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
import 'package:ertl_filament_verwaltung/controller/anwendung_controller.dart';
|
||||
import 'package:ertl_filament_verwaltung/controller/bauteil_controller.dart';
|
||||
import 'package:ertl_filament_verwaltung/controller/home_controller.dart';
|
||||
import 'package:ertl_filament_verwaltung/helper/storage_keys.dart';
|
||||
import 'package:ertl_filament_verwaltung/main.dart';
|
||||
import 'package:ertl_filament_verwaltung/models/bauteil_model.dart';
|
||||
import 'package:ertl_filament_verwaltung/models/kauf_filament_model.dart';
|
||||
import 'package:ertl_filament_verwaltung/pages/anwendung_view.dart';
|
||||
import 'package:ertl_filament_verwaltung/pages/bauteil_view.dart';
|
||||
import 'package:ertl_filament_verwaltung/pages/export_view.dart';
|
||||
import 'package:ertl_filament_verwaltung/pages/home_view.dart';
|
||||
import 'package:ertl_filament_verwaltung/services/get_storage_services.dart';
|
||||
import 'package:ertl_filament_verwaltung/widgets/bauteil_card.dart';
|
||||
import 'package:ertl_filament_verwaltung/widgets/filament_card.dart';
|
||||
|
||||
import 'helpers/memory_storage_service.dart';
|
||||
|
||||
void main() {
|
||||
setUp(() {
|
||||
Get.testMode = true;
|
||||
final storage = MemoryStorageService();
|
||||
storage.tables[StorageKeys.filamente] = [
|
||||
KaufFilamentModel(
|
||||
filamentId: 'filament',
|
||||
kaufdatum: DateTime(2026),
|
||||
haendler: 'Ein Händler mit einem besonders langen Namen',
|
||||
marke: 'Eine besonders lange Filamentmarke',
|
||||
material: 'Benutzerdefiniertes Material mit langem Namen',
|
||||
preis: 12345.67,
|
||||
farbe: 'Ein lang beschriebener Farbton',
|
||||
gewicht: '1000',
|
||||
).toJson(),
|
||||
];
|
||||
storage.tables[StorageKeys.bauteile] = [
|
||||
BauteilModel(
|
||||
bauteilId: 'part',
|
||||
erstelltAm: DateTime(2026),
|
||||
name: 'Ein besonders lang benanntes Bauteil',
|
||||
verwendung: 'Eine ausführliche Beschreibung der Verwendung',
|
||||
druckdauerMinuten: 120,
|
||||
gewichtGramm: 40,
|
||||
abfallGramm: 10,
|
||||
filamente: [
|
||||
const BauteilFilamentModel(
|
||||
filamentId: 'filament',
|
||||
verbrauchGramm: 50,
|
||||
),
|
||||
],
|
||||
).toJson(),
|
||||
];
|
||||
Get.put<GetStorageService>(storage);
|
||||
});
|
||||
tearDown(() => Get.reset());
|
||||
|
||||
Future<void> resize(WidgetTester tester, double width) async {
|
||||
tester.view.physicalSize = Size(width, 640);
|
||||
await tester.pumpAndSettle();
|
||||
expect(tester.takeException(), isNull, reason: 'Width $width');
|
||||
}
|
||||
|
||||
for (final route in [
|
||||
HomeView.routeName,
|
||||
BauteilView.routeName,
|
||||
'${AnwendungView.routeName}/filament',
|
||||
ExportView.routeName,
|
||||
]) {
|
||||
testWidgets(
|
||||
'$route survives resizing across desktop and narrow browser widths',
|
||||
(tester) async {
|
||||
tester.view.physicalSize = const Size(1280, 640);
|
||||
tester.view.devicePixelRatio = 1;
|
||||
addTearDown(tester.view.resetPhysicalSize);
|
||||
addTearDown(tester.view.resetDevicePixelRatio);
|
||||
await tester.pumpWidget(const MyApp());
|
||||
await tester.pumpAndSettle();
|
||||
if (route != HomeView.routeName) {
|
||||
Get.toNamed(route);
|
||||
await tester.pumpAndSettle();
|
||||
}
|
||||
if (route == BauteilView.routeName) {
|
||||
Get.find<BauteilController>().toggleFilament('filament', true);
|
||||
}
|
||||
if (route.startsWith(AnwendungView.routeName)) {
|
||||
Get.find<AnwendungController>().sonstigesController.text = 'Entwurf';
|
||||
}
|
||||
for (final width in [
|
||||
900.0,
|
||||
899.0,
|
||||
600.0,
|
||||
360.0,
|
||||
320.0,
|
||||
240.0,
|
||||
320.0,
|
||||
1280.0,
|
||||
]) {
|
||||
await resize(tester, width);
|
||||
expect(Get.currentRoute, route);
|
||||
if (route == HomeView.routeName || route == BauteilView.routeName) {
|
||||
final card = route == HomeView.routeName
|
||||
? find.byType(FilamentCard)
|
||||
: find.byType(BauteilCard);
|
||||
final scroll = route == HomeView.routeName
|
||||
? Get.find<HomeController>().pageScrollController
|
||||
: Get.find<BauteilController>().scrollController;
|
||||
scroll.jumpTo(scroll.position.maxScrollExtent);
|
||||
await tester.pumpAndSettle();
|
||||
expect(card, findsOneWidget);
|
||||
expect(
|
||||
tester.takeException(),
|
||||
isNull,
|
||||
reason: 'Card at width $width',
|
||||
);
|
||||
scroll.jumpTo(0);
|
||||
await tester.pumpAndSettle();
|
||||
}
|
||||
}
|
||||
if (route.startsWith(AnwendungView.routeName)) {
|
||||
expect(
|
||||
Get.find<AnwendungController>().sonstigesController.text,
|
||||
'Entwurf',
|
||||
);
|
||||
}
|
||||
if (route == BauteilView.routeName) {
|
||||
expect(
|
||||
Get.find<BauteilController>().selectedFilamente,
|
||||
contains('filament'),
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:package_info_plus/package_info_plus.dart';
|
||||
|
||||
import 'package:ertl_filament_verwaltung/controller/home_controller.dart';
|
||||
import 'package:ertl_filament_verwaltung/helper/storage_keys.dart';
|
||||
@@ -17,6 +18,13 @@ void main() {
|
||||
late HomeController controller;
|
||||
|
||||
setUp(() {
|
||||
PackageInfo.setMockInitialValues(
|
||||
appName: 'Filamentverwaltung',
|
||||
packageName: 'com.example.ertl_filament_verwaltung',
|
||||
version: '1.2.3',
|
||||
buildNumber: '42',
|
||||
buildSignature: '',
|
||||
);
|
||||
Get.testMode = true;
|
||||
Get.put<GetStorageService>(MemoryStorageService());
|
||||
controller = Get.put(HomeController());
|
||||
@@ -46,6 +54,17 @@ void main() {
|
||||
gewicht: '1000',
|
||||
);
|
||||
|
||||
testWidgets('Hamburger menu displays the installed version and build', (
|
||||
tester,
|
||||
) async {
|
||||
await showHome(tester, const Size(320, 640));
|
||||
await tester.tap(find.byIcon(Icons.menu));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('Version 1.2.3'), findsOneWidget);
|
||||
expect(find.text('Build 42'), findsOneWidget);
|
||||
expect(tester.takeException(), isNull);
|
||||
});
|
||||
|
||||
testWidgets('Desktop shows both panes and edits PLA+', (tester) async {
|
||||
controller.filamente.add(sample());
|
||||
await showHome(tester, const Size(1280, 900));
|
||||
|
||||
@@ -51,6 +51,19 @@ LRESULT
|
||||
FlutterWindow::MessageHandler(HWND hwnd, UINT const message,
|
||||
WPARAM const wparam,
|
||||
LPARAM const lparam) noexcept {
|
||||
if (message == WM_GETMINMAXINFO) {
|
||||
// Logical client size; include the DPI-scaled title bar and window borders.
|
||||
const UINT dpi = GetDpiForWindow(hwnd);
|
||||
RECT minimum = {0, 0, MulDiv(320, dpi, 96), MulDiv(480, dpi, 96)};
|
||||
AdjustWindowRectExForDpi(
|
||||
&minimum, static_cast<DWORD>(GetWindowLongPtr(hwnd, GWL_STYLE)), FALSE,
|
||||
static_cast<DWORD>(GetWindowLongPtr(hwnd, GWL_EXSTYLE)), dpi);
|
||||
auto* sizes = reinterpret_cast<MINMAXINFO*>(lparam);
|
||||
sizes->ptMinTrackSize.x = minimum.right - minimum.left;
|
||||
sizes->ptMinTrackSize.y = minimum.bottom - minimum.top;
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Give Flutter, including plugins, an opportunity to handle window messages.
|
||||
if (flutter_controller_) {
|
||||
std::optional<LRESULT> result =
|
||||
|
||||
@@ -26,8 +26,8 @@ int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev,
|
||||
|
||||
FlutterWindow window(project);
|
||||
Win32Window::Point origin(10, 10);
|
||||
Win32Window::Size size(1280, 720);
|
||||
if (!window.Create(L"ertl_filament_verwaltung", origin, size)) {
|
||||
Win32Window::Size size(1400, 768);
|
||||
if (!window.Create(L"Filament Verwaltung", origin, size)) {
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
window.SetQuitOnClose(true);
|
||||
|
||||
Reference in New Issue
Block a user