Aller au contenu

Flutter Mémo Rapide

Flutter - Build Apps for Any Screen

Flutter est la boîte à outils d'interface utilisateur de Google pour créer de belles applications compilées nativement pour mobile, web et desktop à partir d'un seul code source. Il est reconnu pour son interface utilisateur expressive, son cycle de développement rapide et ses performances natives.

[No text to translate]

Table des Matières

Would you like me to continue translating the remaining sections? I can proceed with the translations for sections 4-20 if you confirm.```bash

Download Flutter SDK from https://flutter.dev/docs/get-started/install

Extract the zip file

Add Flutter to your path (for the current terminal session)

export PATH=“$PATH:pwd/flutter/bin”

Add Flutter to your path permanently (macOS/Linux)

echo ‘export PATH=“$PATH:[PATH_TO_FLUTTER_GIT_DIRECTORY]/flutter/bin”’ >> ~/.zshrc source ~/.zshrc

Add Flutter to your path permanently (Windows)

Update your system’s environment variables


### Platform Setup

#### Android Setup
```bash
# Install Android Studio
# Download from https://developer.android.com/studio

# Set up Android emulator
# Open Android Studio > AVD Manager > Create Virtual Device

# Accept Android licenses
flutter doctor --android-licenses

iOS Setup (macOS only)

# Install Xcode
# Download from Mac App Store

# Install CocoaPods
sudo gem install cocoapods

# Set up iOS simulator
open -a Simulator

Flutter Doctor

# Check your environment and see a report of the status of your Flutter installation
flutter doctor

# Verbose output
flutter doctor -v

Getting Started

Create a New Project

# Create a new Flutter project
flutter create my_app

# Navigate to the project directory
cd my_app

# Create a project with a specific organization
flutter create --org com.example my_app

# Create a project with a specific template
flutter create -t module my_module

Run the App

# Run the app on an available device
flutter run

# Run on a specific device
flutter run -d chrome
flutter run -d 'iPhone 12'

# Run in release mode
flutter run --release

# Run with hot reload
# Press 'r' in the terminal

# Run with hot restart
# Press 'R' in the terminal

Project Structure

my_app/
├── android/          # Android-specific code
├── ios/              # iOS-specific code
├── lib/              # Main Dart code
│   ├── main.dart     # App entry point
│   ├── screens/      # Screen widgets
│   ├── widgets/      # Reusable widgets
│   ├── models/       # Data models
│   ├── services/     # API services
│   └── utils/        # Utility functions
├── test/             # Test files
├── pubspec.yaml      # Project dependencies and metadata
└── README.md         # Project documentation

Core Concepts

Dart Language Basics

// Variables
var name = 'Flutter';
String language = 'Dart';
int year = 2017;
double version = 2.12;
bool isAwesome = true;

// Functions
int add(int a, int b) {
  return a + b;
}

// Arrow syntax
int multiply(int a, int b) => a * b;

// Classes
class Person {
  String name;
  int age;

  Person(this.name, this.age);

  void sayHello() {
    print('Hello, my name is $name and I am $age years old.');
  }
}

// Asynchronous programming
Future<String> fetchData() async {
  await Future.delayed(Duration(seconds: 2));
  return 'Data fetched';
}

void main() async {
  var person = Person('John Doe', 30);
  person.sayHello();

  print('Fetching data...');
  var data = await fetchData();
  print(data);
}

Everything is a Widget

import 'package:flutter/material.dart';

void main() {
  runApp(
    MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: Text('My First Flutter App'),
        ),
        body: Center(
          child: Text('Hello, Flutter!'),
        ),
      ),
    ),
  );
}

BuildContext

// BuildContext provides information about the location of a widget in the widget tree.
// It is used to access theme data, navigation, and other inherited widgets.

class MyWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return RaisedButton(
      onPressed: () {
        Scaffold.of(context).showSnackBar(
          SnackBar(content: Text('Button pressed')),
        );
      },
      child: Text('Press Me'),
    );
  }
}

