Pardus ETAP 23 İçin Flutter ile Sanal Laboratuvar

Pardus ETAP (Etkileşimli Tahta Arayüzü Projesi), okullardaki akıllı tahtalar için tasarlanmış, Linux tabanlı (Debian) bir işletim sistemidir. Flutter'ın Linux Desktop desteği sayesinde, tek bir kod tabanıyla hem yüksek performanslı hem de dokunmatik ekranlara (akıllı tahtalara) tam uyumlu uygulamalar geliştirebilirsiniz.


Eğitim teknolojilerinde etkileşim, öğrenmeyi kalıcı hale getiren en önemli unsurdur. Bu makalede, Pardus ETAP 23 yüklü etkileşimli tahtalarda çalışacak, öğrencilerin güvenle deney yapabileceği bir Sanal Laboratuvar uygulamasının Flutter ile nasıl geliştirileceğini adım adım inceleyeceğiz.

1. Neden Pardus ETAP ve Flutter?

  • Pardus ETAP: Okullarda yaygın olarak kullanılan, güvenli ve açık kaynaklı bir işletim sistemidir.

  • Flutter: Google'ın UI kiti, Linux masaüstü uygulamaları için "birinci sınıf" (first-class) destek sunar. Akıllı tahtaların donanımını (GPU) verimli kullanarak 60 FPS akıcı animasyonlar sağlar.

2. Geliştirme Ortamının Hazırlanması

Pardus üzerinde Flutter geliştirmek için öncelikle gerekli Linux kütüphanelerini kurmalısınız. Terminali açın ve aşağıdaki komutları uygulayın:

Bash:
# Sistem paketlerini güncelleme
sudo apt update

# Flutter Linux desteği için gerekli derleyiciler ve kütüphaneler
sudo apt install clang cmake ninja-build pkg-config libgtk-3-dev liblzma-dev

Projenin Oluşturulması

Uygulamamıza "sanal_lab" adını verelim:

Bash:
flutter create sanal_lab
cd sanal_lab

Flutter SDK'sını kurduktan sonra Linux masaüstü desteğini aktif hale getirin:

Bash:
flutter config --enable-linux-desktop

3. Proje Mimarisi: Sürükle ve Bırak (Drag & Drop)

Sanal laboratuvarın kalbi, kullanıcının bir nesneyi (örneğin bir direnci) alıp başka bir alana (devre tahtası) taşımasıdır. Flutter'da bu işlem Draggable ve DragTarget widget'ları ile yönetilir.

Temel Mantık

  1. Draggable (Sürüklenebilir Nesne): Öğrencinin parmağıyla tuttuğu element (Örn: HCL Asidi).

  2. DragTarget (Hedef Alan): Nesnenin bırakılacağı alan (Örn: Boş Beher).

  3. Feedback (Geri Bildirim): Sürükleme esnasında parmağın altında görünen yarı saydam kopya.

4. Kodlama: Kablo Çizme / Devre Kurma

Harika! Hem Fizik laboratuvarı için gerekli olan "Kablo Çizme / Devre Kurma" mantığını içeren kod örneğini, hem de uygulamanızı Pardus ETAP menüsüne sabitleyecek .desktop dosya yapılandırmasını aşağıda hazırladım.


1. Fizik Laboratuvarı: Elektrik Devresi ve Kablo Çizimi

Fizik labında (elektrik devreleri) ise en kritik olay, bileşenler arasına kablo çekmektir. Bunu Flutter'da CustomPaint ve GestureDetector kullanarak yapabiliriz.

İşte basit bir "Kablo Çizim Tahtası" örneği:

Dart:
import 'package:flutter/material.dart';

void main() {
  runApp(MaterialApp(home: CircuitBoard()));
}

class CircuitBoard extends StatefulWidget {
  @override
  _CircuitBoardState createState() => _CircuitBoardState();
}

class _CircuitBoardState extends State<CircuitBoard> {
  // Kabloların başlangıç ve bitiş noktalarını tutan liste
  List<List<Offset>> wires = [];
  List<Offset>? currentWire; // Şu an çizilmekte olan kablo

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text("Pardus Sanal Fizik Labı"), backgroundColor: Colors.indigo),
      body: Stack(
        children: [
          // 1. Katman: Çizilen Kablolar (Arka Plan)
          CustomPaint(
            painter: WirePainter(wires, currentWire),
            size: Size.infinite,
          ),
          
          // 2. Katman: Dokunmatik Algılama Alanı
          GestureDetector(
            onPanStart: (details) {
              setState(() {
                // Çizime başla: Başlangıç noktası parmağın değdiği yer
                currentWire = [details.localPosition, details.localPosition];
              });
            },
            onPanUpdate: (details) {
              setState(() {
                // Sürükledikçe bitiş noktasını güncelle
                if (currentWire != null) {
                  currentWire![1] = details.localPosition;
                }
              });
            },
            onPanEnd: (details) {
              setState(() {
                // Parmağı kaldırınca kabloyu listeye kaydet
                if (currentWire != null) {
                  wires.add(currentWire!);
                  currentWire = null;
                }
              });
            },
          ),

          // 3. Katman: Devre Elemanları (Örnek Pil ve Ampul)
          // Gerçek uygulamada bunları da Draggable yapabilirsiniz.
          Positioned(
            left: 50, top: 100,
            child: _buildComponent(Icons.battery_charging_full, "Pil", Colors.green),
          ),
          Positioned(
            right: 50, top: 100,
            child: _buildComponent(Icons.lightbulb, "Ampul", Colors.orange),
          ),
        ],
      ),
      floatingActionButton: FloatingActionButton(
        child: Icon(Icons.delete),
        onPressed: () => setState(() => wires.clear()), // Tahtayı temizle
      ),
    );
  }

  Widget _buildComponent(IconData icon, String label, Color color) {
    return Container(
      padding: EdgeInsets.all(16),
      decoration: BoxDecoration(
        color: Colors.white,
        border: Border.all(color: Colors.black45),
        borderRadius: BorderRadius.circular(8),
        boxShadow: [BoxShadow(blurRadius: 5, color: Colors.black12)]
      ),
      child: Column(
        children: [
          Icon(icon, size: 40, color: color),
          Text(label, style: TextStyle(fontWeight: FontWeight.bold)),
        ],
      ),
    );
  }
}

