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

"Kapalı Kartlar" (Memory Game mantığı), dokunmatik ekranlı etkileşimli tahtalarda (ETAP) öğrencilerin fiziksel olarak tahtaya kalkıp etkileşime girmesi için en heyecan verici yöntemlerden biridir. Flutter'ın animasyon yetenekleri bu "takla atma" (flip) efekti için biçilmiş kaftandır.

Pardus ETAP 23 üzerinde çalışacak, dokunmatik uyumlu ve Linux masaüstü çıktısı alabileceğin "Şanslı Kartlar" uygulaması:

Bu tasarımda:

  • Dinamik Izgara: Sınıfta kaç öğrenci varsa (veya Excel'den kaç kişi geldiyse), ekranı otomatik olarak onlara böler.

  • 3D Animasyon: Karta dokunulduğunda kart gerçekçi bir şekilde (Y ekseninde) döner.

  • Gizem: Kartların arkası "soru işareti" veya renkli desenlidir.

  • Ses: Kart çevirme sesi ("Whoosh") ve isim açılınca alkış sesi.

Bölüm 1: Geliştirme Ortamının Hazırlanması

Pardus üzerinde Flutter ile masaüstü uygulama geliştirebilmek için temel derleme araçlarına ihtiyacımız vardır. Terminali açın ve aşağıdaki komutları sırasıyla uygulayın.

1. Sistem Gereksinimleri

Bash:
sudo apt update
sudo apt install clang cmake ninja-build pkg-config libgtk-3-dev liblzma-dev

2. Projenin Oluşturulması

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

Bash:
flutter create ogrenci_secici
cd ogrenci_secici

Linux masaüstü desteğinin açık olduğundan emin olun:

Bash:

Bölüm 2: Kütüphanelerin ve Varlıkların Eklenmesi

Uygulamanın yeteneklerini artırmak için hazır paketler kullanacağız.

1. Paketlerin Yüklenmesi

Terminalde proje klasöründeyken şu komutu girin:

Bash:
flutter pub add rxdart file_picker excel audioplayers window_manager
  • file_picker: Excel dosyasını seçmek için.

  • excel: Seçilen dosyayı okumak için.

  • audioplayers: Ses efektleri için.

  • window_manager: Kiosk Modunda (Tam Ekran, Kenarlıksız)

2. Ses Dosyalarının Ayarlanması

Projenizin ana dizininde (lib klasörünün yanında) şu klasör yapısını oluşturun ve içine iki adet ses dosyası (.mp3 veya .wav) atın:

  • assets/

    • sounds/

      • cevirme.mp3 (Çark dönerken çalacak ses)

      • alkis.mp3 (Sonuç açıklandığında çalacak ses)

3. Pubspec.yaml Yapılandırması

pubspec.yaml dosyasını açın ve flutter: bölümünün altına assets yolunu ekleyin:

YAML:
flutter:
  uses-material-design: true
  assets:
    - assets/sounds/

Bölüm 3: Kodlama

İşte "Kapalı Kartlar" modu için hazırladığım tam kod (lib/main.dart):

Dart:
import 'dart:async';
import 'dart:io';
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:file_picker/file_picker.dart';
// ÖNEMLİ: Excel kütüphanesindeki Border ile Flutter'ınkini karıştırmasın diye gizliyoruz
import 'package:excel/excel.dart' hide Border;
import 'package:audioplayers/audioplayers.dart';
import 'package:window_manager/window_manager.dart';

void main() async {
WidgetsFlutterBinding.ensureInitialized();
await windowManager.ensureInitialized();

// Kiosk Modu Ayarları (Tam Ekran, Başlıksız)
WindowOptions windowOptions = const WindowOptions(
size: Size(1280, 720),
center: true,
backgroundColor: Color(0xFF263238), // Koyu Arka Plan
skipTaskbar: false,
titleBarStyle: TitleBarStyle.hidden,
fullScreen: true,
);

windowManager.waitUntilReadyToShow(windowOptions, () async {
await windowManager.show();
await windowManager.focus();
});

runApp(const OgrenciSeciciApp());
}

class OgrenciSeciciApp extends StatelessWidget {
const OgrenciSeciciApp({super.key});

@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Şans Kartları',
debugShowCheckedModeBanner: false,
theme: ThemeData(
primarySwatch: Colors.teal,
scaffoldBackgroundColor: const Color(0xFF263238),
fontFamily: 'Sans', // Pardus uyumlu font
useMaterial3: false,
),
home: const HomePage(),
);
}
}

