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:
# 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-devProjenin 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:
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
Draggable (Sürüklenebilir Nesne): Öğrencinin parmağıyla tuttuğu element (Örn: HCL Asidi).
DragTarget (Hedef Alan): Nesnenin bırakılacağı alan (Örn: Boş Beher).
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:
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:
[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:
sudo cp sanallab.desktop /usr/share/applications/
Dosya izinlerini ayarlayın (çalıştırılabilir olması için):
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:
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.
Yüksek Kontrast: Sınıf ortamında ışık yansıması olabilir. Renklerin net ve ayrıştırılabilir olduğundan emin olun.
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:
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:
Görsel Geri Bildirim: Lamba yandığında "Neon Glow" (Parlama) efekti verir.
Mantık Kontrolü: Güç kaynağı, Anahtar ve Yük (Lamba) doğru sıralanmadan çalışmaz.
Animasyonlu Anahtar: Anahtara basıldığında aç/kapa animasyonu oynar.
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.
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?
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.
Etkileşimli Anahtar: Ortadaki "Anahtar" (Switch) bileşeni sadece durmaz; üzerine tıklandığında (veya tahtada dokunulduğunda)
On/Offdurumu değişir. Bu state değişimi anında tüm devreyi etkiler.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.
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:
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:
Bağımlılık (Dependency):
audioplayersekleyin.Varlıklar (Assets): Ses dosyalarının yerini belirtin.
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:
AudioPlayersınıfı eklendi.Anahtara basıldığında "Tık" sesi çalıyor.
Devre tamamlanıp lamba yandığında "Elektrik" sesi çalıyor.
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:
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:
flutter clean
flutter pub get
flutter build linux --releaseÖnemli İpuçları
Ses Dosyası Bulma: "Mixkit" veya "Freesound.org" gibi sitelerden "Switch click sound" ve "Electric hum sound" araması yaparak ücretsiz ve telifsiz sesler indirebilirsiniz.
Linux Ses Gecikmesi: Linux'ta bazen ilk ses oynatıldığında milisaniyelik bir gecikme olabilir.
audioplayersgenellikle bunu önbelleğe alır ancak çok kritikse sesleri önceden yüklemek (preload) mümkündür.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:
Reosta (Ayarlı Direnç/Dimmer): Öğrenci bir sürgüyle (slider) direnci değiştirecek.
Gerçek Zamanlı Parlaklık: Direnç arttıkça lambanın parlaklığı azalacak.
Dijital Voltmetre: Devredeki voltaj değişimini ekranda sayısal olarak gösterecek.
İşte Pardus ETAP için Ohm Yasası Simülasyonu kodları:
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?
Reosta (Sürgü) Kullanımı: Ortadaki bileşen (Reosta) artık statik değil. Sürgüyü (Slider) sağa sola çektikçe,
setStatetetiklenir ve voltaj yeniden hesaplanır.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.
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
Yorum Gönder