Stateless vs. Stateful Widgets

// StatelessWidget: A widget that does not require mutable state.
class MyStatelessWidget extends StatelessWidget {
  final String title;

  MyStatelessWidget({required this.title});

  @override
  Widget build(BuildContext context) {
    return Text(title);
  }
}

// StatefulWidget: A widget that has mutable state.
class MyStatefulWidget extends StatefulWidget {
  @override
  _MyStatefulWidgetState createState() => _MyStatefulWidgetState();
}

class _MyStatefulWidgetState extends State<MyStatefulWidget> {
  int _counter = 0;

  void _incrementCounter() {
    setState(() {
      _counter++;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        Text('Counter: $_counter'),
        RaisedButton(
          onPressed: _incrementCounter,
          child: Text('Increment'),
        ),
      ],
    );
  }
}

Widgets

Basic Widgets

import 'package:flutter/material.dart';

class BasicWidgets extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Basic Widgets')),
      body: SingleChildScrollView(
        padding: EdgeInsets.all(16.0),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            // Text
            Text('This is a Text widget', style: TextStyle(fontSize: 18)),
            SizedBox(height: 20),

            // Icon
            Icon(Icons.favorite, color: Colors.red, size: 30),
            SizedBox(height: 20),

            // Image
            Image.network('https://flutter.dev/images/flutter-logo-sharing.png'),
            SizedBox(height: 20),

            // Asset Image
            Image.asset('assets/images/logo.png'),
            SizedBox(height: 20),

            // Button
            RaisedButton(
              onPressed: () {},
              child: Text('RaisedButton'),
            ),
            SizedBox(height: 10),
            FlatButton(
              onPressed: () {},
              child: Text('FlatButton'),
            ),
            SizedBox(height: 10),
            IconButton(
              onPressed: () {},
              icon: Icon(Icons.thumb_up),
            ),
            SizedBox(height: 20),

            // TextField
            TextField(
              decoration: InputDecoration(
                labelText: 'Enter your name',
                border: OutlineInputBorder(),
              ),
            ),
            SizedBox(height: 20),

            // Checkbox
            CheckboxListTile(
              title: Text('Accept terms and conditions'),
              value: true,
              onChanged: (value) {},
            ),
            SizedBox(height: 20),

            // Radio
            RadioListTile(
              title: Text('Option 1'),
              value: 1,
              groupValue: 1,
              onChanged: (value) {},
            ),
            SizedBox(height: 20),

            // Switch
            SwitchListTile(
              title: Text('Enable notifications'),
              value: true,
              onChanged: (value) {},
            ),
            SizedBox(height: 20),

            // Slider
            Slider(
              value: 0.5,
              onChanged: (value) {},
            ),
            SizedBox(height: 20),

            // Progress Indicator
            CircularProgressIndicator(),
            SizedBox(height: 10),
            LinearProgressIndicator(),
          ],
        ),
      ),
    );
  }
}

Material Components

import 'package:flutter/material.dart';

class MaterialComponents extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Material Components')),
      drawer: Drawer(
        child: ListView(
          children: [
            DrawerHeader(child: Text('Drawer Header')),
            ListTile(title: Text('Item 1')),
            ListTile(title: Text('Item 2')),
          ],
        ),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            // Card
            Card(
              child: Padding(
                padding: EdgeInsets.all(16.0),
                child: Text('This is a Card'),
              ),
            ),
            SizedBox(height: 20),

            // Chip
            Chip(
              avatar: CircleAvatar(child: Text('A')),
              label: Text('Chip'),
            ),
            SizedBox(height: 20),

            // DataTable
            DataTable(
              columns: [
                DataColumn(label: Text('ID')),
                DataColumn(label: Text('Name')),
              ],
              rows: [
                DataRow(cells: [DataCell(Text('1')), DataCell(Text('John'))]),
                DataRow(cells: [DataCell(Text('2')), DataCell(Text('Jane'))]),
              ],
            ),
            SizedBox(height: 20),

            // SnackBar
            RaisedButton(
              onPressed: () {
                Scaffold.of(context).showSnackBar(
                  SnackBar(content: Text('This is a SnackBar')),
                );
              },
              child: Text('Show SnackBar'),
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {},
        child: Icon(Icons.add),
      ),
      bottomNavigationBar: BottomNavigationBar(
        items: [
          BottomNavigationBarItem(icon: Icon(Icons.home), label: 'Home'),
          BottomNavigationBarItem(icon: Icon(Icons.search), label: 'Search'),
          BottomNavigationBarItem(icon: Icon(Icons.person), label: 'Profile'),
        ],
      ),
    );
  }
}