class HomePage extends StatefulWidget {
const HomePage({super.key});

@override
State<HomePage> createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
// Öğrenci Havuzu
List<String> allStudents = [
'Öğrenci 1',
'Öğrenci 2',
'Öğrenci 3',
'Öğrenci 4',
];

// Ekrana dağıtılan kartların listesi
List<String> cardAssignments = [];

// Kartların açık/kapalı durumu
List<bool> cardRevealedState = [];

final TextEditingController _textController = TextEditingController();
final AudioPlayer _soundPlayer = AudioPlayer();

@override
void initState() {
super.initState();

// İlk açılışta varsayılan listeyi karıştır
_resetAndShuffleCards();

// Uygulama açılınca Excel kontrolü yap
WidgetsBinding.instance.addPostFrameCallback((_) {
_loadStartupExcel();
});
}

@override
void dispose() {
_textController.dispose();
_soundPlayer.dispose();
super.dispose();
}

// --- KART MANTIĞI ---
void _resetAndShuffleCards() {
setState(() {
// Listeyi kopyala ve karıştır
cardAssignments = List.from(allStudents)..shuffle();
// Tüm kartları kapat
cardRevealedState = List.filled(cardAssignments.length, false);
});
}

void _onCardTapped(int index) {
if (cardRevealedState[index]) return; // Zaten açıksa işlem yapma

// 1. Çevirme Sesi
_playSound('cevirme.mp3');

// 2. Kartı Aç
setState(() {
cardRevealedState[index] = true;
});

// 3. Alkış ve Popup (Gecikmeli)
Future.delayed(const Duration(milliseconds: 600), () {
_playSound('alkis.mp3');
_showWinnerDialog(cardAssignments[index]);
});
}

// --- SES ---
Future<void> _playSound(String fileName) async {
await _soundPlayer.stop();
// assets/sounds/ klasöründe olduğundan emin olun
await _soundPlayer.play(AssetSource('sounds/$fileName'));
}

// --- POPUP (SONUÇ EKRANI) ---
void _showWinnerDialog(String name) {
showDialog(
context: context,
barrierDismissible: true,
builder: (context) {
return AlertDialog(
backgroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.star, size: 80, color: Colors.amber),
const SizedBox(height: 10),
const Text(
"ŞANSLI KİŞİ",
style: TextStyle(
color: Colors.grey,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 10),
Text(
name,
textAlign: TextAlign.center,
style: const TextStyle(
fontSize: 40,
fontWeight: FontWeight.bold,
color: Colors.teal,
),
),
const SizedBox(height: 20),
ElevatedButton(
onPressed: () => Navigator.pop(context),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.teal,
padding: const EdgeInsets.symmetric(
horizontal: 30,
vertical: 10,
),
),
child: const Text(
"TAMAM",
style: TextStyle(color: Colors.white),
),
),
],
),
);
},
);
}

