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

Eğitim teknolojilerinin merkezinde yer alan Pardus ETAP (Etkileşimli Tahta Projesi), sınıflarda verimliliği artırmak için güçlü araçlar sunar. Bu yazıda, öğretmenlerin ders içi etkileşimi artırmak, sözlüye kalkacak öğrenciyi adil bir şekilde seçmek veya sunum sırasını belirlemek için kullanabileceği "Dijital Öğrenci Seçici" uygulamasını Flutter ile nasıl geliştireceğinizi adım adım anlatacağız.

Bu uygulama şunları yapabilecek:

  • Excel'den Liste Yükleme: e-Okul listelerini saniyeler içinde içeri aktarma.

  • Görsel Seçim: Heyecan verici bir çark arayüzü ile seçim yapma.

  • Ses Efektleri: Dönme ve alkış sesleri ile sınıf atmosferini canlandırma.

  • Sonuç Ekranı: Seçilen öğrenciyi büyük bir pencerede ilan etme.

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:
flutter config --enable-linux-desktop

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 flutter_fortune_wheel rxdart file_picker excel audioplayers window_manager
  • flutter_fortune_wheel: Görsel seçim çarkı için.

  • 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 (The Master Code)

Uygulamanın tüm mantığını tek bir dosyada toplayacağız. lib/main.dart dosyasını açın, içeriğini tamamen silin ve aşağıdaki kodu yapıştırın.

Dart:
import 'dart:async';
import 'dart:io'; // Dosya işlemleri için şart
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:flutter_fortune_wheel/flutter_fortune_wheel.dart';
import 'package:rxdart/rxdart.dart';
import 'package:file_picker/file_picker.dart';
import 'package:excel/excel.dart';
import 'package:audioplayers/audioplayers.dart';
import 'package:window_manager/window_manager.dart';

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

WindowOptions windowOptions = const WindowOptions(
size: Size(1280, 720),
center: true,
backgroundColor: Colors.transparent,
skipTaskbar: false,
titleBarStyle: TitleBarStyle.hidden,
fullScreen: true, // Kiosk modu
);

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: 'Öğrenci Seçici',
debugShowCheckedModeBanner: false,
theme: ThemeData(
primarySwatch: Colors.indigo,
scaffoldBackgroundColor: const Color(0xFFECEFF1),
fontFamily: 'Sans',
useMaterial3: false,
),
home: const HomePage(),
);
}
}

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

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

class _HomePageState extends State<HomePage> {
// Varsayılan liste (Dosya bulunamazsa bu görünür)
List<String> students = ['Öğrenci 1', 'Öğrenci 2'];

final selectedIndex = BehaviorSubject<int>.seeded(0);
final TextEditingController _textController = TextEditingController();
final AudioPlayer _spinPlayer = AudioPlayer();
final AudioPlayer _applausePlayer = AudioPlayer();

@override
void initState() {
super.initState();
// Uygulama açılır açılmaz otomatik dosyayı kontrol et
WidgetsBinding.instance.addPostFrameCallback((_) {
_loadStartupExcel();
});
}

@override
void dispose() {
selectedIndex.close();
_textController.dispose();
_spinPlayer.dispose();
_applausePlayer.dispose();
super.dispose();
}

// --- YENİ: OTOMATİK EXCEL YÜKLEME ---
Future<void> _loadStartupExcel() async {
try {
// 1. Uygulamanın çalıştığı klasörü bul
// Linux'ta derlenmiş dosyanın (executable) olduğu klasörü verir.
String exePath = File(Platform.resolvedExecutable).parent.path;
String filePath = "$exePath/liste.xlsx";
File autoFile = File(filePath);

// 2. Dosya var mı kontrol et
if (await autoFile.exists()) {
var bytes = await autoFile.readAsBytes();
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
}

// 3. Liste doluysa güncelle
if (newItems.isNotEmpty) {
setState(() {
students = newItems;
selectedIndex.add(0);
});

if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
"Otomatik yüklendi: liste.xlsx (${newItems.length} kişi)",
),
backgroundColor: Colors.green[800],
duration: const Duration(seconds: 3),
),
);
}
}
} else {
// Dosya yoksa kullanıcıyı rahatsız etme, varsayılan listeyle devam et.
print("Otomatik dosya bulunamadı: $filePath");
}
} catch (e) {
print("Otomatik yükleme hatası: $e");
}
}