Layouts

Single-Child Layout Widgets

import 'package:flutter/material.dart';

class SingleChildLayouts extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Single-Child Layouts')),
      body: Column(
        children: [
          // Container
          Container(
            color: Colors.blue,
            padding: EdgeInsets.all(16.0),
            margin: EdgeInsets.all(16.0),
            child: Text('Container'),
          ),

          // Center
          Center(
            child: Text('Center'),
          ),

          // Padding
          Padding(
            padding: EdgeInsets.all(16.0),
            child: Text('Padding'),
          ),

          // Align
          Align(
            alignment: Alignment.bottomRight,
            child: Text('Align'),
          ),

          // SizedBox
          SizedBox(
            width: 200,
            height: 100,
            child: Card(child: Center(child: Text('SizedBox'))),
          ),

          // AspectRatio
          AspectRatio(
            aspectRatio: 16 / 9,
            child: Container(color: Colors.green),
          ),

          // ConstrainedBox
          ConstrainedBox(
            constraints: BoxConstraints(maxWidth: 200, maxHeight: 100),
            child: Container(color: Colors.red),
          ),
        ],
      ),
    );
  }
}

Multi-Child Layout Widgets

import 'package:flutter/material.dart';

class MultiChildLayouts extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Multi-Child Layouts')),
      body: Column(
        children: [
          // Row
          Row(
            mainAxisAlignment: MainAxisAlignment.spaceEvenly,
            children: [Text('Row 1'), Text('Row 2'), Text('Row 3')],
          ),

          // Column
          Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [Text('Column 1'), Text('Column 2'), Text('Column 3')],
          ),

          // Stack
          Stack(
            children: [
              Container(width: 100, height: 100, color: Colors.red),
              Container(width: 80, height: 80, color: Colors.green),
              Container(width: 60, height: 60, color: Colors.blue),
            ],
          ),

          // ListView
          Expanded(
            child: ListView(
              children: [
                ListTile(title: Text('Item 1')),
                ListTile(title: Text('Item 2')),
                ListTile(title: Text('Item 3')),
              ],
            ),
          ),

          // GridView
          Expanded(
            child: GridView.count(
              crossAxisCount: 2,
              children: List.generate(4, (index) => Center(child: Text('Item $index'))),
            ),
          ),
        ],
      ),
    );
  }
}
// Navigate to a new screen
Navigator.push(
  context,
  MaterialPageRoute(builder: (context) => DetailsScreen()),
);

// Navigate back
Navigator.pop(context);

// Navigate with data
Navigator.push(
  context,
  MaterialPageRoute(
    builder: (context) => DetailsScreen(item: item),
  ),
);

// Receive data from a screen
final result = await Navigator.push(
  context,
  MaterialPageRoute(builder: (context) => SelectionScreen()),
);

// Send data back
Navigator.pop(context, 'Yes');
// pubspec.yaml
dependencies:
  flutter:
    sdk: flutter
  go_router: ^3.0.0