// Kabloları çizen ressam sınıfı
class WirePainter extends CustomPainter {
  final List<List<Offset>> wires;
  final List<Offset>? currentWire;

  WirePainter(this.wires, this.currentWire);

  @override
  void paint(Canvas canvas, Size size) {
    final paint = Paint()
      ..color = Colors.red // Kablo rengi
      ..strokeWidth = 4.0 // Kablo kalınlığı
      ..strokeCap = StrokeCap.round;

    // Tamamlanmış kabloları çiz
    for (var wire in wires) {
      canvas.drawLine(wire[0], wire[1], paint);
    }

    // Şu an çizilmekte olan kabloyu çiz (ön izleme)
    if (currentWire != null) {
      paint.color = Colors.red.withOpacity(0.5); // Çizilirken şeffaf olsun
      canvas.drawLine(currentWire![0], currentWire![1], paint);
    }
  }

  @override
  bool shouldRepaint(WirePainter oldDelegate) => true;
}

2. Uygulamayı Pardus Menüsüne Ekleme (.desktop Dosyası)

Uygulamanızı derledikten (flutter build linux --release) sonra, Pardus ETAP kullanan öğretmenlerin uygulamayı "Başlat Menüsü" veya "Uygulama Aratıcı" üzerinden bulabilmesi için bir .desktop dosyası oluşturmalısınız.

Adım 1: Metin düzenleyici ile yeni bir dosya oluşturun: sanallab.desktop

Adım 2: İçeriği aşağıdaki şablona göre doldurun:

Ini:
[Desktop Entry]
Version=1.0
Type=Application
Name=Sanal Laboratuvar
Comment=Pardus ETAP Fizik ve Kimya Simülasyonu
# Uygulamanızın derlenmiş dosyasının tam yolu (Örn: /opt klasörüne attıysanız)
Exec=/opt/sanal_lab/sanal_lab_app
# Uygulamanızın simgesinin yolu
Icon=/opt/sanal_lab/assets/icon.png
# Kategoriler uygulamanın menüde nerede çıkacağını belirler
Categories=Education;Science;Physics;Chemistry;
Terminal=false
StartupNotify=true
# Dokunmatik ekran dostu olduğunu belirtir (opsiyonel)
X-Pardus-Touch=true

Adım 3: Dosyayı sisteme yükleme

Bu dosyayı tüm kullanıcıların (öğretmen ve öğrenci oturumlarının) görebilmesi için şu dizine kopyalamalısınız:

Bash:
sudo cp sanallab.desktop /usr/share/applications/

Dosya izinlerini ayarlayın (çalıştırılabilir olması için):

Bash:
sudo chmod +x /usr/share/applications/sanallab.desktop

Artık Pardus menüsüne "Sanal Laboratuvar" yazdığınızda uygulamanız, verdiğiniz ikon ile birlikte görünecektir.


5. Pardus ETAP İçin Optimizasyon İpuçları

Akıllı tahtalar büyük dokunmatik ekranlardır. Bu nedenle UI tasarımında şunlara dikkat etmelisiniz:

  1. Büyük Dokunma Hedefleri: Butonlar ve sürüklenebilir nesneler en az 48x48 dp olmalıdır. Öğretmenler tahtayı genellikle parmaklarıyla kullanır, hassas fare hareketleri zordur.

  2. Yüksek Kontrast: Sınıf ortamında ışık yansıması olabilir. Renklerin net ve ayrıştırılabilir olduğundan emin olun.

  3. Font Boyutları: Tahtanın en arkasındaki öğrencinin de görebilmesi için yazılar büyük olmalıdır.

6. Uygulamayı Derleme ve Dağıtma

Uygulamayı tamamladığınızda Pardus üzerinde çalışacak çalıştırılabilir dosyayı üretmek için:

Bash:
flutter build linux --release

Çıktı dosyası build/linux/x64/release/bundle/ dizininde oluşacaktır. Bu klasörü herhangi bir Pardus ETAP tahtasına kopyalayıp içindeki çalıştırılabilir dosyayı çift tıklayarak uygulamanızı çalıştırabilirsiniz.

Okulda dağıtım için bir .desktop dosyası oluşturarak uygulamayı Pardus menüsüne de ekleyebilirsiniz.

