feat: implement AI export service and establish project navigation and theming infrastructure + acces internet
Build & Release Android APK / build-apk (push) Successful in 23m34s
Build & Release Android APK / build-apk (push) Successful in 23m34s
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user