// main.dart
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp.router(
      routeInformationParser: _router.routeInformationParser,
      routerDelegate: _router.routerDelegate,
    );
  }

  final _router = GoRouter(
    routes: [
      GoRoute(
        path: '/',
        builder: (context, state) => HomeScreen(),
      ),
      GoRoute(
        path: '/details/:id',
        builder: (context, state) => DetailsScreen(id: state.params['id']!),
      ),
    ],
  );
}

// Navigate with go_router
context.go('/details/123');
context.push('/details/456');

State Management

Provider

// pubspec.yaml
dependencies:
  provider: ^6.0.0

// main.dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

void main() {
  runApp(
    ChangeNotifierProvider(
      create: (context) => Counter(),
      child: MyApp(),
    ),
  );
}

class Counter with ChangeNotifier {
  int _count = 0;
  int get count => _count;

  void increment() {
    _count++;
    notifyListeners();
  }
}

// Usage in a widget
class CounterScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Provider Example')),
      body: Center(
        child: Text('Count: ${context.watch<Counter>().count}'),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () => context.read<Counter>().increment(),
        child: Icon(Icons.add),
      ),
    );
  }
}
```### BLoC (Business Logic Component)
```dart
// pubspec.yaml
dependencies:
  flutter_bloc: ^8.0.0

// counter_event.dart
abstract class CounterEvent {}
class Increment extends CounterEvent {}

// counter_bloc.dart
import 'package:bloc/bloc.dart';
import 'counter_event.dart';

class CounterBloc extends Bloc<CounterEvent, int> {
  CounterBloc() : super(0) {
    on<Increment>((event, emit) => emit(state + 1));
  }
}

// main.dart
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';

void main() {
  runApp(
    BlocProvider(
      create: (context) => CounterBloc(),
      child: MyApp(),
    ),
  );
}

// Usage in a widget
class CounterScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('BLoC Example')),
      body: Center(
        child: BlocBuilder<CounterBloc, int>(
          builder: (context, count) => Text('Count: $count'),
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () => context.read<CounterBloc>().add(Increment()),
        child: Icon(Icons.add),
      ),
    );
  }
}
```### Riverpod
```dart
// pubspec.yaml
dependencies:
  flutter_riverpod: ^1.0.0

// main.dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

final counterProvider = StateProvider((ref) => 0);

void main() {
  runApp(ProviderScope(child: MyApp()));
}

// Usage in a widget
class CounterScreen extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final count = ref.watch(counterProvider);

    return Scaffold(
      appBar: AppBar(title: Text('Riverpod Example')),
      body: Center(
        child: Text('Count: $count'),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () => ref.read(counterProvider.state).state++,
        child: Icon(Icons.add),
      ),
    );
  }
}
```## Mise en Réseau

### Package http
```dart
// pubspec.yaml
dependencies:
  http: ^0.13.0

// services/api_service.dart
import 'package:http/http.dart' as http;
import 'dart:convert';

class ApiService {
  Future<Map<String, dynamic>> fetchData() async {
    final response = await http.get(Uri.parse('https://jsonplaceholder.typicode.com/posts/1'));
    if (response.statusCode == 200) {
      return json.decode(response.body);
    } else {
      throw Exception('Failed to load data');
    }
  }
}
```### Package dio
```dart
// pubspec.yaml
dependencies:
  dio: ^4.0.0

// services/api_service.dart
import 'package:dio/dio.dart';

class ApiService {
  final dio = Dio();

  Future<Map<String, dynamic>> fetchData() async {
    try {
      final response = await dio.get('https://jsonplaceholder.typicode.com/posts/1');
      return response.data;
    } catch (e) {
      throw Exception('Failed to load data');
    }
  }
}
```## Persistance

### shared_preferences
```dart
// pubspec.yaml
dependencies:
  shared_preferences: ^2.0.0

// utils/storage.dart
import 'package:shared_preferences/shared_preferences.dart';

class Storage {
  Future<void> saveUsername(String username) async {
    final prefs = await SharedPreferences.getInstance();
    await prefs.setString('username', username);
  }