// --- OTOMATİK EXCEL YÜKLEME (MASAÜSTÜ DESTEKLİ) ---
Future<void> _loadStartupExcel() async {
try {
List<String> pathsToCheck = [];

// 1. Ev Dizinini Bul (/home/kullanici)
String? home = Platform.environment['HOME'];
if (home != null) {
// Pardus (Türkçe) Masaüstü
pathsToCheck.add("$home/Masaüstü/liste.xlsx");
// İngilizce Desktop
pathsToCheck.add("$home/Desktop/liste.xlsx");
}

// 2. Uygulamanın Kendi Klasörü
String exePath = File(Platform.resolvedExecutable).parent.path;
pathsToCheck.add("$exePath/liste.xlsx");

File? foundFile;
String loadedFrom = "";

// 3. Dosyaları Kontrol Et
for (String path in pathsToCheck) {
File f = File(path);
if (await f.exists()) {
foundFile = f;
loadedFrom = path;
break;
}
}

// 4. Bulunduysa Yükle
if (foundFile != null) {
var bytes = await foundFile.readAsBytes();
_parseAndLoadExcel(bytes);

if (mounted) {
String message =
loadedFrom.contains("Masaüstü") || loadedFrom.contains("Desktop")
? "Masaüstündeki liste yüklendi!"
: "Otomatik liste yüklendi.";

ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(message),
backgroundColor: Colors.teal[700],
duration: const Duration(seconds: 3),
),
);
}
}
} catch (e) {
print("Oto yükleme hatası: $e");
}
}

// --- MANUEL EXCEL SEÇME ---
Future<void> _importExcel() async {
try {
FilePickerResult? result = await FilePicker.platform.pickFiles(
type: FileType.custom,
allowedExtensions: ['xlsx', 'xls'],
);

if (result != null) {
var file = File(result.files.single.path!);
_parseAndLoadExcel(file.readAsBytesSync());
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text("Liste Başarıyla Güncellendi")),
);
}
}
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text("Hata: $e"), backgroundColor: Colors.red),
);
}
}
}

// --- EXCEL PARSE (OKUMA) ---
void _parseAndLoadExcel(List<int> bytes) {
var excel = Excel.decodeBytes(bytes);
List<String> newItems = [];
for (var table in excel.tables.keys) {
for (var row in excel.tables[table]!.rows) {
if (row.isNotEmpty && row[0] != null) {
String cellValue = row[0]!.value.toString();
if (cellValue.trim().isNotEmpty && cellValue != "null") {
newItems.add(cellValue);
}
}
}
break; // Sadece ilk sayfa
}

if (newItems.isNotEmpty) {
setState(() {
allStudents = newItems;
_resetAndShuffleCards(); // Yeni liste gelince kartları yeniden dağıt
});
}
}

// --- MANUEL EKLEME ---
void _addStudent() {
if (_textController.text.trim().isNotEmpty) {
setState(() {
allStudents.add(_textController.text.trim());
_textController.clear();
_resetAndShuffleCards();
});
}
}

