feat: implement AI export service and establish project navigation and theming infrastructure + acces internet
Build & Release Android APK / build-apk (push) Successful in 23m34s

This commit is contained in:
streaper2
2026-08-27 18:32:59 +02:00
parent e943538133
commit 8dc6542603
19 changed files with 3312 additions and 1055 deletions
+109
View File
@@ -0,0 +1,109 @@
import 'dart:ui';
import 'package:flutter/material.dart';
import '../theme/app_theme.dart';
/// Conteneur avec effet de verre givré (Glassmorphism),
/// bordures translucides fines et reflets subtils pour casser l'aspect standard Flutter.
class GlassContainer extends StatelessWidget {
final Widget child;
final double borderRadius;
final double blur;
final EdgeInsetsGeometry padding;
final EdgeInsetsGeometry margin;
final Color? customBackgroundColor;
final Color? borderColor;
final Color? glowColor;
final VoidCallback? onTap;
final VoidCallback? onLongPress;
const GlassContainer({
super.key,
required this.child,
this.borderRadius = 18.0,
this.blur = 12.0,
this.padding = const EdgeInsets.all(16.0),
this.margin = EdgeInsets.zero,
this.customBackgroundColor,
this.borderColor,
this.glowColor,
this.onTap,
this.onLongPress,
});
@override
Widget build(BuildContext context) {
final isDark = Theme.of(context).brightness == Brightness.dark;
final defaultBg = isDark
? const Color(0xFF141C26).withValues(alpha: 0.72)
: Colors.white.withValues(alpha: 0.82);
final defaultBorder = isDark
? Colors.white.withValues(alpha: 0.12)
: Colors.black.withValues(alpha: 0.08);
final resolvedBorderColor = borderColor ?? defaultBorder;
final resolvedBg = customBackgroundColor ?? defaultBg;
Widget content = Container(
padding: padding,
decoration: BoxDecoration(
color: resolvedBg,
borderRadius: BorderRadius.circular(borderRadius),
border: Border.all(color: resolvedBorderColor, width: 1.2),
boxShadow: [
if (glowColor != null)
BoxShadow(
color: glowColor!.withValues(alpha: isDark ? 0.25 : 0.15),
blurRadius: 18,
spreadRadius: -2,
offset: const Offset(0, 4),
)
else
BoxShadow(
color: Colors.black.withValues(alpha: isDark ? 0.28 : 0.04),
blurRadius: 16,
offset: const Offset(0, 6),
),
],
),
child: child,
);
if (blur > 0) {
content = ClipRRect(
borderRadius: BorderRadius.circular(borderRadius),
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: blur, sigmaY: blur),
child: content,
),
);
} else {
content = ClipRRect(
borderRadius: BorderRadius.circular(borderRadius),
child: content,
);
}
if (margin != EdgeInsets.zero) {
content = Padding(padding: margin, child: content);
}
if (onTap != null || onLongPress != null) {
return Material(
color: Colors.transparent,
borderRadius: BorderRadius.circular(borderRadius),
child: InkWell(
borderRadius: BorderRadius.circular(borderRadius),
onTap: onTap,
onLongPress: onLongPress,
splashColor: (glowColor ?? AppTheme.primaryColor).withValues(alpha: 0.15),
highlightColor: (glowColor ?? AppTheme.primaryColor).withValues(alpha: 0.08),
child: content,
),
);
}
return content;
}
}
+335
View File
@@ -0,0 +1,335 @@
import 'package:flutter/material.dart';
import '../../data/models/weapon.dart';
/// Widget affichant une icône vectorielle stylisée et fidèle pour chaque type d'arme.
class WeaponTypeIcon extends StatelessWidget {
final WeaponType type;
final Color? color;
final double size;
const WeaponTypeIcon({
super.key,
required this.type,
this.color,
this.size = 24,
});
@override
Widget build(BuildContext context) {
final effectiveColor = color ?? Theme.of(context).colorScheme.primary;
return CustomPaint(
size: Size(size, size),
painter: _WeaponTypePainter(
type: type,
color: effectiveColor,
),
);
}
}
class _WeaponTypePainter extends CustomPainter {
final WeaponType type;
final Color color;
_WeaponTypePainter({
required this.type,
required this.color,
});
@override
void paint(Canvas canvas, Size size) {
final fillPaint = Paint()
..color = color
..style = PaintingStyle.fill
..isAntiAlias = true;
final strokePaint = Paint()
..color = color
..style = PaintingStyle.stroke
..strokeWidth = 1.0
..strokeCap = StrokeCap.round
..strokeJoin = StrokeJoin.round
..isAntiAlias = true;
canvas.save();
// Normalisation sur une grille 32x32 pour un rendu très précis
final scale = size.width / 32.0;
canvas.scale(scale, scale);
switch (type) {
case WeaponType.handgun:
_drawGlock(canvas, fillPaint, strokePaint);
break;
case WeaponType.rifle:
_drawKalashnikov(canvas, fillPaint, strokePaint);
break;
case WeaponType.shotgun:
_drawPumpShotgun(canvas, fillPaint, strokePaint);
break;
case WeaponType.airgun:
_drawAirgun(canvas, fillPaint, strokePaint);
break;
}
canvas.restore();
}
/// Silhouette fidèle d'un Glock : culasse carrée "boxy", pontet angulaire et poignée ergonomique
void _drawGlock(Canvas canvas, Paint fillPaint, Paint strokePaint) {
// 1. Culasse rectangulaire Glock (Slide)
final slidePath = Path();
slidePath.moveTo(4, 9);
slidePath.lineTo(26.5, 9); // dessus de culasse
slidePath.lineTo(27.5, 10); // chanfrein nez avant
slidePath.lineTo(27.5, 14.5); // bouche du canon
slidePath.lineTo(4, 14.5); // bas de culasse
slidePath.lineTo(3.5, 13); // chanfrein arrière
slidePath.lineTo(3.5, 10);
slidePath.close();
canvas.drawPath(slidePath, fillPaint);
// Organes de visée Glock
// Cran de mire arrière carré
canvas.drawRect(const Rect.fromLTWH(5, 7.2, 2.5, 1.8), fillPaint);
// Guidon avant
canvas.drawRect(const Rect.fromLTWH(24.5, 7.2, 1.8, 1.8), fillPaint);
// 2. Carcasse inférieure & Poignée (Frame & Grip)
final framePath = Path();
framePath.moveTo(4, 14.5);
framePath.lineTo(26, 14.5); // rail picatinny avant
framePath.lineTo(25.5, 16.5);
framePath.lineTo(17.5, 16.5); // bas du rail avant pontet
framePath.lineTo(17.5, 19.5); // face avant du pontet carré Glock
framePath.lineTo(12.5, 20.5); // bas du pontet
framePath.lineTo(12, 17.5); // jonction détente / poignée avant
framePath.lineTo(9.5, 27); // poignée avant avec empreinte
framePath.lineTo(4.5, 27); // talon de chargeur / magwell
framePath.lineTo(8, 16.5); // dos de poignée / beavertail Glock
framePath.lineTo(4, 15.5);
framePath.close();
canvas.drawPath(framePath, fillPaint);
// 3. Queue de détente Safe Action
final triggerPath = Path();
triggerPath.moveTo(15.5, 15.5);
triggerPath.quadraticBezierTo(14, 17.5, 15.2, 19);
canvas.drawPath(triggerPath, strokePaint..strokeWidth = 1.3);
// 4. Stries de préhension arrière (Serrations)
for (double i = 6; i <= 10; i += 1.5) {
canvas.drawLine(
Offset(i, 10),
Offset(i, 13.5),
Paint()
..color = Colors.black.withValues(alpha: 0.35)
..strokeWidth = 0.8,
);
}
}
/// Silhouette fidèle d'une Kalashnikov (AK-47 / AKM) : chargeur banane, emprunt de gaz et guidon haut
void _drawKalashnikov(Canvas canvas, Paint fillPaint, Paint strokePaint) {
// 1. Crosse arrière en bois / épaule
final stockPath = Path();
stockPath.moveTo(1.5, 13);
stockPath.lineTo(8, 14.5);
stockPath.lineTo(8, 18);
stockPath.lineTo(1.5, 20.5);
stockPath.close();
canvas.drawPath(stockPath, fillPaint);
// 2. Boîtier de culasse (Receiver) & Capot supérieur
final receiverPath = Path();
receiverPath.moveTo(8, 13.5);
receiverPath.lineTo(17, 13.5);
receiverPath.lineTo(17, 18);
receiverPath.lineTo(8, 18);
receiverPath.close();
canvas.drawPath(receiverPath, fillPaint);
// 3. Garde-main & Tube d'emprunt de gaz supérieur
final handguardPath = Path();
handguardPath.moveTo(17, 13.5);
handguardPath.lineTo(24, 13.5);
handguardPath.lineTo(24, 17);
handguardPath.lineTo(17, 17);
handguardPath.close();
canvas.drawPath(handguardPath, fillPaint);
// 4. Canon fin avant & Bloc guidon Kalash
final barrelPath = Path();
barrelPath.moveTo(24, 14.5);
barrelPath.lineTo(30.5, 14.5);
barrelPath.lineTo(30.5, 16);
barrelPath.lineTo(24, 16);
barrelPath.close();
canvas.drawPath(barrelPath, fillPaint);
// Bloc guidon triangulaire surélevé avant
final frontSightPath = Path();
frontSightPath.moveTo(28, 14.5);
frontSightPath.lineTo(29, 11);
frontSightPath.lineTo(30, 14.5);
frontSightPath.close();
canvas.drawPath(frontSightPath, fillPaint);
// Hausse arrière au dessus du boîtier
canvas.drawRect(const Rect.fromLTWH(16.5, 12, 2, 1.5), fillPaint);
// 5. Chargeur courbé "banane" emblématique (30 coups)
final magPath = Path();
magPath.moveTo(14, 18);
magPath.lineTo(16.5, 18);
magPath.quadraticBezierTo(17.5, 22.5, 15, 26);
magPath.lineTo(12, 25.5);
magPath.quadraticBezierTo(14, 22, 13.5, 18);
magPath.close();
canvas.drawPath(magPath, fillPaint);
// 6. Poignée pistolet séparée
final gripPath = Path();
gripPath.moveTo(8.5, 18);
gripPath.lineTo(10.5, 18);
gripPath.lineTo(9.5, 23.5);
gripPath.lineTo(7.5, 23);
gripPath.close();
canvas.drawPath(gripPath, fillPaint);
// 7. Pontet et détente
final triggerGuard = Path();
triggerGuard.moveTo(11, 18);
triggerGuard.quadraticBezierTo(11.5, 20, 13, 19.5);
canvas.drawPath(triggerGuard, strokePaint..strokeWidth = 1.0);
}
/// Silhouette fidèle d'un Fusil à Pompe (Pump Action Shotgun : type Remington 870)
void _drawPumpShotgun(Canvas canvas, Paint fillPaint, Paint strokePaint) {
// 1. Crosse arrière traditionnelle ergonomique
final stockPath = Path();
stockPath.moveTo(1, 14.5);
stockPath.lineTo(9, 15.5);
stockPath.lineTo(9, 19.5);
stockPath.lineTo(6.5, 20); // poignée semi-pistolet
stockPath.lineTo(1, 21.5);
stockPath.close();
canvas.drawPath(stockPath, fillPaint);
// 2. Boîtier de culasse récepteur (Receiver)
final receiverPath = Path();
receiverPath.moveTo(9, 14);
receiverPath.lineTo(16.5, 14);
receiverPath.lineTo(16.5, 19);
receiverPath.lineTo(9, 19);
receiverPath.close();
canvas.drawPath(receiverPath, fillPaint);
// 3. Canon long calibre 12 (Barrel)
final barrelPath = Path();
barrelPath.moveTo(16.5, 14);
barrelPath.lineTo(31, 14);
barrelPath.lineTo(31, 15.5);
barrelPath.lineTo(16.5, 15.5);
barrelPath.close();
canvas.drawPath(barrelPath, fillPaint);
// Grain d'orge / mire avant sur le canon
canvas.drawCircle(const Offset(30, 13.2), 0.8, fillPaint);
// 4. Tube magasin inférieur sous le canon
final magTubePath = Path();
magTubePath.moveTo(16.5, 16);
magTubePath.lineTo(27, 16);
magTubePath.lineTo(27, 17.5);
magTubePath.lineTo(16.5, 17.5);
magTubePath.close();
canvas.drawPath(magTubePath, fillPaint);
// Bague de fixation canon / tube magasin
canvas.drawRect(const Rect.fromLTWH(26, 14, 1.2, 4), fillPaint);
// 5. Pompe mobile striée d'actionnement (Forend / Pump)
final pumpPath = Path();
pumpPath.addRRect(RRect.fromRectAndRadius(
const Rect.fromLTWH(18, 15.5, 6.5, 3.5),
const Radius.circular(0.8),
));
canvas.drawPath(pumpPath, fillPaint);
// Stries sur la pompe
for (double x = 19.5; x <= 23.5; x += 1.3) {
canvas.drawLine(
Offset(x, 16),
Offset(x, 18.5),
Paint()
..color = Colors.black.withValues(alpha: 0.4)
..strokeWidth = 0.6,
);
}
// 6. Pontet et détente
final triggerGuard = Path();
triggerGuard.moveTo(10, 19);
triggerGuard.quadraticBezierTo(11.5, 21.5, 13.5, 20);
triggerGuard.lineTo(13.5, 19);
canvas.drawPath(triggerGuard, strokePaint..strokeWidth = 1.0);
}
/// Silhouette fidèle d'une arme à air comprimé / cible plomb (Airgun)
void _drawAirgun(Canvas canvas, Paint fillPaint, Paint strokePaint) {
// Pistolet match 10m / Airgun olympique avec réservoir d'air cylindrique
// 1. Canon supérieur et boîtier
final barrelPath = Path();
barrelPath.moveTo(4, 12);
barrelPath.lineTo(28, 12);
barrelPath.lineTo(28, 14);
barrelPath.lineTo(4, 14);
barrelPath.close();
canvas.drawPath(barrelPath, fillPaint);
// Organes de visée match : dioptre arrière et tunnel de guidon avant
canvas.drawRect(const Rect.fromLTWH(4.5, 9.5, 3, 2.5), fillPaint);
canvas.drawRect(const Rect.fromLTWH(25, 10, 2.5, 2), fillPaint);
// 2. Bonbonne d'air comprimé cylindrique inférieure (Cylinder)
final tankPath = Path();
tankPath.addRRect(RRect.fromRectAndRadius(
const Rect.fromLTWH(10, 14.5, 15, 3),
const Radius.circular(1.5),
));
canvas.drawPath(tankPath, fillPaint);
// Manomètre avant
canvas.drawCircle(const Offset(25, 16), 1.2, strokePaint..strokeWidth = 0.8);
// 3. Poignée anatomique bois de tir sportif avec repose-paume
final gripPath = Path();
gripPath.moveTo(6, 14);
gripPath.lineTo(10, 14);
gripPath.lineTo(9.5, 23.5);
gripPath.lineTo(3.5, 22.5);
gripPath.close();
canvas.drawPath(gripPath, fillPaint);
// Tablette repose-paume réglable
canvas.drawRRect(
RRect.fromRectAndRadius(
const Rect.fromLTWH(2, 22.5, 9, 2.2),
const Radius.circular(0.8),
),
fillPaint,
);
// 4. Pontet match
final guardPath = Path();
guardPath.moveTo(10, 14.5);
guardPath.quadraticBezierTo(12, 18.5, 9.5, 18.5);
canvas.drawPath(guardPath, strokePaint..strokeWidth = 1.0);
}
@override
bool shouldRepaint(covariant _WeaponTypePainter oldDelegate) {
return oldDelegate.type != type || oldDelegate.color != color;
}
}