  Future<String?> getUsername() async {
    final prefs = await SharedPreferences.getInstance();
    return prefs.getString('username');
  }
}
```### sqflite
```dart
// pubspec.yaml
dependencies:
  sqflite: ^2.0.0
  path: ^1.8.0

// services/database_service.dart
import 'package:sqflite/sqflite.dart';
import 'package:path/path.dart';

class DatabaseService {
  late Database _database;

  Future<void> initDatabase() async {
    _database = await openDatabase(
      join(await getDatabasesPath(), 'my_database.db'),
      onCreate: (db, version) {
        return db.execute(
          'CREATE TABLE users(id INTEGER PRIMARY KEY, name TEXT, age INTEGER)',
        );
      },
      version: 1,
    );
  }

  Future<void> insertUser(Map<String, dynamic> user) async {
    await _database.insert('users', user, conflictAlgorithm: ConflictAlgorithm.replace);
  }

  Future<List<Map<String, dynamic>>> getUsers() async {
    return await _database.query('users');
  }
}
```## Animations

### Animations Implicites
```dart
class ImplicitAnimation extends StatefulWidget {
  @override
  _ImplicitAnimationState createState() => _ImplicitAnimationState();
}

class _ImplicitAnimationState extends State<ImplicitAnimation> {
  bool _isBig = false;

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: () => setState(() => _isBig = !_isBig),
      child: AnimatedContainer(
        duration: Duration(seconds: 1),
        width: _isBig ? 200 : 100,
        height: _isBig ? 200 : 100,
        color: _isBig ? Colors.blue : Colors.red,
      ),
    );
  }
}
```### Animations Explicites
```dart
class ExplicitAnimation extends StatefulWidget {
  @override
  _ExplicitAnimationState createState() => _ExplicitAnimationState();
}

class _ExplicitAnimationState extends State<ExplicitAnimation> with SingleTickerProviderStateMixin {
  late AnimationController _controller;
  late Animation<double> _animation;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(vsync: this, duration: Duration(seconds: 2));
    _animation = Tween<double>(begin: 0, end: 300).animate(_controller)
      ..addListener(() => setState(() {}));
    _controller.forward();
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Container(
      width: _animation.value,
      height: _animation.value,
      color: Colors.green,
    );
  }
}
```## Intégration de Plateforme

### MethodChannel
```dart
// Dart side
import 'package:flutter/services.dart';

const platform = MethodChannel('com.example.myapp/channel');

Future<String> getBatteryLevel() async {
  try {
    final int result = await platform.invokeMethod('getBatteryLevel');
    return 'Battery level at $result % .';
  } on PlatformException catch (e) {
    return "Failed to get battery level: '${e.message}'.";
  }
}

// Android side (MainActivity.java)
import io.flutter.embedding.android.FlutterActivity;
import io.flutter.plugin.common.MethodChannel;

public class MainActivity extends FlutterActivity {
  private static final String CHANNEL = "com.example.myapp/channel";

  @Override
  public void configureFlutterEngine(@NonNull FlutterEngine flutterEngine) {
    super.configureFlutterEngine(flutterEngine);
    new MethodChannel(flutterEngine.getDartExecutor().getBinaryMessenger(), CHANNEL)
        .setMethodCallHandler(
          (call, result) -> {
            if (call.method.equals("getBatteryLevel")) {
              // ... get battery level
              result.success(batteryLevel);
            }
          }
        );
  }
}
```## Tests

### Tests Unitaires
```dart
// test/counter_test.dart
import 'package:test/test.dart';
import 'package:my_app/counter.dart';

void main() {
  test('Counter value should be incremented', () {
    final counter = Counter();
    counter.increment();
    expect(counter.value, 1);
  });
}
```### Tests de Widgets
```dart
// test/widget_test.dart
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:my_app/main.dart';