Sonuç

Flutter ve Pardus ETAP kombinasyonu, milli eğitim teknolojileri için büyük bir potansiyel taşımaktadır. Bu makaledeki temel DragTarget mantığını kullanarak sadece kimya değil; elektrik devreleri, fizik kuvvet denemeleri veya biyoloji hücre modelleri gibi birçok farklı simülasyon geliştirebilirsiniz.

Basit çizimden öte, "Etkileşimli ve Mantıksal" bir devre simülasyonu hazırladım. Bu uygulama, sadece şekilleri göstermekle kalmayacak, devrenin doğru kurulup kurulmadığını kontrol edecek ve ampulü gerçekten yakacaktır.

Bu proje şunları içerir:

  1. Görsel Geri Bildirim: Lamba yandığında "Neon Glow" (Parlama) efekti verir.

  2. Mantık Kontrolü: Güç kaynağı, Anahtar ve Yük (Lamba) doğru sıralanmadan çalışmaz.

  3. Animasyonlu Anahtar: Anahtara basıldığında aç/kapa animasyonu oynar.

  4. Karanlık Mod UI: Elektrik deneyleri karanlık temada daha etkileyici görünür.

İşte Pardus ETAP akıllı tahtaları için modern bir "Seri Devre Simülatörü":

Gelişmiş Elektrik Devresi Kodu (Flutter)

Bu kodu yeni bir main.dart dosyasına yapıştırıp çalıştırabilirsiniz.

Dart:
import 'package:flutter/material.dart';

void main() {
  runApp(MaterialApp(
    debugShowCheckedModeBanner: false,
    theme: ThemeData.dark().copyWith(
      scaffoldBackgroundColor: Color(0xFF1E1E2C),
      primaryColor: Colors.amber,
    ),
    home: AdvancedCircuitLab(),
  ));
}

// Devre elemanlarının türleri
enum ComponentType { battery, switchDev, bulb, empty }

// Devre elemanı modeli
class CircuitComponent {
  ComponentType type;
  bool isSwitchOn; // Sadece anahtar için
  
  CircuitComponent({required this.type, this.isSwitchOn = false});
}

class AdvancedCircuitLab extends StatefulWidget {
  @override
  _AdvancedCircuitLabState createState() => _AdvancedCircuitLabState();
}

class _AdvancedCircuitLabState extends State<AdvancedCircuitLab> {
  // 3 Slotlu Basit Seri Devre: [Güç] -- [Anahtar] -- [Yük]
  List<CircuitComponent> circuitSlots = [
    CircuitComponent(type: ComponentType.empty),
    CircuitComponent(type: ComponentType.empty),
    CircuitComponent(type: ComponentType.empty),
  ];