// --- ARAYÜZ ---
@override
Widget build(BuildContext context) {
return Scaffold(
body: Row(
children: [
// SOL PANEL (KONTROLLER)
Expanded(
flex: 2,
child: Container(
color: Colors.white,
padding: const EdgeInsets.all(16),
child: Column(
children: [
// Çıkış Butonu (Kiosk Modu İçin Şart)
Align(
alignment: Alignment.centerLeft,
child: IconButton(
icon: const Icon(
Icons.power_settings_new,
color: Colors.red,
size: 32,
),
onPressed: () async => await windowManager.close(),
tooltip: "Uygulamadan Çık",
),
),
const Divider(),

// Karıştır Butonu
ElevatedButton.icon(
onPressed: _resetAndShuffleCards,
icon: const Icon(Icons.shuffle, size: 28),
label: const Text(
"Kartları Karıştır\nve Sıfırla",
textAlign: TextAlign.center,
),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.teal,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 20),
minimumSize: const Size(double.infinity, 80),
),
),
const SizedBox(height: 20),

// Excel Yükle Butonu
OutlinedButton.icon(
onPressed: _importExcel,
icon: const Icon(Icons.file_upload),
label: const Text("Farklı Excel Seç"),
style: OutlinedButton.styleFrom(
minimumSize: const Size(double.infinity, 50),
),
),

const Spacer(),

// Manuel Ekleme
TextField(
controller: _textController,
decoration: InputDecoration(
labelText: "Öğrenci Ekle",
border: const OutlineInputBorder(),
suffixIcon: IconButton(
icon: const Icon(Icons.add_circle, color: Colors.teal),
onPressed: _addStudent,
),
),
onSubmitted: (_) => _addStudent(),
),
const SizedBox(height: 10),
Text(
"Toplam: ${cardAssignments.length} Kişi",
style: const TextStyle(color: Colors.grey),
),
],
),
),
),

// SAĞ PANEL (KART IZGARASI)
Expanded(
flex: 8,
child: Container(
padding: const EdgeInsets.all(20),
color: const Color(0xFF263238), // Masa Örtüsü Rengi
child: cardAssignments.isEmpty
? const Center(
child: Text(
"Liste Boş",
style: TextStyle(color: Colors.white, fontSize: 20),
),
)
: LayoutBuilder(
builder: (context, constraints) {
// Dinamik Sütun Hesabı
int crossAxisCount = (constraints.maxWidth / 180)
.floor();
if (crossAxisCount < 2) crossAxisCount = 2;

return GridView.builder(
itemCount: cardAssignments.length,
gridDelegate:
SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: crossAxisCount,
childAspectRatio: 0.75, // Kart Oranı
crossAxisSpacing: 15,
mainAxisSpacing: 15,
),
itemBuilder: (context, index) {
return FlipCardWidget(
name: cardAssignments[index],
isRevealed: cardRevealedState[index],
onTap: () => _onCardTapped(index),
index: index,
);
},
);
},
),
),
),
],
),
);
}
}

// --- 3D KART ANİMASYONU ---
class FlipCardWidget extends StatefulWidget {
final String name;
final bool isRevealed;
final VoidCallback onTap;
final int index;

const FlipCardWidget({
super.key,
required this.name,
required this.isRevealed,
required this.onTap,
required this.index,
});

@override
State<FlipCardWidget> createState() => _FlipCardWidgetState();
}

class _FlipCardWidgetState extends State<FlipCardWidget>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _animation;

@override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 600),
);
_animation = Tween<double>(begin: 0, end: 1).animate(
CurvedAnimation(parent: _controller, curve: Curves.easeInOutBack),
);
}

@override
void didUpdateWidget(covariant FlipCardWidget oldWidget) {
super.didUpdateWidget(oldWidget);
// Kart durumu değiştiyse animasyonu tetikle
if (widget.isRevealed && !oldWidget.isRevealed) {
_controller.forward();
} else if (!widget.isRevealed && oldWidget.isRevealed) {
_controller.reverse(); // Kartlar karıştırılınca geri kapat
}
}

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

@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: widget.onTap,
child: AnimatedBuilder(
animation: _animation,
builder: (context, child) {
// Dönüş Açısı
final angle = _animation.value * pi;

// Kartın arkası mı önü mü görünüyor?
final isBackVisible = angle >= pi / 2;

final transform = Matrix4.identity()
..setEntry(3, 2, 0.001) // 3D Derinlik
..rotateY(angle); // Döndürme

return Transform(
transform: transform,
alignment: Alignment.center,
child: isBackVisible
// --- KARTIN ÖN YÜZÜ (İSİM) ---
? Transform(
alignment: Alignment.center,
transform: Matrix4.identity()..rotateY(pi), // Yazıyı düzelt
child: Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(15),
boxShadow: [
BoxShadow(
color: Colors.black26,
blurRadius: 8,
offset: Offset(2, 4),
),
],
border: Border.all(color: Colors.teal, width: 4),
),
alignment: Alignment.center,
child: Text(
widget.name,
textAlign: TextAlign.center,
style: const TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
color: Colors.teal,
),
),
),
)
// --- KARTIN ARKA YÜZÜ (KAPALI) ---
: Container(
decoration: BoxDecoration(
gradient: const LinearGradient(
colors: [Color(0xFF00695C), Color(0xFF4DB6AC)],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(15),
boxShadow: [
BoxShadow(
color: Colors.black45,
blurRadius: 5,
offset: Offset(2, 4),
),
],
),
alignment: Alignment.center,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(
Icons.help_outline,
size: 50,
color: Colors.white24,
),
const SizedBox(height: 5),
Text(
"${widget.index + 1}",
style: const TextStyle(
color: Colors.white54,
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
],
),
),
);
},
),
);
}
}

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