void main() {
  testWidgets('Counter increments smoke test', (WidgetTester tester) async {
    await tester.pumpWidget(MyApp());

    expect(find.text('0'), findsOneWidget);
    expect(find.text('1'), findsNothing);

    await tester.tap(find.byIcon(Icons.add));
    await tester.pump();

    expect(find.text('0'), findsNothing);
    expect(find.text('1'), findsOneWidget);
  });
}
```### Tests d'Intégration
```dart
// test_driver/app_test.dart
import 'package:flutter_driver/flutter_driver.dart';
import 'package:test/test.dart';

void main() {
  group('Counter App', () {
    FlutterDriver driver;

    setUpAll(() async {
      driver = await FlutterDriver.connect();
    });

    tearDownAll(() async {
      if (driver != null) {
        driver.close();
      }
    });

    test('increments the counter', () async {
      final counterTextFinder = find.byValueKey('counter');
      final buttonFinder = find.byValueKey('increment');

      expect(await driver.getText(counterTextFinder), '0');

      await driver.tap(buttonFinder);

      expect(await driver.getText(counterTextFinder), '1');
    });
  });
}
```## Débogage

### DevTools
```bash
# Open DevTools from the terminal
flutter pub global activate devtools
flutter pub global run devtools

# Or from VS Code / Android Studio
```### Journalisation
```dart
import 'dart:developer' as developer;

void myFunction() {
  developer.log('This is a log message', name: 'my.app.category');
}
```## Déploiement

### Android
```bash
# Build an APK
flutter build apk

# Build an App Bundle
flutter build appbundle
```### iOS
```bash
# Build an IPA
flutter build ipa
```### Web
```bash
# Build for web
flutter build web
```## Meilleures Pratiques

- **Suivez les directives Dart efficaces** : Écrivez du code Dart propre et maintenable.
- **Organisez la structure de votre projet** : Gardez votre code organisé et facile à naviguer.
- **Utilisez une solution de gestion d'état** : Choisissez une solution de gestion d'état qui convient à la complexité de votre application.
- **Écrivez des tests** : Écrivez des tests unitaires, de widgets et d'intégration pour vous assurer que votre application fonctionne correctement.
- **Optimisez les performances** : Utilisez DevTools pour identifier et corriger les goulots d'étranglement de performance.
- **Gérez les erreurs avec élégance** : Utilisez des blocs try-catch et des widgets d'erreur pour gérer les erreurs.

## Résolution de Problèmes

### Problèmes Courants
- **Problèmes spécifiques à la plateforme** : Vérifiez la configuration de votre plateforme (Android Studio, Xcode).
- **Conflits de dépendances** : Exécutez `flutter pub deps`pour vérifier les conflits de dépendances.
- **Erreurs de compilation** : Exécutez `flutter clean`puis 

Would you like me to fill in the specific commands for items 19 and 20, or do you want to provide those?`flutter pub get`pour nettoyer vos fichiers de build.

---

## Résumé

Flutter est un toolkit d'interface utilisateur puissant et flexible pour créer des applications multiplateformes de haute qualité. Ses principales caractéristiques incluent :

- **Développement Rapide** : Le rechargement à chaud vous permet de voir les modifications instantanément.
- **Interface Utilisateur Expressive** : Créez des interfaces utilisateur magnifiques et personnalisées avec un ensemble riche de widgets.
- **Performance Native** : Les applications Flutter sont compilées en code natif, offrant une excellente performance.
- **Base de Code Unique** : Écrivez une fois, exécutez sur mobile, web et desktop.
- **Communauté Croissante** : Une communauté large et active fournit du support et un écosystème riche de packages.

Flutter est un excellent choix pour les développeurs qui souhaitent créer des applications performantes et élégantes pour plusieurs plateformes avec une base de code unique.

<script>
function copyToClipboard() {
    const commands = document.querySelectorAll('code');
    let allCommands = '';
    commands.forEach(cmd => allCommands += cmd.textContent + '\n');
    navigator.clipboard.writeText(allCommands);
    alert('All commands copied to clipboard!');
}

function generatePDF() {
    window.print();
}
</script>