  bool get isCircuitWorking {
    // 1. Slot Pil mi?
    bool hasPower = circuitSlots[0].type == ComponentType.battery;
    // 2. Slot Anahtar mı ve Açık mı?
    bool hasSwitch = circuitSlots[1].type == ComponentType.switchDev;
    bool switchOn = circuitSlots[1].isSwitchOn;
    // 3. Slot Lamba mı?
    bool hasBulb = circuitSlots[2].type == ComponentType.bulb;

    return hasPower && hasSwitch && switchOn && hasBulb;
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Pardus ETAP - Akıllı Devre Labı"),
        backgroundColor: Colors.transparent,
        elevation: 0,
        centerTitle: true,
      ),
      body: Row(
        children: [
          // SOL PANEL: Alet Çantası (Draggable Items)
          Expanded(
            flex: 1,
            child: Container(
              color: Color(0xFF2D2D44),
              child: Column(
                mainAxisAlignment: MainAxisAlignment.center,
                children: [
                  Text("Bileşenler", style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold)),
                  SizedBox(height: 30),
                  _buildDraggableItem(ComponentType.battery, Icons.battery_charging_full, "Güç Kaynağı", Colors.green),
                  _buildDraggableItem(ComponentType.switchDev, Icons.toggle_off, "Anahtar", Colors.blue),
                  _buildDraggableItem(ComponentType.bulb, Icons.lightbulb_outline, "Lamba", Colors.orange),
                  Spacer(),
                  IconButton(
                    icon: Icon(Icons.refresh, size: 30),
                    onPressed: _resetCircuit,
                    tooltip: "Tahtayı Temizle",
                  ),
                  SizedBox(height: 20),
                ],
              ),
            ),
          ),

          // SAĞ PANEL: Devre Tahtası (Drop Targets)
          Expanded(
            flex: 3,
            child: Center(
              child: Stack(
                alignment: Alignment.center,
                children: [
                  // Kablo Bağlantıları (Arka Plan Çizgileri)
                  Container(
                    width: 600,
                    height: 10,
                    color: isCircuitWorking ? Colors.amber.withOpacity(0.5) : Colors.grey.withOpacity(0.3),
                  ),
                  
                  // Slotlar
                  Row(
                    mainAxisAlignment: MainAxisAlignment.spaceEvenly,
                    children: [
                      _buildCircuitSlot(0, "Güç Yuvası"),
                      _buildCircuitSlot(1, "Kontrol Yuvası"),
                      _buildCircuitSlot(2, "Yük Yuvası"),
                    ],
                  ),
                ],
              ),
            ),
          ),
        ],
      ),
    );
  }

  // Sürüklenebilir Nesne Widget'ı
  Widget _buildDraggableItem(ComponentType type, IconData icon, String label, Color color) {
    return Draggable<ComponentType>(
      data: type,
      feedback: Material(
        color: Colors.transparent,
        child: Icon(icon, size: 80, color: color.withOpacity(0.8)),
      ),
      childWhenDragging: Icon(icon, size: 60, color: Colors.grey),
      child: Padding(
        padding: const EdgeInsets.symmetric(vertical: 20.0),
        child: Column(
          children: [
            Container(
              padding: EdgeInsets.all(10),
              decoration: BoxDecoration(
                shape: BoxShape.circle,
                color: Colors.white10,
              ),
              child: Icon(icon, size: 50, color: color),
            ),
            SizedBox(height: 5),
            Text(label, style: TextStyle(color: Colors.white70)),
          ],
        ),
      ),
    );
  }

  // Devre Yuvası (Drop Target)
  Widget _buildCircuitSlot(int index, String hint) {
    CircuitComponent component = circuitSlots[index];

    return DragTarget<ComponentType>(
      onAccept: (receivedType) {
        setState(() {
          circuitSlots[index] = CircuitComponent(type: receivedType);
        });
      },
      builder: (context, candidateData, rejectedData) {
        return Container(
          width: 140,
          height: 140,
          decoration: BoxDecoration(
            color: Color(0xFF3E3E55),
            borderRadius: BorderRadius.circular(15),
            border: Border.all(
              color: candidateData.isNotEmpty ? Colors.amber : Colors.white24,
              width: candidateData.isNotEmpty ? 3 : 1,
            ),
            boxShadow: isCircuitWorking && component.type == ComponentType.bulb 
                ? [BoxShadow(color: Colors.amber, blurRadius: 30, spreadRadius: 5)] 
                : [],
          ),
          child: component.type == ComponentType.empty
              ? Center(child: Text(hint, textAlign: TextAlign.center, style: TextStyle(color: Colors.white30)))
              : _buildActiveComponent(index, component),
        );
      },
    );
  }

  // Yuvanın içine yerleşmiş aktif bileşen
  Widget _buildActiveComponent(int index, CircuitComponent component) {
    switch (component.type) {
      case ComponentType.battery:
        return Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Icon(Icons.battery_charging_full, size: 60, color: Colors.greenAccent),
            Text("9V Pil", style: TextStyle(fontWeight: FontWeight.bold)),
          ],
        );
      case ComponentType.switchDev:
        return GestureDetector(
          onTap: () {
            setState(() {
              component.isSwitchOn = !component.isSwitchOn;
            });
          },
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              AnimatedContainer(
                duration: Duration(milliseconds: 300),
                child: Icon(
                  component.isSwitchOn ? Icons.toggle_on : Icons.toggle_off,
                  size: 70,
                  color: component.isSwitchOn ? Colors.green : Colors.red,
                ),
              ),
              Text(component.isSwitchOn ? "AÇIK" : "KAPALI"),
            ],
          ),
        );
      case ComponentType.bulb:
        return Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            AnimatedContainer(
              duration: Duration(milliseconds: 500),
              child: Icon(
                Icons.lightbulb,
                size: 70,
                color: isCircuitWorking ? Colors.amber : Colors.grey,
              ),
            ),
            Text("Lamba"),
          ],
        );
      default:
        return SizedBox();
    }
  }

  void _resetCircuit() {
    setState(() {
      circuitSlots = [
        CircuitComponent(type: ComponentType.empty),
        CircuitComponent(type: ComponentType.empty),
        CircuitComponent(type: ComponentType.empty),
      ];
    });
  }
}

Bu Uygulamanın "Havalı" Özellikleri Neler?

  1. Mantıksal Akış: Kullanıcı "Pili" Lamba yerine takabilir ama devre çalışmaz. Bu, öğrenciye devrenin sırasını ve mantığını (Kaynak -> Anahtar -> Yük) öğretir.

  2. Etkileşimli Anahtar: Ortadaki "Anahtar" (Switch) bileşeni sadece durmaz; üzerine tıklandığında (veya tahtada dokunulduğunda) On/Off durumu değişir. Bu state değişimi anında tüm devreyi etkiler.

  3. Glow Efekti (Parlama):

    • Lamba yandığında arkasında beliren BoxShadow (gölge), gerçek bir ışık yayılma efekti yaratır.

    • Kablolar (arkadaki çizgi), devre tamamlandığında gri renkten "elektrik akıyor" hissi veren şeffaf turuncuya döner.

  4. Akıllı Tahta Uyumu: UI elemanları (ikonlar ve kutular) büyük tutulmuştur. 140x140 piksellik kutular, tahta başında parmakla isabet ettirmeyi çok kolaylaştırır.

Ses efektleri (işitsel geri bildirim), uygulamanın "oyunlaştırma" (gamification) hissini artıracak ve öğrencilerin dikkatini daha çok çekecektir.

Flutter'da ses oynatmak için endüstri standardı olan audioplayers paketini kullanacağız. Pardus ETAP (Linux) üzerinde sesin sorunsuz çalışması için küçük bir sistem hazırlığı yapmamız gerekiyor.

