feat: alphabet list, change Layout etc

This commit is contained in:
2026-10-11 20:37:59 +02:00
parent 489896305d
commit 072f560989
31 changed files with 1847 additions and 832 deletions
@@ -45,6 +45,7 @@ class ProductListViewModel extends ChangeNotifier {
required String category,
required int stockQuantity,
required int lowStockThreshold,
required bool infiniteStock,
required int priceInCents,
required String? imagePath,
}) {
@@ -55,6 +56,7 @@ class ProductListViewModel extends ChangeNotifier {
category: category,
stockQuantity: stockQuantity,
lowStockThreshold: lowStockThreshold,
infiniteStock: infiniteStock,
priceInCents: priceInCents,
imagePath: imagePath,
),
+142 -111
View File
@@ -2,31 +2,45 @@ import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:ota_update/ota_update.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
import '../models/update_failure.dart';
import '../utils/app_config.dart';
import '../utils/app_update_util.dart';
enum OtaPhase { downloading, installing, done, error }
enum UpdatePhase {
downloading,
installing,
/// Android's installer prompt is open; the app is replaced once the user
/// confirms it, so there is no "done" state to show.
waitingForInstaller,
failed,
}
class UpdateProgressViewModel extends ChangeNotifier {
final UpdateInfo update;
final bool simulate;
final bool simulateError;
OtaPhase _phase = OtaPhase.downloading;
double _progress = 0;
String _statusText = 'Preparing download…';
String? _errorMessage;
bool _done = false;
UpdatePhase _phase = UpdatePhase.downloading;
double? _progress;
UpdateFailure? _failure;
String? _currentVersion;
StreamSubscription? _subscription;
StreamSubscription<OtaEvent>? _subscription;
Timer? _simTimer;
OtaPhase get phase => _phase;
double get progress => _progress;
String get statusText => _statusText;
String? get errorMessage => _errorMessage;
UpdatePhase get phase => _phase;
/// Download progress from 0 to 1, or null while it is not known yet.
double? get progress => _progress;
UpdateFailure? get failure => _failure;
String? get currentVersion => _currentVersion;
bool get isBusy =>
_phase == UpdatePhase.downloading || _phase == UpdatePhase.installing;
UpdateProgressViewModel({
required this.update,
@@ -34,7 +48,18 @@ class UpdateProgressViewModel extends ChangeNotifier {
this.simulateError = false,
});
Future<void> start() async {
void init() {
unawaited(_loadCurrentVersion());
start();
}
void start() {
cancel();
_phase = UpdatePhase.downloading;
_progress = null;
_failure = null;
notifyListeners();
if (simulate) {
_runSimulation();
return;
@@ -44,8 +69,6 @@ class UpdateProgressViewModel extends ChangeNotifier {
? update.download
: '$kUpdateServerUrl${update.download}';
debugPrint('UpdateProgressViewModel: starting download from $url');
_subscription = OtaUpdate()
.execute(
url,
@@ -55,118 +78,126 @@ class UpdateProgressViewModel extends ChangeNotifier {
.listen(_onEvent, onError: _onError, onDone: _onDone);
}
void cancel() {
_simTimer?.cancel();
_simTimer = null;
unawaited(_subscription?.cancel());
_subscription = null;
}
Future<void> _loadCurrentVersion() async {
try {
final info = await PackageInfo.fromPlatform();
_currentVersion = info.version;
notifyListeners();
} catch (e) {
debugPrint('UpdateProgressViewModel: could not read version: $e');
}
}
void _onEvent(OtaEvent event) {
switch (event.status) {
case OtaStatus.DOWNLOADING:
final percent = double.tryParse(event.value ?? '') ?? 0;
_progress = (percent / 100).clamp(0.0, 1.0);
_phase = UpdatePhase.downloading;
notifyListeners();
case OtaStatus.INSTALLING:
_progress = 1;
_phase = UpdatePhase.installing;
notifyListeners();
case OtaStatus.INSTALLATION_DONE:
_phase = UpdatePhase.waitingForInstaller;
notifyListeners();
case OtaStatus.DOWNLOAD_ERROR:
_fail(UpdateFailure.download, event.value);
case OtaStatus.CHECKSUM_ERROR:
_fail(UpdateFailure.checksum, event.value);
case OtaStatus.PERMISSION_NOT_GRANTED_ERROR:
_fail(UpdateFailure.permission, event.value);
case OtaStatus.INSTALLATION_ERROR:
_fail(UpdateFailure.install, event.value);
case OtaStatus.ALREADY_RUNNING_ERROR:
_fail(UpdateFailure.alreadyRunning, event.value);
case OtaStatus.INTERNAL_ERROR:
_fail(UpdateFailure.unknown, event.value);
case OtaStatus.CANCELED:
break;
}
}
/// The plugin closes the stream once it has handed the APK to Android's
/// installer; closing earlier means it gave up without telling why.
void _onDone() {
_subscription = null;
switch (_phase) {
case UpdatePhase.installing:
_phase = UpdatePhase.waitingForInstaller;
notifyListeners();
case UpdatePhase.downloading:
_fail(UpdateFailure.unknown, 'Update stream closed while downloading');
case UpdatePhase.waitingForInstaller || UpdatePhase.failed:
break;
}
}
void _onError(Object error, StackTrace stackTrace) {
debugPrint('UpdateProgressViewModel: stream error: $error');
unawaited(Sentry.captureException(error, stackTrace: stackTrace));
_fail(UpdateFailure.unknown, null, report: false);
}
void _fail(UpdateFailure failure, String? detail, {bool report = true}) {
debugPrint('UpdateProgressViewModel: $failure: $detail');
if (report) {
unawaited(
Sentry.captureMessage(
'OTA update failed ($failure, ${update.version}): $detail',
),
);
}
cancel();
_phase = UpdatePhase.failed;
_failure = failure;
notifyListeners();
}
void _runSimulation() {
debugPrint('UpdateProgressViewModel: running simulated download');
const totalSteps = 40;
var step = 0;
int step = 0;
const totalSteps = 20;
const stepDuration = Duration(milliseconds: 500);
_simTimer = Timer.periodic(stepDuration, (timer) {
_simTimer = Timer.periodic(const Duration(milliseconds: 150), (timer) {
step++;
if (simulateError && step == 10) {
if (simulateError && step == totalSteps ~/ 2) {
_phase = UpdatePhase.failed;
_failure = UpdateFailure.simulated;
timer.cancel();
_phase = OtaPhase.error;
_errorMessage = 'Simulated download error (network timeout)';
_statusText = 'Download failed';
notifyListeners();
return;
}
if (step < totalSteps) {
_progress = step / totalSteps;
_statusText = 'Downloading ${(_progress * 100).toStringAsFixed(0)}%';
_phase = OtaPhase.downloading;
notifyListeners();
} else {
timer.cancel();
_startInstallPhase();
}
});
}
void _startInstallPhase() {
_phase = OtaPhase.installing;
_progress = 1.0;
_statusText = 'Installing update…';
notifyListeners();
_simTimer = Timer(const Duration(seconds: 3), () {
_phase = OtaPhase.done;
_progress = 1.0;
_statusText = 'Update installed! Restarting…';
notifyListeners();
});
}
void _onDone() {
if (_done) return;
_done = true;
_phase = OtaPhase.done;
_progress = 1.0;
_statusText = 'Update installed! Restarting…';
notifyListeners();
}
void _onEvent(OtaEvent event) {
switch (event.status) {
case OtaStatus.DOWNLOADING:
final raw = double.tryParse(event.value ?? '0') ?? 0;
_progress = (raw / 100).clamp(0.0, 1.0);
_statusText = 'Downloading ${_progress.toStringAsFixed(0)}%';
_phase = OtaPhase.downloading;
notifyListeners();
case OtaStatus.INSTALLING:
_phase = OtaPhase.installing;
_progress = 1.0;
_statusText = 'Installing update…';
notifyListeners();
case OtaStatus.INSTALLATION_DONE:
_onDone();
return;
}
case OtaStatus.DOWNLOAD_ERROR:
_phase = OtaPhase.error;
_errorMessage = 'Download failed: ${event.value}';
_statusText = 'Download failed';
Sentry.captureMessage('OTA download error: ${event.value}');
timer.cancel();
_progress = 1;
_phase = UpdatePhase.installing;
notifyListeners();
_simTimer = Timer(const Duration(seconds: 2), () {
_phase = UpdatePhase.waitingForInstaller;
notifyListeners();
case OtaStatus.INSTALLATION_ERROR:
_phase = OtaPhase.error;
_errorMessage = 'Installation failed: ${event.value}';
_statusText = 'Installation failed';
Sentry.captureMessage('OTA installation error: ${event.value}');
notifyListeners();
case OtaStatus.ALREADY_RUNNING_ERROR:
_phase = OtaPhase.error;
_errorMessage = 'Update already in progress';
_statusText = 'Update already running';
notifyListeners();
default:
break;
}
}
void _onError(Object error, StackTrace stack) {
debugPrint('UpdateProgressViewModel: stream error: $error');
_phase = OtaPhase.error;
_errorMessage = error.toString();
_statusText = 'Update failed';
Sentry.captureException(error, stackTrace: stack);
notifyListeners();
}
void cancel() {
_simTimer?.cancel();
_simTimer = null;
_subscription?.cancel();
_subscription = null;
});
});
}
@override