// --- Çark İşlemleri ---
void _startSelection() {
if (students.length < 2) return;
setState(() {
int randomIndex = Fortune.randomInt(0, students.length);
selectedIndex.add(randomIndex);
});
_playSpinSound();
}

Future<void> _playSpinSound() async {
await _spinPlayer.stop();
await _spinPlayer.play(AssetSource('sounds/cevirme.mp3'));
}

Future<void> _playApplauseSound() async {
await _spinPlayer.stop();
await _applausePlayer.stop();
await _applausePlayer.play(AssetSource('sounds/alkis.mp3'));
}

void _onSelectionEnd() {
_playApplauseSound();
final winnerName = students[selectedIndex.value];

showDialog(
context: context,
barrierDismissible: false,
builder: (context) {
return AlertDialog(
backgroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(
Icons.emoji_events,
size: 80,
color: Colors.orangeAccent,
),
const SizedBox(height: 20),
const Text(
"SEÇİLEN ÖĞRENCİ",
style: TextStyle(
fontSize: 16,
color: Colors.grey,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 10),
Text(
winnerName,
textAlign: TextAlign.center,
style: const TextStyle(
fontSize: 36,
fontWeight: FontWeight.bold,
color: Colors.indigo,
),
),
const SizedBox(height: 30),
ElevatedButton(
onPressed: () {
Navigator.of(context).pop();
_applausePlayer.stop();
},
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(
horizontal: 40,
vertical: 15,
),
backgroundColor: Colors.green,
),
child: const Text(
"TAMAM",
style: TextStyle(fontSize: 18, color: Colors.white),
),
),
],
),
);
},
);
}

// --- Manuel Excel Yükleme (Buton İçin) ---
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!);
var bytes = file.readAsBytesSync();
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;
}

if (newItems.isNotEmpty) {
setState(() {
students = newItems;
selectedIndex.add(0);
});
_showSnack(
"${newItems.length} öğrenci başarıyla yüklendi.",
isError: false,
);
}
}
} catch (e) {
_showSnack("Hata: $e", isError: true);
}
}

void _addStudent() {
if (_textController.text.trim().isNotEmpty) {
setState(() {
students.add(_textController.text.trim());
_textController.clear();
});
}
}

void _removeStudent(int index) {
if (students.length > index) {
setState(() {
students.removeAt(index);
selectedIndex.add(0);
});
}
}