İşte adım adım sesli devre simülasyonu:

1. Hazırlık: Pardus İçin Gerekli Kütüphane

Öncelikle Linux'un ses geliştirme kütüphanesini kurmalıyız. Terminali açın ve şu komutu girin:

Bash:
sudo apt update
sudo apt install libasound2-dev

2. Proje Ayarları (pubspec.yaml)

Projenizdeki pubspec.yaml dosyasını açın ve aşağıdaki iki eklemeyi yapın:

  1. Bağımlılık (Dependency): audioplayers ekleyin.

  2. Varlıklar (Assets): Ses dosyalarının yerini belirtin.

YAML:
dependencies:
  flutter:
    sdk: flutter
  audioplayers: ^6.0.0 # Güncel sürümü kullanıyoruz

flutter:
  assets:
    - assets/sounds/click.mp3   # Anahtar sesi
    - assets/sounds/electric.mp3 # Lamba yanma sesi

Not: Proje klasörünüzde assets isminde bir klasör, onun içine de sounds klasörü açıp internetten bulduğunuz kısa click.mp3 ve electric.mp3 dosyalarını buraya atın.

3. Sesli Devre Simülasyonu Kodları

Aşağıdaki kod, önceki örneğin ses efektleri entegre edilmiş halidir.

Değişiklikler:

  • AudioPlayer sınıfı eklendi.

  • Anahtara basıldığında "Tık" sesi çalıyor.

  • Devre tamamlanıp lamba yandığında "Elektrik" sesi çalıyor.

Dart:
import 'package:flutter/material.dart';
import 'package:audioplayers/audioplayers.dart'; // Ses paketi eklendi

void main() {
  runApp(MaterialApp(
    debugShowCheckedModeBanner: false,
    theme: ThemeData.dark().copyWith(
      scaffoldBackgroundColor: Color(0xFF1E1E2C),
    ),
    home: AudioCircuitLab(),
  ));
}

enum ComponentType { battery, switchDev, bulb, empty }

class CircuitComponent {
  ComponentType type;
  bool isSwitchOn;
  CircuitComponent({required this.type, this.isSwitchOn = false});
}

class AudioCircuitLab extends StatefulWidget {
  @override
  _AudioCircuitLabState createState() => _AudioCircuitLabState();
}

class _AudioCircuitLabState extends State<AudioCircuitLab> {
  // Ses oynatıcıyı tanımla
  final AudioPlayer _audioPlayer = AudioPlayer();

  List<CircuitComponent> circuitSlots = [
    CircuitComponent(type: ComponentType.empty),
    CircuitComponent(type: ComponentType.empty),
    CircuitComponent(type: ComponentType.empty),
  ];

  // Devre çalışıyor mu kontrolü
  bool get isCircuitWorking {
    bool hasPower = circuitSlots[0].type == ComponentType.battery;
    bool hasSwitch = circuitSlots[1].type == ComponentType.switchDev;
    bool switchOn = circuitSlots[1].isSwitchOn;
    bool hasBulb = circuitSlots[2].type == ComponentType.bulb;
    return hasPower && hasSwitch && switchOn && hasBulb;
  }