Bu Tasarımın Özellikleri:

  1. Gerçekçi 3D Dönüş: Kartlar "takla atarak" (Y ekseninde dönerek) açılır. Bu efekti Transform matrisleri ile sağladık.

  2. Otomatik Boyutlandırma: Sınıfta 5 kişi de olsa, 40 kişi de olsa ızgara (GridView) kart boyutlarını ekrana en iyi sığacak şekilde otomatik ayarlar.

  3. Karıştır & Sıfırla: Sol paneldeki büyük "Kartları Karıştır" butonuna bastığınızda tüm kartlar kapanır, isimler yer değiştirir ve yeniden dağıtılır.

  4. İnteraktiflik: Öğretmen veya öğrenci tahtaya gelip istediği numaralı karta dokunabilir.

  5. Görsel İpuçları: Kartların üzerinde numaralar yazar (1, 2, 3...), böylece öğretmen "3 numaralı kartı seçtim" diyebilir.

    Projeyi temizlemek.

    Terminali proje klasöründe açın ve sırasıyla şu komutları uygulayın:

    1. Eski Derleme Dosyalarını Silin

    Bu komut, hataya sebep olan CMakeCache.txt dosyasını ve tüm build klasörünü siler.

    Bash
    flutter clean
    

    2. Kütüphaneleri Tekrar Çağırın

    Temizlikten sonra paketleri tekrar getirelim.

    Bash:
    flutter pub get
    

    3. Tekrar Çalıştırın

    Artık CMake, yeni klasör yolunu (karta_dokun_ogrenci_sec) tanıyarak dosyaları sıfırdan oluşturacaktır.

    Bash:
    flutter run -d linux
    

    Bu işlemden sonra uygulamanız sorunsuz bir şekilde "Kartları Çevir" modunda açılacaktır.

Paketleme ve Dağıtma:

Pardus ETAP (ve diğer Debian tabanlı sistemler) için en profesyonel dağıtım yöntemi .deb paketidir. Bu sayede uygulamanız sisteme düzgünce kurulur, "Başlat" menüsüne ikon olarak gelir ve tüm kullanıcılar tarafından erişilebilir olur.

Flutter projelerini .deb haline getirmenin en kolay yolu, topluluk tarafından geliştirilen flutter_to_debian paketini kullanmaktır.

Adım 1: Paketleme Aracını Projeye Ekleyin

Terminali proje klasöründe açın ve şu komutu girin (Bu araç sadece geliştirme aşamasında lazım olduğu için dev olarak ekliyoruz):

Bash
flutter pub add -d flutter_to_debian

Adım 2: Ayarları Yapılandırın (pubspec.yaml)

pubspec.yaml dosyanızı açın. Dosyanın en altına (veya dev_dependencies hizasına) aşağıdaki yapılandırma ayarlarını ekleyin.

Dikkat: exec kısmına, uygulamanızın derlendikten sonra oluşan dosya adını (genelde proje adı) yazmalısınız.

