map follow on drone focus. rubberband pointing to next waypoint. drone icon bigger 40px
This commit is contained in:
@@ -0,0 +1,23 @@
|
|||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
|
||||||
|
/// Ob die Karte im Fly-Modus der Live-Telemetrieposition folgt (Doku 3.11).
|
||||||
|
/// Geteilter Zustand, weil der ausloesende GPS-Knopf (TopModeBar ->
|
||||||
|
/// FlyMapControls) und die Kamera-Nachfuehrung inkl. Abbruch bei manueller
|
||||||
|
/// Karteninteraktion (FlyScreen) unabhaengige Geschwister-Widgets in
|
||||||
|
/// AppShell sind, die sich sonst nicht erreichen koennten.
|
||||||
|
class FollowDroneNotifier extends Notifier<bool> {
|
||||||
|
@override
|
||||||
|
bool build() => false;
|
||||||
|
|
||||||
|
void set(bool value) => state = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// isAutoDispose: sobald weder der Knopf noch FlyScreen noch beobachten
|
||||||
|
/// (z.B. nach Verlassen des Fly-Modus), faellt der Zustand zurueck auf
|
||||||
|
/// false - ein erneuter Eintritt in den Fly-Modus startet also immer
|
||||||
|
/// unverfolgt (entspricht dem vorherigen Verhalten als FlyScreen-lokales
|
||||||
|
/// State-Feld, das beim Moduswechsel ohnehin verworfen wurde).
|
||||||
|
final followDroneProvider = NotifierProvider<FollowDroneNotifier, bool>(
|
||||||
|
FollowDroneNotifier.new,
|
||||||
|
isAutoDispose: true,
|
||||||
|
);
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import 'dart:async';
|
||||||
import 'dart:math' as math;
|
import 'dart:math' as math;
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
@@ -9,8 +10,10 @@ import '../../../domain/mission/mission_stats.dart';
|
|||||||
import '../../../domain/mission/mission_warnings.dart';
|
import '../../../domain/mission/mission_warnings.dart';
|
||||||
import '../../../domain/mission/route_geometry.dart';
|
import '../../../domain/mission/route_geometry.dart';
|
||||||
import '../../../domain/wind/wind_math.dart';
|
import '../../../domain/wind/wind_math.dart';
|
||||||
|
import '../../../transport/flight_controller_link.dart';
|
||||||
import '../../providers/active_drone_profile_provider.dart';
|
import '../../providers/active_drone_profile_provider.dart';
|
||||||
import '../../providers/current_mission_provider.dart';
|
import '../../providers/current_mission_provider.dart';
|
||||||
|
import '../../providers/follow_drone_provider.dart';
|
||||||
import '../../providers/map_controller_provider.dart';
|
import '../../providers/map_controller_provider.dart';
|
||||||
import '../../providers/telemetry_provider.dart';
|
import '../../providers/telemetry_provider.dart';
|
||||||
import '../../providers/terrain_provider.dart';
|
import '../../providers/terrain_provider.dart';
|
||||||
@@ -28,22 +31,50 @@ import '../../widgets/warnings_panel.dart';
|
|||||||
/// die Kameraposition bleibt also beim Umschalten erhalten. Wegpunkte lassen
|
/// die Kameraposition bleibt also beim Umschalten erhalten. Wegpunkte lassen
|
||||||
/// sich weiterhin ueber die Liste anpassen (z.B. spontane Hoehenaenderung),
|
/// sich weiterhin ueber die Liste anpassen (z.B. spontane Hoehenaenderung),
|
||||||
/// nur das Karten-Reticle zum Anlegen/Verschieben entfaellt hier.
|
/// nur das Karten-Reticle zum Anlegen/Verschieben entfaellt hier.
|
||||||
class FlyScreen extends ConsumerWidget {
|
class FlyScreen extends ConsumerStatefulWidget {
|
||||||
const FlyScreen({super.key});
|
const FlyScreen({super.key});
|
||||||
|
|
||||||
void _onCenterOnWaypoint(WidgetRef ref, int index) {
|
@override
|
||||||
|
ConsumerState<FlyScreen> createState() => _FlyScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _FlyScreenState extends ConsumerState<FlyScreen> {
|
||||||
|
late final MapController _mapController;
|
||||||
|
StreamSubscription<MapEvent>? _mapEventSub;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_mapController = ref.read(mapControllerProvider);
|
||||||
|
_mapEventSub = _mapController.mapEventStream.listen(_onMapEvent);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_mapEventSub?.cancel();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Jede manuelle Kartengeste (Pan/Pinch/Fling, ...) bricht den ueber den
|
||||||
|
/// GPS-Knopf in der Kopfleiste (FlyMapControls) aktivierten Folgemodus ab
|
||||||
|
/// - nur Bewegungen mit source == mapController sind programmatisch
|
||||||
|
/// (durch die Telemetrie-Nachfuehrung unten selbst oder andere
|
||||||
|
/// Kartensprmuenge ausgeloest) und sollen ihn nicht beenden.
|
||||||
|
void _onMapEvent(MapEvent event) {
|
||||||
|
if (ref.read(followDroneProvider) &&
|
||||||
|
event.source != MapEventSource.mapController) {
|
||||||
|
ref.read(followDroneProvider.notifier).set(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onCenterOnWaypoint(int index) {
|
||||||
final waypoints = ref.read(currentMissionProvider);
|
final waypoints = ref.read(currentMissionProvider);
|
||||||
if (index < 0 || index >= waypoints.length) return;
|
if (index < 0 || index >= waypoints.length) return;
|
||||||
final wp = waypoints[index];
|
final wp = waypoints[index];
|
||||||
final controller = ref.read(mapControllerProvider);
|
_mapController.move(LatLng(wp.lat, wp.lon), _mapController.camera.zoom);
|
||||||
controller.move(LatLng(wp.lat, wp.lon), controller.camera.zoom);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void _openWaypointList(
|
void _openWaypointList({PanelTab initialTab = PanelTab.list}) {
|
||||||
BuildContext context,
|
|
||||||
WidgetRef ref, {
|
|
||||||
PanelTab initialTab = PanelTab.list,
|
|
||||||
}) {
|
|
||||||
Navigator.of(context).push(
|
Navigator.of(context).push(
|
||||||
PageRouteBuilder<void>(
|
PageRouteBuilder<void>(
|
||||||
opaque: true,
|
opaque: true,
|
||||||
@@ -57,7 +88,7 @@ class FlyScreen extends ConsumerWidget {
|
|||||||
initialTab: initialTab,
|
initialTab: initialTab,
|
||||||
onClose: () => Navigator.of(routeContext).pop(),
|
onClose: () => Navigator.of(routeContext).pop(),
|
||||||
onCenterOnWaypoint: (index) {
|
onCenterOnWaypoint: (index) {
|
||||||
_onCenterOnWaypoint(ref, index);
|
_onCenterOnWaypoint(index);
|
||||||
Navigator.of(routeContext).pop();
|
Navigator.of(routeContext).pop();
|
||||||
},
|
},
|
||||||
onDelete: (index) =>
|
onDelete: (index) =>
|
||||||
@@ -79,11 +110,7 @@ class FlyScreen extends ConsumerWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _openWarningsPanel(
|
void _openWarningsPanel(List<MissionWarning> warnings) {
|
||||||
BuildContext context,
|
|
||||||
WidgetRef ref,
|
|
||||||
List<MissionWarning> warnings,
|
|
||||||
) {
|
|
||||||
Navigator.of(context).push(MaterialPageRoute<void>(
|
Navigator.of(context).push(MaterialPageRoute<void>(
|
||||||
builder: (routeContext) => WarningsPanel(
|
builder: (routeContext) => WarningsPanel(
|
||||||
warnings: warnings,
|
warnings: warnings,
|
||||||
@@ -92,9 +119,9 @@ class FlyScreen extends ConsumerWidget {
|
|||||||
Navigator.of(routeContext).pop();
|
Navigator.of(routeContext).pop();
|
||||||
switch (warning.action) {
|
switch (warning.action) {
|
||||||
case WarningAction.altitude:
|
case WarningAction.altitude:
|
||||||
_openWaypointList(context, ref, initialTab: PanelTab.altitude);
|
_openWaypointList(initialTab: PanelTab.altitude);
|
||||||
case WarningAction.speed:
|
case WarningAction.speed:
|
||||||
_openWaypointList(context, ref, initialTab: PanelTab.speed);
|
_openWaypointList(initialTab: PanelTab.speed);
|
||||||
case WarningAction.map:
|
case WarningAction.map:
|
||||||
fitMapToWaypoints(ref, ref.read(currentMissionProvider));
|
fitMapToWaypoints(ref, ref.read(currentMissionProvider));
|
||||||
}
|
}
|
||||||
@@ -104,10 +131,23 @@ class FlyScreen extends ConsumerWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context) {
|
||||||
final waypoints = ref.watch(currentMissionProvider);
|
final waypoints = ref.watch(currentMissionProvider);
|
||||||
final droneProfile = ref.watch(activeDroneProfileProvider);
|
final droneProfile = ref.watch(activeDroneProfileProvider);
|
||||||
final telemetry = ref.watch(telemetryProvider).value;
|
final telemetry = ref.watch(telemetryProvider).value;
|
||||||
|
final followDrone = ref.watch(followDroneProvider);
|
||||||
|
|
||||||
|
// Solange der ueber den GPS-Knopf in der Kopfleiste (FlyMapControls)
|
||||||
|
// aktivierte Folgemodus aktiv ist, jede neue Telemetrie-Position
|
||||||
|
// programmatisch nachziehen (source == mapController, siehe
|
||||||
|
// _onMapEvent - bricht den Modus dadurch nicht selbst wieder ab).
|
||||||
|
ref.listen<AsyncValue<TelemetryFrame>>(telemetryProvider, (previous, next) {
|
||||||
|
if (!followDrone) return;
|
||||||
|
final frame = next.value;
|
||||||
|
if (frame != null && frame.hasFix) {
|
||||||
|
_mapController.move(LatLng(frame.lat, frame.lon), _mapController.camera.zoom);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
final routeGeometry = buildRouteGeometry(
|
final routeGeometry = buildRouteGeometry(
|
||||||
waypoints,
|
waypoints,
|
||||||
@@ -147,8 +187,8 @@ class FlyScreen extends ConsumerWidget {
|
|||||||
final droneMarker = telemetry != null && telemetry.hasFix
|
final droneMarker = telemetry != null && telemetry.hasFix
|
||||||
? Marker(
|
? Marker(
|
||||||
point: LatLng(telemetry.lat, telemetry.lon),
|
point: LatLng(telemetry.lat, telemetry.lon),
|
||||||
width: 28,
|
width: 40,
|
||||||
height: 28,
|
height: 40,
|
||||||
alignment: Alignment.center,
|
alignment: Alignment.center,
|
||||||
child: DroneMarkerIcon(headingDeg: telemetry.headingDeg),
|
child: DroneMarkerIcon(headingDeg: telemetry.headingDeg),
|
||||||
)
|
)
|
||||||
@@ -156,17 +196,37 @@ class FlyScreen extends ConsumerWidget {
|
|||||||
? null
|
? null
|
||||||
: Marker(
|
: Marker(
|
||||||
point: LatLng(waypoints.first.lat, waypoints.first.lon),
|
point: LatLng(waypoints.first.lat, waypoints.first.lon),
|
||||||
width: 28,
|
width: 40,
|
||||||
height: 28,
|
height: 40,
|
||||||
alignment: Alignment.center,
|
alignment: Alignment.center,
|
||||||
child: const DroneMarkerIcon(),
|
child: const DroneMarkerIcon(),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Gestrichelte Linie von der Drohne zum aktiven Wegpunkt (Doku 3.4:
|
||||||
|
// TelemetryFrame.activeWaypointIndex aus MSP_NAV_STATUS) - derselbe
|
||||||
|
// Darstellungsstil wie das Reticle-Gummiband im Plan-Modus (MissionMap.
|
||||||
|
// rubberBandLine), hier aber Drohne -> naechster Wegpunkt statt letzter
|
||||||
|
// Wegpunkt -> Kartenmitte.
|
||||||
|
List<LatLng>? rubberBand;
|
||||||
|
final activeWpIndex = telemetry?.activeWaypointIndex;
|
||||||
|
if (telemetry != null &&
|
||||||
|
telemetry.hasFix &&
|
||||||
|
activeWpIndex != null &&
|
||||||
|
activeWpIndex >= 0 &&
|
||||||
|
activeWpIndex < waypoints.length) {
|
||||||
|
final target = waypoints[activeWpIndex];
|
||||||
|
rubberBand = [
|
||||||
|
LatLng(telemetry.lat, telemetry.lon),
|
||||||
|
LatLng(target.lat, target.lon),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
return Stack(
|
return Stack(
|
||||||
children: [
|
children: [
|
||||||
MissionMap(
|
MissionMap(
|
||||||
mapController: ref.watch(mapControllerProvider),
|
mapController: _mapController,
|
||||||
routeSegments: routeGeometry.segments,
|
routeSegments: routeGeometry.segments,
|
||||||
|
rubberBandLine: rubberBand,
|
||||||
waypointMarkers: [
|
waypointMarkers: [
|
||||||
for (var i = 0; i < waypoints.length; i++)
|
for (var i = 0; i < waypoints.length; i++)
|
||||||
CircleMarker(
|
CircleMarker(
|
||||||
@@ -209,8 +269,8 @@ class FlyScreen extends ConsumerWidget {
|
|||||||
totalDistanceM: missionStats.distanceM,
|
totalDistanceM: missionStats.distanceM,
|
||||||
totalDurationSec: missionStats.durationSec,
|
totalDurationSec: missionStats.durationSec,
|
||||||
warnings: warnings,
|
warnings: warnings,
|
||||||
onDetailsTap: () => _openWaypointList(context, ref),
|
onDetailsTap: _openWaypointList,
|
||||||
onWarningsTap: () => _openWarningsPanel(context, ref, warnings),
|
onWarningsTap: () => _openWarningsPanel(warnings),
|
||||||
onSettingsTap: () => Navigator.of(context).push(MaterialPageRoute(
|
onSettingsTap: () => Navigator.of(context).push(MaterialPageRoute(
|
||||||
builder: (_) => const SettingsScreen(),
|
builder: (_) => const SettingsScreen(),
|
||||||
)),
|
)),
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import 'package:flutter/material.dart';
|
|||||||
/// dasselbe Muster). Zeigt unrotiert (Spitze nach oben) bei fehlendem
|
/// dasselbe Muster). Zeigt unrotiert (Spitze nach oben) bei fehlendem
|
||||||
/// [headingDeg], sonst im Uhrzeigersinn um den Kurs ueber Grund gedreht.
|
/// [headingDeg], sonst im Uhrzeigersinn um den Kurs ueber Grund gedreht.
|
||||||
class DroneMarkerIcon extends StatelessWidget {
|
class DroneMarkerIcon extends StatelessWidget {
|
||||||
const DroneMarkerIcon({super.key, this.size = 28, this.headingDeg});
|
const DroneMarkerIcon({super.key, this.size = 40, this.headingDeg});
|
||||||
|
|
||||||
final double size;
|
final double size;
|
||||||
|
|
||||||
|
|||||||
@@ -3,26 +3,42 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|||||||
import 'package:latlong2/latlong.dart';
|
import 'package:latlong2/latlong.dart';
|
||||||
|
|
||||||
import '../providers/current_mission_provider.dart';
|
import '../providers/current_mission_provider.dart';
|
||||||
|
import '../providers/follow_drone_provider.dart';
|
||||||
import '../providers/map_controller_provider.dart';
|
import '../providers/map_controller_provider.dart';
|
||||||
import '../providers/telemetry_provider.dart';
|
import '../providers/telemetry_provider.dart';
|
||||||
import 'nav_icon_button.dart';
|
import 'nav_icon_button.dart';
|
||||||
|
|
||||||
/// Kartensteuerung im Fly-Modus (Doku 3.11): Ortssuche/Home entfallen hier
|
/// Kartensteuerung im Fly-Modus (Doku 3.11): Ortssuche/Home entfallen hier
|
||||||
/// (keine Missionsbearbeitung), stattdessen zoomt der Drohnen-Button auf die
|
/// (keine Missionsbearbeitung), stattdessen schaltet der Drohnen-Button den
|
||||||
/// live Telemetrieposition - "Home" (erster Wegpunkt) ist waehrend des
|
/// Kartenfolgemodus um (followDroneProvider) - "Home" (erster Wegpunkt) ist
|
||||||
/// Flugs kein sinnvoller Bezugspunkt mehr, der Fit-Button (gesamte Route)
|
/// waehrend des Flugs kein sinnvoller Bezugspunkt mehr, der Fit-Button
|
||||||
/// bleibt dagegen unveraendert wie im Plan-Modus erhalten.
|
/// (gesamte Route) bleibt dagegen unveraendert wie im Plan-Modus erhalten.
|
||||||
class FlyMapControls extends ConsumerWidget {
|
class FlyMapControls extends ConsumerWidget {
|
||||||
const FlyMapControls({super.key});
|
const FlyMapControls({super.key});
|
||||||
|
|
||||||
|
/// Zielzoom beim Aktivieren des Folgemodus - naeher als der
|
||||||
|
/// Standard-Kartenzoom (siehe MissionMap._initialZoom), damit der Sprung
|
||||||
|
/// tatsaechlich als Zoom wahrgenommen wird und nicht nur als Verschieben.
|
||||||
|
static const _followZoom = 18.0;
|
||||||
|
|
||||||
|
/// Erster Tap zoomt auf die aktuelle Telemetrieposition und aktiviert den
|
||||||
|
/// Folgemodus (FlyScreen haengt sich darauf ein, zieht die Karte bei
|
||||||
|
/// jedem neuen Telemetrie-Frame nach und bricht bei manueller
|
||||||
|
/// Karteninteraktion selbststaendig wieder ab). Zweiter Tap beendet ihn
|
||||||
|
/// wieder, ohne die Kamera zu bewegen.
|
||||||
void _onDronePressed(BuildContext context, WidgetRef ref) {
|
void _onDronePressed(BuildContext context, WidgetRef ref) {
|
||||||
|
if (ref.read(followDroneProvider)) {
|
||||||
|
ref.read(followDroneProvider.notifier).set(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
final frame = ref.read(telemetryProvider).value;
|
final frame = ref.read(telemetryProvider).value;
|
||||||
if (frame == null) {
|
if (frame == null) {
|
||||||
ScaffoldMessenger.of(context)
|
ScaffoldMessenger.of(context)
|
||||||
.showSnackBar(const SnackBar(content: Text('No telemetry yet')));
|
.showSnackBar(const SnackBar(content: Text('No telemetry yet')));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
ref.read(mapControllerProvider).move(LatLng(frame.lat, frame.lon), 16);
|
ref.read(mapControllerProvider).move(LatLng(frame.lat, frame.lon), _followZoom);
|
||||||
|
ref.read(followDroneProvider.notifier).set(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onFitPressed(BuildContext context, WidgetRef ref) {
|
void _onFitPressed(BuildContext context, WidgetRef ref) {
|
||||||
@@ -37,11 +53,13 @@ class FlyMapControls extends ConsumerWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final following = ref.watch(followDroneProvider);
|
||||||
return Row(
|
return Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
NavIconButton(
|
NavIconButton(
|
||||||
icon: Icons.my_location,
|
icon: following ? Icons.gps_fixed : Icons.my_location,
|
||||||
|
active: following,
|
||||||
onTap: () => _onDronePressed(context, ref),
|
onTap: () => _onDronePressed(context, ref),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
|
|||||||
@@ -37,8 +37,11 @@ class MissionMap extends StatelessWidget {
|
|||||||
/// markierte Segmente ueberschreiten Steig-/Sinkrate oder Kurvenradius.
|
/// markierte Segmente ueberschreiten Steig-/Sinkrate oder Kurvenradius.
|
||||||
final List<RouteSegment> routeSegments;
|
final List<RouteSegment> routeSegments;
|
||||||
|
|
||||||
/// Gestrichelte Vorschaulinie vom letzten Wegpunkt zur aktuellen
|
/// Gestrichelte Verbindungslinie, Bedeutung je nach Modus: im Plan-Modus
|
||||||
/// Kartenmitte, waehrend noch kein neuer Punkt bestaetigt wurde.
|
/// vom letzten Wegpunkt zur aktuellen Kartenmitte, solange noch kein
|
||||||
|
/// neuer Punkt bestaetigt wurde (PlanScreen); im Fly-Modus von der
|
||||||
|
/// Drohne zum aktiven Wegpunkt (FlyScreen, TelemetryFrame.
|
||||||
|
/// activeWaypointIndex).
|
||||||
final List<LatLng>? rubberBandLine;
|
final List<LatLng>? rubberBandLine;
|
||||||
|
|
||||||
final VoidCallback? onMapReady;
|
final VoidCallback? onMapReady;
|
||||||
|
|||||||
@@ -4,11 +4,20 @@ import 'package:flutter/material.dart';
|
|||||||
/// Plan- (MapSearchControls: Home/Fit) und Fly-Modus-Kartensteuerungen
|
/// Plan- (MapSearchControls: Home/Fit) und Fly-Modus-Kartensteuerungen
|
||||||
/// (FlyMapControls: Drohne/Fit).
|
/// (FlyMapControls: Drohne/Fit).
|
||||||
class NavIconButton extends StatelessWidget {
|
class NavIconButton extends StatelessWidget {
|
||||||
const NavIconButton({super.key, required this.icon, required this.onTap});
|
const NavIconButton({
|
||||||
|
super.key,
|
||||||
|
required this.icon,
|
||||||
|
required this.onTap,
|
||||||
|
this.active = false,
|
||||||
|
});
|
||||||
|
|
||||||
final IconData icon;
|
final IconData icon;
|
||||||
final VoidCallback onTap;
|
final VoidCallback onTap;
|
||||||
|
|
||||||
|
/// Rosa Akzentrand (Doku 3.10: DroneMarkerIcon-Farbe) fuer Knoepfe mit
|
||||||
|
/// an/aus-Zustand, z.B. den Drohnen-Folgemodus (FlyMapControls).
|
||||||
|
final bool active;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
@@ -19,6 +28,9 @@ class NavIconButton extends StatelessWidget {
|
|||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
shape: BoxShape.circle,
|
shape: BoxShape.circle,
|
||||||
color: Colors.black.withValues(alpha: 0.55),
|
color: Colors.black.withValues(alpha: 0.55),
|
||||||
|
border: active
|
||||||
|
? Border.all(color: const Color(0xFFFF2E93), width: 2)
|
||||||
|
: null,
|
||||||
),
|
),
|
||||||
child: Icon(icon, color: Colors.white, size: 16),
|
child: Icon(icon, color: Colors.white, size: 16),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
,AI/Consti,AI,03.08.2026 21:25,file:///C:/Users/Consti/AppData/Roaming/LibreOffice/4;
|
,AI/Consti,AI,03.08.2026 21:43,file:///C:/Users/Consti/AppData/Roaming/LibreOffice/4;
|
||||||
Binary file not shown.
Reference in New Issue
Block a user