  // SES OYNATMA FONKSİYONU
  Future<void> _playSound(String fileName) async {
    // Ses dosyasını assets klasöründen oynat
    await _audioPlayer.play(AssetSource('sounds/$fileName'));
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Pardus ETAP - Sesli Fizik Labı"),
        backgroundColor: Colors.transparent,
        elevation: 0,
        centerTitle: true,
      ),
      body: Row(
        children: [
          // SOL PANEL: Alet Çantası
          Expanded(
            flex: 1,
            child: Container(
              color: Color(0xFF2D2D44),
              child: Column(
                mainAxisAlignment: MainAxisAlignment.center,
                children: [
                  Text("Bileşenler", style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold)),
                  SizedBox(height: 30),
                  _buildDraggableItem(ComponentType.battery, Icons.battery_charging_full, "Pil", Colors.green),
                  _buildDraggableItem(ComponentType.switchDev, Icons.toggle_off, "Anahtar", Colors.blue),
                  _buildDraggableItem(ComponentType.bulb, Icons.lightbulb_outline, "Lamba", Colors.orange),
                  Spacer(),
                  IconButton(
                    icon: Icon(Icons.refresh, size: 30),
                    onPressed: _resetCircuit,
                    tooltip: "Sıfırla",
                  ),
                  SizedBox(height: 20),
                ],
              ),
            ),
          ),

          // SAĞ PANEL: Devre Tahtası
          Expanded(
            flex: 3,
            child: Center(
              child: Stack(
                alignment: Alignment.center,
                children: [
                  // Kablolar
                  Container(
                    width: 600,
                    height: 10,
                    decoration: BoxDecoration(
                      color: isCircuitWorking ? Colors.amber.withOpacity(0.6) : Colors.grey.withOpacity(0.3),
                      boxShadow: isCircuitWorking ? [BoxShadow(color: Colors.amber, blurRadius: 20)] : [],
                    ),
                  ),
                  
                  // Yuvalar
                  Row(
                    mainAxisAlignment: MainAxisAlignment.spaceEvenly,
                    children: [
                      _buildCircuitSlot(0, "Güç Yuvası"),
                      _buildCircuitSlot(1, "Anahtar Yuvası"),
                      _buildCircuitSlot(2, "Lamba Yuvası"),
                    ],
                  ),
                ],
              ),
            ),
          ),
        ],
      ),
    );
  }

  Widget _buildDraggableItem(ComponentType type, IconData icon, String label, Color color) {
    return Draggable<ComponentType>(
      data: type,
      feedback: Material(
        color: Colors.transparent,
        child: Icon(icon, size: 80, color: color.withOpacity(0.8)),
      ),
      childWhenDragging: Icon(icon, size: 60, color: Colors.grey),
      child: Padding(
        padding: const EdgeInsets.symmetric(vertical: 20.0),
        child: Column(
          children: [
            Icon(icon, size: 50, color: color),
            SizedBox(height: 5),
            Text(label),
          ],
        ),
      ),
    );
  }

  Widget _buildCircuitSlot(int index, String hint) {
    CircuitComponent component = circuitSlots[index];

    return DragTarget<ComponentType>(
      onAccept: (receivedType) {
        setState(() {
          circuitSlots[index] = CircuitComponent(type: receivedType);
        });
        // Parça yerleşince tok bir ses çıkabilir (opsiyonel)
         _playSound('click.mp3'); 
      },
      builder: (context, candidateData, rejectedData) {
        return Container(
          width: 140,
          height: 140,
          decoration: BoxDecoration(
            color: Color(0xFF3E3E55),
            borderRadius: BorderRadius.circular(15),
            border: Border.all(
              color: candidateData.isNotEmpty ? Colors.amber : Colors.white24,
              width: 2,
            ),
            boxShadow: isCircuitWorking && component.type == ComponentType.bulb 
                ? [BoxShadow(color: Colors.amber, blurRadius: 40, spreadRadius: 5)] 
                : [],
          ),
          child: component.type == ComponentType.empty
              ? Center(child: Text(hint, style: TextStyle(color: Colors.white30)))
              : _buildActiveComponent(index, component),
        );
      },
    );
  }

  Widget _buildActiveComponent(int index, CircuitComponent component) {
    switch (component.type) {
      case ComponentType.battery:
        return Icon(Icons.battery_charging_full, size: 60, color: Colors.greenAccent);
      
      case ComponentType.switchDev:
        return GestureDetector(
          onTap: () {
            // 1. Önce sesi oynat
            _playSound('click.mp3');

            bool wasWorkingBefore = isCircuitWorking;

            setState(() {
              component.isSwitchOn = !component.isSwitchOn;
            });

            // 2. Eğer devre ŞİMDİ çalışmaya başladıysa ve ampul takılıysa başarı sesi çal
            if (!wasWorkingBefore && isCircuitWorking) {
              Future.delayed(Duration(milliseconds: 100), () {
                 _playSound('electric.mp3'); 
              });
            }
          },
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              Icon(
                component.isSwitchOn ? Icons.toggle_on : Icons.toggle_off,
                size: 70,
                color: component.isSwitchOn ? Colors.green : Colors.red,
              ),
              Text(component.isSwitchOn ? "AÇIK" : "KAPALI"),
            ],
          ),
        );
      
      case ComponentType.bulb:
        return Icon(
          Icons.lightbulb,
          size: 70,
          color: isCircuitWorking ? Colors.amber : Colors.grey,
        );
      
      default:
        return SizedBox();
    }
  }

  void _resetCircuit() {
    setState(() {
      circuitSlots = [CircuitComponent(type: ComponentType.empty), CircuitComponent(type: ComponentType.empty), CircuitComponent(type: ComponentType.empty)];
    });
  }
}

Eksik Kütüphaneleri Yükleyin

Terminali açın ve şu komutu yapıştırın:

Bash:
sudo apt-get update
sudo apt-get install libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev

(Bu komut, uygulamanızın Linux ses kartı ile konuşmasını sağlayan C++ başlık dosyalarını indirir.)

2. Projeyi Temizleyin ve Yeniden Derleyin

CMake bazen eski ayarları hafızada tutabilir (cache). Bu yüzden önce temizlik yapıp sonra tekrar derlememiz gerekiyor:

Bash:
flutter clean
flutter pub get
flutter build linux --release

Önemli İpuçları

  1. Ses Dosyası Bulma: "Mixkit" veya "Freesound.org" gibi sitelerden "Switch click sound" ve "Electric hum sound" araması yaparak ücretsiz ve telifsiz sesler indirebilirsiniz.

  2. Linux Ses Gecikmesi: Linux'ta bazen ilk ses oynatıldığında milisaniyelik bir gecikme olabilir. audioplayers genellikle bunu önbelleğe alır ancak çok kritikse sesleri önceden yüklemek (preload) mümkündür.

  3. Tahta Hoparlörü: Pardus ETAP tahtalarında ses varsayılan olarak kısıktır. Uygulamayı denemeden önce tahtanın sesinin açık olduğundan emin olun.

Bu haliyle uygulama hem görsel (ışık) hem işitsel (tık sesi) geri bildirim vererek tam bir laboratuvar deneyimi sunuyor.

Uygulamayı bir adım daha ileri taşıyarak **"Ohm Yasası"**nı simüle eden bir versiyona dönüştürelim.