void _showSnack(String msg, {bool isError = false}) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(msg),
backgroundColor: isError ? Colors.red : Colors.green[700],
duration: const Duration(seconds: 2),
),
);
}

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Pardus ETAP - Sınıf Çarkı'),
centerTitle: true,
backgroundColor: const Color(0xFF283593),
elevation: 0,
leading: IconButton(
icon: const Icon(Icons.power_settings_new, color: Colors.redAccent),
tooltip: "Uygulamadan Çık",
onPressed: () async {
await windowManager.close();
},
),
actions: [
IconButton(
icon: const Icon(Icons.delete_forever),
tooltip: "Listeyi Temizle",
onPressed: () {
setState(() {
students = ['Öğrenci A', 'Öğrenci B'];
selectedIndex.add(0);
});
},
),
],
),
body: Row(
children: [
// Sol Panel
Expanded(
flex: 3,
child: Container(
color: Colors.white,
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
ElevatedButton.icon(
onPressed: _importExcel,
icon: const Icon(Icons.file_upload, color: Colors.white),
label: const Text(
"Farklı Excel Seç",
style: TextStyle(color: Colors.white),
),
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF43A047),
minimumSize: const Size(double.infinity, 50),
),
),
const SizedBox(height: 15),
Row(
children: [
Expanded(
child: TextField(
controller: _textController,
decoration: const InputDecoration(
labelText: 'Öğrenci Adı',
border: OutlineInputBorder(),
isDense: true,
),
onSubmitted: (_) => _addStudent(),
),
),
const SizedBox(width: 8),
Ink(
decoration: const ShapeDecoration(
color: Colors.indigo,
shape: CircleBorder(),
),
child: IconButton(
icon: const Icon(Icons.add, color: Colors.white),
onPressed: _addStudent,
),
),
],
),
const Divider(height: 25),
Expanded(
child: students.isEmpty
? const Center(child: Text("Liste Boş"))
: ListView.separated(
itemCount: students.length,
separatorBuilder: (ctx, i) =>
const Divider(height: 1),
itemBuilder: (context, index) {
return ListTile(
dense: true,
leading: CircleAvatar(
backgroundColor: Colors.indigo[100],
radius: 14,
child: Text(
"${index + 1}",
style: const TextStyle(fontSize: 12),
),
),
title: Text(students[index]),
trailing: IconButton(
icon: const Icon(
Icons.close,
color: Colors.redAccent,
size: 20,
),
onPressed: () => _removeStudent(index),
),
);
},
),
),
const SizedBox(height: 5),
Text(
"Toplam Mevcut: ${students.length}",
style: const TextStyle(
fontWeight: FontWeight.bold,
color: Colors.grey,
),
),
],
),
),
),
// Sağ Panel (Çark)
Expanded(
flex: 5,
child: Container(
padding: const EdgeInsets.all(20),
decoration: const BoxDecoration(
gradient: LinearGradient(
colors: [Color(0xFFECEFF1), Color(0xFFCFD8DC)],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Expanded(
child: students.length < 2
? Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: const [
Icon(
Icons.warning_amber_rounded,
size: 50,
color: Colors.orange,
),
SizedBox(height: 10),
Text(
"En az 2 öğrenci gerekli",
style: TextStyle(fontSize: 18),
),
],
),
)
: Padding(
padding: const EdgeInsets.all(20.0),
child: FortuneWheel(
selected: selectedIndex.stream,
animateFirst: false,
items: [
for (int i = 0; i < students.length; i++)
FortuneItem(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
students[i],
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: Colors.black,
),
textAlign: TextAlign.center,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
style: FortuneItemStyle(
color:
Colors.primaries[i %
Colors.primaries.length],
borderColor: Colors.white,
borderWidth: 2,
),
),
],
onFling: _startSelection,
onAnimationEnd: _onSelectionEnd,
physics: CircularPanPhysics(
duration: const Duration(seconds: 4),
curve: Curves.decelerate,
),
),
),
),
const SizedBox(height: 20),
if (students.length >= 2)
ElevatedButton(
onPressed: _startSelection,
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(
horizontal: 60,
vertical: 22,
),
backgroundColor: const Color(0xFF283593),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(50),
),
elevation: 5,
),
child: const Text(
"KURA ÇEK",
style: TextStyle(
fontSize: 22,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
),
],
),
),
),
],
),
);
}
}

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

Bölüm 4: Test ve Dağıtım

1. Uygulamayı Test Etme

Pardus terminalinden uygulamayı başlatın:

Bash:
flutter run -d linux

Adım 1: Uygulamayı "Release" Modunda Derleyin

Geliştirme (Debug) modu yavaştır ve hataları gösterir. Tahtalar için optimize edilmiş, hızlı çalışan "Release" sürümünü oluşturmalısınız.

Terminali proje klasöründe açın ve şu komutu verin:

Bash
flutter build linux --release

Adım 2: Dosyaları Hazırlayın