YAML:
# pubspec.yaml dosyasının en altı
flutter_to_debian:
main_config:
app_name: "Ogrenci Secici" # Uygulamanın görünen adı
application_id: "com.okul.ogrencisecici" # Benzersiz kimlik
bundle_name: "ogrenci-secici" # Oluşacak .deb dosyasının adı
version: "1.0.0" # Sürüm
maintainer: "Adiniz Soyadiniz <email@adresiniz.com>" # Paket sorumlusu
icon: "assets/icon/logo.png" # Logonuzun yolu (Varsa)
# Uygulamanın nereye kurulacağı ve nasıl çalışacağı
structure_config:
exec: "karta_dokun_ogrenci_sec" # DİKKAT: Proje oluştururken verdiğiniz küçük harfli ad
desktop_file_name: "ogrenci-secici" # Masaüstü kısayol adı (.desktop)
# Pardus'un bu uygulamayı çalıştırması için gereken sistem paketleri
debian_dependency:
- libgstreamer1.0-0
- gstreamer1.0-plugins-base
- gstreamer1.0-plugins-good
- libgtk-3-0

Adım 3: Release Sürümü Derleyin

Önce uygulamanın son halini Linux için derleyin:

Bash
flutter build linux --release

Adım 4: .deb Paketini Oluşturun

Şimdi sihirli komutu çalıştırın. Bu komut, derlenmiş dosyaları alır ve kurulabilir bir .deb dosyası üretir:

Bash:
dart run flutter_to_debian

İşlem bittiğinde terminalde "Debian package created..." mesajını göreceksiniz. Oluşturulan .deb dosyasını şu yolda bulabilirsiniz: 📂 [Proje Klasörü]/debian/dist/ogrenci-secici_1.0.0_amd64.deb


Adım 5: Dağıtım ve Kurulum (Tüm Tahtalar İçin)

Artık elinizde tek bir kurulum dosyası var. Bunu tahtalara kurmanın birkaç yolu vardır:

Yöntem A: USB Bellek ile Manuel Kurulum

  1. .deb dosyasını USB belleğe atın.

  2. Tahtaya takın ve dosyayı masaüstüne kopyalayın.

  3. Dosyaya çift tıklayın. Pardus Paket Kurucu açılacaktır. "Paketi Kur" diyerek şifreyi girin.

  4. Alternatif (Terminalden):

    Bash:
    sudo dpkg -i ogrenci-secici_1.0.0_amd64.deb
    

    (Eğer bağımlılık hatası verirse peşinden sudo apt -f install komutunu çalıştırın.)

Yöntem B: LiderAhenk ile Merkezi Dağıtım (Önerilen)

Eğer okulunuzda LiderAhenk sunucusu kuruluysa ve tahtalar buna bağlıysa:

  1. LiderAhenk yönetim paneline girin.

  2. "Paket Yönetimi" veya "Dosya Dağıtımı" modülünü kullanın.

  3. Oluşturduğunuz .deb dosyasını sunucuya yükleyin.

  4. Hedef tahtaları seçip uzaktan kurulum emri gönderin. Bu sayede sınıfları gezmenize gerek kalmaz.

Kurulum Sonrası Ne Olur?

Uygulama artık sistemin bir parçasıdır. Öğretmenler:

  1. Başlat Menüsü > Diğer (veya Eğitim) kategorisinde uygulamanın adını ve logosunu görecekler.

  2. İstedikleri zaman tıklayıp çalıştırabilirler.

  3. Otomatik Excel yükleme özelliği için liste.xlsx dosyasını /usr/bin/ veya /opt/ altına atmaları gerekebilir ancak en kolayı; öğretmenlere "Masaüstüne bir liste.xlsx koyarsanız uygulama oradan da okur" şeklinde küçük bir uyarı yapılabilir.

Masaüstünde yoksa , uygulama yanındaki Excel dosyasını bulamayabilir. En garantisi: Öğretmenlere "Uygulama içinden Excel Yükle butonunu kullanın" demektir.

Yorumlar

Bu blogdaki popüler yayınlar

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

Uygulama: Pardus Logosunu Göster

Pardus ETAP 23 İçin Flutter ile Sanal Laboratuvar