Bu seferki eklemelerimiz şunlar olacak:

  1. Reosta (Ayarlı Direnç/Dimmer): Öğrenci bir sürgüyle (slider) direnci değiştirecek.

  2. Gerçek Zamanlı Parlaklık: Direnç arttıkça lambanın parlaklığı azalacak.

  3. Dijital Voltmetre: Devredeki voltaj değişimini ekranda sayısal olarak gösterecek.

İşte Pardus ETAP için Ohm Yasası Simülasyonu kodları:

Dart:
import 'package:flutter/material.dart';
import 'package:audioplayers/audioplayers.dart';

void main() {
  runApp(MaterialApp(
    debugShowCheckedModeBanner: false,
    theme: ThemeData.dark().copyWith(
      scaffoldBackgroundColor: Color(0xFF1E1E2C),
      sliderTheme: SliderThemeData(
        activeTrackColor: Colors.amber,
        thumbColor: Colors.amberAccent,
      ),
    ),
    home: OhmsLawLab(),
  ));
}

enum ComponentType { battery, rheostat, bulb, empty }

class CircuitComponent {
  ComponentType type;
  double resistanceValue; // Reosta için direnç değeri (0.0 - 1.0 arası)
  
  CircuitComponent({required this.type, this.resistanceValue = 0.5});
}

class OhmsLawLab extends StatefulWidget {
  @override
  _OhmsLawLabState createState() => _OhmsLawLabState();
}

class _OhmsLawLabState extends State<OhmsLawLab> {
  final AudioPlayer _audioPlayer = AudioPlayer();

  // 3 Slotlu Devre: [Güç] -- [Reosta] -- [Lamba]
  List<CircuitComponent> circuitSlots = [
    CircuitComponent(type: ComponentType.empty),
    CircuitComponent(type: ComponentType.empty),
    CircuitComponent(type: ComponentType.empty),
  ];

  // Devre tam mı?
  bool get isCircuitComplete {
    return circuitSlots[0].type == ComponentType.battery &&
           circuitSlots[1].type == ComponentType.rheostat &&
           circuitSlots[2].type == ComponentType.bulb;
  }