Derleme işlemi bittikten sonra uygulamanız şu yolda hazır olacaktır: [Proje Klasörü]/build/linux/x64/release/bundle/

Bu bundle klasörü, uygulamanızın çalışması için gereken her şeyi içerir. Şimdi bunu bir USB belleğe atmak için hazırlayalım:

  1. Masaüstünde "OgrenciSecici" adında yeni bir klasör oluşturun.

  2. bundle klasörünün içindeki tüm dosyaları (data klasörü, lib klasörü, ogrenci_secici dosyası vb.) kopyalayıp masaüstündeki bu yeni klasörün içine yapıştırın.

  3. Önemli: Hazırladığınız liste.xlsx dosyasını da bu klasörün içine atın (Otomatik yükleme özelliği için).

Klasör içeriği şöyle görünmeli:

  • data/ (Klasör)

  • lib/ (Klasör)

  • ogrenci_secici (Çalıştırılabilir dosya)

  • liste.xlsx (Öğrenci listeniz)

Adım 3: Akıllı Tahtaya Taşıma ve Hazırlık

Hazırladığınız "OgrenciSecici" klasörünü USB belleğe atın ve akıllı tahtaya götürün.

  1. Klasörü tahtanın Masaüstüne veya Belgelerim klasörüne kopyalayın.

  2. Gerekli Kütüphanelerin Kurulması (Sadece 1 Kere): Uygulamanız ses çaldığı için (alkış/çark sesi), tahtada GStreamer kütüphanesinin yüklü olması gerekir. Pardus ETAP'ta bu genellikle yüklüdür ama garantiye almak için tahtada uçbirimi (terminali) açıp şu komutu çalıştırın:

    Bash
    sudo apt update
    sudo apt install libgstreamer1.0-0 gstreamer1.0-plugins-base gstreamer1.0-plugins-good gstreamer1.0-pulseaudio
    

    (Bu işlem internet bağlantısı gerektirir.)

Adım 4: Masaüstü Kısayolu Oluşturma (İsteğe Bağlı ama Önerilir)

Öğretmenlerin klasörün içine girip dosyayı araması yerine masaüstünde şık bir ikonla tıklamalarını sağlamak için bir başlatıcı oluşturabilirsiniz.

  1. Masaüstünde boş bir yere sağ tıklayın -> "Başlatıcı Oluştur" deyin.

  2. Tür: Uygulama

  3. Ad: Öğrenci Seçici

  4. Komut: "Gözat" butonuna basın ve masaüstüne kopyaladığınız klasörün içindeki ogrenci_secici dosyasını seçin.

  5. Simge: İsterseniz internetten .png formatında bir ikon indirip onu seçebilirsiniz.

  6. Tamam'a basın.

Adım 5: Çalıştırma İzni (Eğer Açılmazsa)

Eğer çift tıkladığınızda açılmıyorsa, dosyanın çalışma izni kaybolmuş olabilir.

  1. Kopyaladığınız klasörün içine girin.

  2. ogrenci_secici dosyasına sağ tıklayın -> Özellikler.

  3. Erişim Hakları (İzinler) sekmesine gelin.

  4. "Dosyayı bir program gibi çalıştırmaya izin ver" kutucuğunu işaretleyin.


Sonuç: Artık Pardus ETAP 23 üzerinde çalışan; Excel listelerini okuyabilen, sesli ve görsel olarak zenginleştirilmiş profesyonel bir sınıf yönetim aracınız var. Güle güle kullanın!

Uygulamanızın logosunu değiştirmek, onu daha profesyonel göstermenin en önemli adımlarından biridir. Flutter'da bunu yapmanın en kolay ve standart yolu, flutter_launcher_icons adlı paketi kullanmaktır.

Bu paket, tek bir yüksek çözünürlüklü resimden; Android, iOS, Web, Windows, macOS ve Linux (Pardus ETAP) için gerekli tüm boyutlardaki ikonları otomatik olarak oluşturur.