  // Voltaj Hesaplama (Simülasyon)
  // Pil 9V kabul edilir. Reosta direncine göre voltaj düşer.
  double get currentVoltage {
    if (!isCircuitComplete) return 0.0;
    // Basit mantık: Reosta değeri (0-1) arttıkça voltaj düşsün.
    // 1.0 dirençte voltaj 0'a yaklaşır (Lamba söner)
    // 0.0 dirençte voltaj 9V olur (Tam parlaklık)
    double resistance = circuitSlots[1].resistanceValue;
    return 9.0 * (1.0 - resistance);
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Pardus ETAP - Ohm Yasası Laboratuvarı"),
        backgroundColor: Colors.transparent,
        elevation: 0,
        actions: [
          // VOLTMETRE GÖSTERGESİ (Sağ Üst Köşe)
          Container(
            margin: EdgeInsets.all(10),
            padding: EdgeInsets.symmetric(horizontal: 20, vertical: 5),
            decoration: BoxDecoration(
              color: Colors.black,
              border: Border.all(color: Colors.red, width: 2),
              borderRadius: BorderRadius.circular(5),
            ),
            child: Row(
              children: [
                Icon(Icons.electric_meter, color: Colors.red),
                SizedBox(width: 10),
                Text(
                  "${currentVoltage.toStringAsFixed(1)} V",
                  style: TextStyle(fontFamily: 'monospace', fontSize: 24, color: Colors.redAccent),
                ),
              ],
            ),
          )
        ],
      ),
      body: Row(
        children: [
          // --- SOL PANEL: ARAÇLAR ---
          Expanded(
            flex: 1,
            child: Container(
              color: Color(0xFF2D2D44),
              child: Column(
                mainAxisAlignment: MainAxisAlignment.center,
                children: [
                  _buildDraggableItem(ComponentType.battery, Icons.battery_charging_full, "9V Pil", Colors.green),
                  _buildDraggableItem(ComponentType.rheostat, Icons.tune, "Reosta", Colors.purple),
                  _buildDraggableItem(ComponentType.bulb, Icons.lightbulb_outline, "Lamba", Colors.orange),
                  Spacer(),
                  IconButton(icon: Icon(Icons.refresh, size: 30), onPressed: _resetCircuit),
                  SizedBox(height: 20),
                ],
              ),
            ),
          ),

          // --- SAĞ PANEL: DENEY ALANI ---
          Expanded(
            flex: 3,
            child: Center(
              child: Stack(
                alignment: Alignment.center,
                children: [
                  // Kablolar (Voltaj varsa parlar)
                  Container(
                    width: 600,
                    height: 12,
                    decoration: BoxDecoration(
                      color: currentVoltage > 0.5 ? Colors.amber.withOpacity(0.4) : Colors.grey.withOpacity(0.2),
                      boxShadow: currentVoltage > 0.5 ? [BoxShadow(color: Colors.amber, blurRadius: 10 * (currentVoltage/9))] : [],
                    ),
                  ),
                  
                  // Yuvalar
                  Row(
                    mainAxisAlignment: MainAxisAlignment.spaceEvenly,
                    children: [
                      _buildCircuitSlot(0, "Güç Kaynağı"),
                      _buildCircuitSlot(1, "Direnç (Reosta)"),
                      _buildCircuitSlot(2, "Lamba"),
                    ],
                  ),
                ],
              ),
            ),
          ),
        ],
      ),
    );
  }

  // Sürüklenebilir Nesne
  Widget _buildDraggableItem(ComponentType type, IconData icon, String label, Color color) {
    return Draggable<ComponentType>(
      data: type,
      feedback: Material(color: Colors.transparent, child: Icon(icon, size: 80, color: color.withOpacity(0.8))),
      childWhenDragging: Icon(icon, size: 60, color: Colors.grey),
      child: Padding(
        padding: const EdgeInsets.symmetric(vertical: 20.0),
        child: Column(children: [Icon(icon, size: 50, color: color), SizedBox(height: 5), Text(label)]),
      ),
    );
  }

  // Devre Yuvası
  Widget _buildCircuitSlot(int index, String hint) {
    CircuitComponent component = circuitSlots[index];

    return DragTarget<ComponentType>(
      onAccept: (receivedType) {
        setState(() {
          circuitSlots[index] = CircuitComponent(type: receivedType);
          if (isCircuitComplete) _audioPlayer.play(AssetSource('sounds/click.mp3'));
        });
      },
      builder: (context, candidateData, rejectedData) {
        return Container(
          width: 160, // Tahtada daha rahat tutmak için genişlettik
          height: 180,
          decoration: BoxDecoration(
            color: Color(0xFF3E3E55),
            borderRadius: BorderRadius.circular(15),
            border: Border.all(color: candidateData.isNotEmpty ? Colors.amber : Colors.white12),
          ),
          child: component.type == ComponentType.empty
              ? Center(child: Text(hint, textAlign: TextAlign.center, style: TextStyle(color: Colors.white30)))
              : _buildActiveComponent(index, component),
        );
      },
    );
  }

  // Aktif Bileşen Görünümü
  Widget _buildActiveComponent(int index, CircuitComponent component) {
    switch (component.type) {
      case ComponentType.battery:
        return Column(mainAxisAlignment: MainAxisAlignment.center, children: [
          Icon(Icons.battery_charging_full, size: 60, color: Colors.greenAccent),
          Text("9V", style: TextStyle(fontWeight: FontWeight.bold))
        ]);
      
      case ComponentType.rheostat:
        return Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Icon(Icons.tune, size: 40, color: Colors.purpleAccent),
            Text("Direnç Ayarı"),
            // REOSTA SÜRGÜSÜ
            Slider(
              value: component.resistanceValue,
              onChanged: (newValue) {
                setState(() {
                  component.resistanceValue = newValue;
                });
              },
            ),
          ],
        );
      
      case ComponentType.bulb:
        // Lamba parlaklığı voltaja göre değişir (Opacity)
        double opacity = (currentVoltage / 9.0).clamp(0.1, 1.0); 
        return Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            AnimatedContainer(
              duration: Duration(milliseconds: 100),
              decoration: BoxDecoration(
                shape: BoxShape.circle,
                boxShadow: [
                  // Parlama Efekti
                  BoxShadow(color: Colors.amber.withOpacity(opacity), blurRadius: 40 * opacity, spreadRadius: 5 * opacity)
                ]
              ),
              child: Icon(Icons.lightbulb, size: 70, color: Colors.amber.withOpacity(opacity)),
            ),
            Text("Lamba"),
          ],
        );
      
      default:
        return SizedBox();
    }
  }

  void _resetCircuit() {
    setState(() {
      circuitSlots = [CircuitComponent(type: ComponentType.empty), CircuitComponent(type: ComponentType.empty), CircuitComponent(type: ComponentType.empty)];
    });
  }
}

Kaynak Kod: https://github.com/nuritiras/sanal_lab

Bu Sürümde Neler Öğreniliyor?

  1. Reosta (Sürgü) Kullanımı: Ortadaki bileşen (Reosta) artık statik değil. Sürgüyü (Slider) sağa sola çektikçe, setState tetiklenir ve voltaj yeniden hesaplanır.

  2. Ohm Yasası Görselleştirmesi:

    • Sürgüyü hareket ettirdiğinizde, sağ üstteki Voltmetre anlık olarak değişir (Örn: 9.0V -> 4.5V -> 1.2V).

    • Aynı anda Lamba Parlaklığı (Opacity ve Shadow) artar veya azalır.

  3. Matematiksel İlişki: Öğrenci, "Direnç artarsa, akım (parlaklık) ve lamba üzerindeki gerilim düşer" mantığını görerek deneyimler.

Pardus ETAP İçin İpucu

Bu uygulamada dokunmatik hassasiyeti çok önemlidir. Kod içerisindeki Slider widget'ı, akıllı tahtalarda parmakla çok rahat kontrol edilebilir. Ancak sürgünün çok küçük olmaması için SliderTheme içerisinde thumbColor ve boyutlandırmaları özelleştirebilirsiniz (kodda basit bir tema örneği ekledim).

Yorumlar

Bu blogdaki popüler yayınlar

Pardus ETAP 23 İçin Flutter ile Dijital "Öğrenci Seçici" Uygulaması

Uygulama: Pardus Logosunu Göster