İşte adım adım logonuzu değiştirme rehberi:

1. Hazırlık: Logo Görselini Hazırlayın

  • Format: .png formatında olmalıdır.

  • Boyut: Yüksek çözünürlüklü ve kare olmalıdır (Önerilen: 1024x1024 piksel). Arka planı şeffaf olabilir.

  • Konum: Projenizin ana dizininde (lib klasörünün yanında) aşağıdaki gibi bir klasör yapısı oluşturun ve logonuzu içine atın.

    • assets/

      • icon/

        • logo.png (Dosya adınız bu olsun)

2. Paketi Projeye Ekleyin

Bu paket sadece geliştirme aşamasında lazım olduğu için "dev dependency" olarak ekleyeceğiz. Terminali proje klasöründe açın ve şu komutu girin:

Bash:
flutter pub add -d flutter_launcher_icons

3. Yapılandırmayı Ayarlayın (pubspec.yaml)

pubspec.yaml dosyanızı açın. Dosyanın en altına inin ve aşağıdaki kod bloğunu yapıştırın.

⚠️ Dikkat: pubspec.yaml dosyasında boşluklar (girintiler) çok önemlidir. Aşağıdaki girintileri aynen koruyun.

YAML:
# ... Dosyanın üst kısımları ...

# EN ALTA BU KISMI EKLEYİN:
flutter_icons:
  android: true        # Android için de üretsin (zararı yok)
  ios: true            # iOS için de üretsin (zararı yok)
  image_path: "assets/icon/logo.png" # Hazırladığınız görselin yolu
  
  # Linux (Pardus ETAP) için özel ayarlar
  linux:
    generate: true
    icon_name: "ogrenci_secici" # Uygulamanızın çalıştırılabilir dosya adı ile aynı olmalı

(Not: Eğer pubspec.yaml dosyanızda dev_dependencies: altında flutter_lints: gibi başka şeyler varsa, flutter_icons: bloğunu en dış seviyeye, yani dev_dependencies: ile aynı hizaya eklemelisiniz, onun altına değil.)

4. İkonları Oluşturun

Ayarları kaydettikten sonra terminale şu sihirli komutu girin. Bu komut, belirttiğiniz logo.png dosyasını alacak ve Linux dahil tüm platformlar için gerekli yerlere yerleştirecektir:

Bash:
dart run flutter_launcher_icons

(Eski Flutter sürümlerinde: flutter pub run flutter_launcher_icons:main)

Terminalde yeşil renkli başarı mesajları (Successfully generated icons...) görmelisiniz.

5. Sonuçları Görmek İçin Derleyin

Linux masaüstü ortamında ikon değişikliğinin tam olarak uygulanması için uygulamayı "Release" modunda tekrar derlemeniz gerekir:

Bash
flutter clean
flutter build linux --release

Önemli Notlar:

  • Pencere Kenarı İkonu: Uygulama çalıştığında sol üst köşede (pencere başlığında) yeni logonuzu göreceksiniz.

  • Masaüstü Kısayolu: Bir önceki adımda anlattığım "Başlatıcı Oluştur" işlemi sırasında, "Simge" kısmına tıkladığınızda artık projenizin içindeki linux/packaging/media/ klasöründe oluşan yeni ikonları seçebilirsiniz. Veya derleme sonrası oluşan klasörün içinde ikon dosyasını aratıp onu kullanabilirsiniz.

  • Kiosk Modu: Şu anki kodumuz Kiosk modunda (tam ekran ve başlık çubuğu gizli) çalıştığı için uygulama çalışırken pencere ikonunu göremeyebilirsiniz, ancak masaüstü kısayolunda ve görev çubuğunda yeni logonuz görünecektir.

Yorumlar

Bu blogdaki popüler yayınlar

Uygulama: Pardus Logosunu Göster

Pardus ETAP 23 İçin Flutter ile Sanal Laboratuvar