libretranslatr

This commit is contained in:
y1lm0z
2026-07-14 17:37:17 +03:00
parent 1b8149a4cf
commit 8a562e5bf6
46 changed files with 2040 additions and 106 deletions
+117
View File
@@ -0,0 +1,117 @@
import 'package:flutter/material.dart';
import '../models/history_item.dart';
import '../services/history_service.dart';
class HistoryScreen extends StatefulWidget {
const HistoryScreen({super.key});
@override
State<HistoryScreen> createState() => _HistoryScreenState();
}
class _HistoryScreenState extends State<HistoryScreen> {
final _historyService = HistoryService();
List<HistoryItem> _items = [];
bool _loading = true;
@override
void initState() {
super.initState();
_load();
}
Future<void> _load() async {
final items = await _historyService.getAll();
if (!mounted) return;
setState(() {
_items = items;
_loading = false;
});
}
Future<void> _clear() async {
final confirmed = await showDialog<bool>(
context: context,
builder: (ctx) => AlertDialog(
title: const Text('Geçmişi Sil'),
content: const Text('Tüm çeviri geçmişi silinsin mi? Bu işlem geri alınamaz.'),
actions: [
TextButton(onPressed: () => Navigator.pop(ctx, false), child: const Text('Vazgeç')),
FilledButton(onPressed: () => Navigator.pop(ctx, true), child: const Text('Sil')),
],
),
);
if (confirmed == true) {
await _historyService.clear();
_load();
}
}
String _formatTime(DateTime dt) {
final now = DateTime.now();
final sameDay = dt.year == now.year && dt.month == now.month && dt.day == now.day;
final time = '${dt.hour.toString().padLeft(2, '0')}:${dt.minute.toString().padLeft(2, '0')}';
if (sameDay) return time;
return '${dt.day}.${dt.month}.${dt.year} $time';
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Geçmiş'),
actions: [
if (_items.isNotEmpty)
IconButton(
icon: const Icon(Icons.delete_outline),
tooltip: 'Geçmişi temizle',
onPressed: _clear,
),
],
),
body: _loading
? const Center(child: CircularProgressIndicator())
: _items.isEmpty
? const Center(child: Text('Henüz çeviri geçmişi yok'))
: ListView.separated(
padding: const EdgeInsets.all(12),
itemCount: _items.length,
separatorBuilder: (_, __) => const SizedBox(height: 8),
itemBuilder: (ctx, i) {
final item = _items[i];
return Card(
margin: EdgeInsets.zero,
child: Padding(
padding: const EdgeInsets.all(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Text(
'${item.sourceLang.toUpperCase()}${item.targetLang.toUpperCase()}',
style: Theme.of(context).textTheme.labelMedium,
),
const Spacer(),
Text(
_formatTime(item.timestamp),
style: Theme.of(context).textTheme.labelSmall,
),
],
),
const SizedBox(height: 6),
Text(item.sourceText),
const Divider(height: 16),
Text(
item.translatedText,
style: TextStyle(color: Theme.of(context).colorScheme.primary),
),
],
),
),
);
},
),
);
}
}
+368
View File
@@ -0,0 +1,368 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import '../models/language.dart';
import '../models/history_item.dart';
import '../services/settings_service.dart';
import '../services/translate_service.dart';
import '../services/history_service.dart';
import 'settings_screen.dart';
import 'history_screen.dart';
class HomeScreen extends StatefulWidget {
const HomeScreen({super.key});
@override
State<HomeScreen> createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
final _settingsService = SettingsService();
late final TranslateService _translateService = TranslateService(_settingsService);
final _historyService = HistoryService();
final _sourceController = TextEditingController();
Timer? _debounce;
List<Language> _languages = [];
Language _sourceLang = Language.auto;
Language _targetLang = const Language(code: 'en', name: 'İngilizce');
String _resultText = '';
String? _detectedLangLabel;
bool _loadingLanguages = true;
bool _translating = false;
String? _connectionError;
@override
void initState() {
super.initState();
_bootstrap();
_sourceController.addListener(_onTextChanged);
}
@override
void dispose() {
_debounce?.cancel();
_sourceController.dispose();
super.dispose();
}
/// Uygulama açılışında sunucuya otomatik bağlanmayı dener.
Future<void> _bootstrap() async {
final hasSettings = await _settingsService.hasSettings();
if (!hasSettings) {
final saved = await Navigator.of(context).push<bool>(
MaterialPageRoute(builder: (_) => const SettingsScreen(isFirstRun: true)),
);
if (saved != true) {
// Kullanıcı ayarları kaydetmeden geri döndüyse yine de tekrar dene.
}
}
await _loadLanguages();
}
Future<void> _loadLanguages() async {
setState(() {
_loadingLanguages = true;
_connectionError = null;
});
try {
final langs = await _translateService.fetchLanguages();
setState(() {
_languages = langs;
// Hedef dil listede yoksa ilk uygun dile düş.
if (!_languages.any((l) => l.code == _targetLang.code) && _languages.isNotEmpty) {
_targetLang = _languages.firstWhere((l) => l.code != 'auto', orElse: () => _languages.first);
}
});
} catch (e) {
setState(() => _connectionError = e.toString());
} finally {
setState(() => _loadingLanguages = false);
}
}
void _onTextChanged() {
_debounce?.cancel();
_debounce = Timer(const Duration(milliseconds: 600), _runTranslate);
}
Future<void> _runTranslate() async {
final text = _sourceController.text;
if (text.trim().isEmpty) {
setState(() {
_resultText = '';
_detectedLangLabel = null;
});
return;
}
setState(() => _translating = true);
try {
String effectiveSource = _sourceLang.code;
if (_sourceLang.code == 'auto') {
final detected = await _translateService.detect(text);
if (detected != null) {
effectiveSource = detected.language;
final match = _languages.firstWhere(
(l) => l.code == detected.language,
orElse: () => Language(code: detected.language, name: detected.language),
);
setState(() => _detectedLangLabel = 'Algılanan dil: ${match.name}');
}
} else {
_detectedLangLabel = null;
}
final translated = await _translateService.translate(
text: text,
sourceLang: effectiveSource,
targetLang: _targetLang.code,
);
if (!mounted) return;
setState(() => _resultText = translated);
await _historyService.add(HistoryItem(
sourceLang: effectiveSource,
targetLang: _targetLang.code,
sourceText: text,
translatedText: translated,
timestamp: DateTime.now(),
));
} catch (e) {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Çeviri hatası: $e')),
);
} finally {
if (mounted) setState(() => _translating = false);
}
}
void _swapLanguages() {
if (_sourceLang.code == 'auto') return; // otomatik iken takas anlamsız
setState(() {
final tmp = _sourceLang;
_sourceLang = _targetLang;
_targetLang = tmp;
final tmpText = _sourceController.text;
_sourceController.text = _resultText;
_resultText = tmpText;
});
}
List<Language> get _sourceOptions => [Language.auto, ..._languages];
List<Language> get _targetOptions => _languages;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Çeviri'),
actions: [
IconButton(
tooltip: 'Geçmiş',
icon: const Icon(Icons.history),
onPressed: () => Navigator.of(context).push(
MaterialPageRoute(builder: (_) => const HistoryScreen()),
),
),
IconButton(
tooltip: 'Ayarlar',
icon: const Icon(Icons.settings_outlined),
onPressed: () async {
await Navigator.of(context).push(
MaterialPageRoute(builder: (_) => const SettingsScreen()),
);
_loadLanguages();
},
),
],
),
body: _loadingLanguages
? const Center(child: CircularProgressIndicator())
: _connectionError != null
? _buildConnectionError()
: _buildTranslator(),
);
}
Widget _buildConnectionError() {
return Center(
child: Padding(
padding: const EdgeInsets.all(24),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.cloud_off, size: 48, color: Colors.grey),
const SizedBox(height: 16),
Text('Sunucuya bağlanılamadı', style: Theme.of(context).textTheme.titleMedium),
const SizedBox(height: 8),
Text(_connectionError!, textAlign: TextAlign.center),
const SizedBox(height: 20),
Wrap(
spacing: 12,
children: [
OutlinedButton(onPressed: _loadLanguages, child: const Text('Tekrar Dene')),
FilledButton(
onPressed: () async {
await Navigator.of(context).push(
MaterialPageRoute(builder: (_) => const SettingsScreen()),
);
_loadLanguages();
},
child: const Text('Ayarları'),
),
],
),
],
),
),
);
}
Widget _buildTranslator() {
return Padding(
padding: const EdgeInsets.all(16),
child: Column(
children: [
_buildLanguageBar(),
const SizedBox(height: 12),
Expanded(
child: Column(
children: [
Expanded(child: _buildInputCard()),
const SizedBox(height: 12),
Expanded(child: _buildResultCard()),
],
),
),
],
),
);
}
Widget _buildLanguageBar() {
return Row(
children: [
Expanded(child: _buildLangDropdown(isSource: true)),
IconButton(
icon: const Icon(Icons.swap_horiz),
tooltip: 'Dilleri değiştir',
onPressed: _sourceLang.code == 'auto' ? null : _swapLanguages,
),
Expanded(child: _buildLangDropdown(isSource: false)),
],
);
}
Widget _buildLangDropdown({required bool isSource}) {
final options = isSource ? _sourceOptions : _targetOptions;
final value = isSource ? _sourceLang : _targetLang;
return DropdownButtonFormField<Language>(
isExpanded: true,
value: options.contains(value) ? value : null,
decoration: const InputDecoration(isDense: true),
items: options
.map((l) => DropdownMenuItem(value: l, child: Text(l.name, overflow: TextOverflow.ellipsis)))
.toList(),
onChanged: (l) {
if (l == null) return;
setState(() {
if (isSource) {
_sourceLang = l;
} else {
_targetLang = l;
}
});
_runTranslate();
},
);
}
Widget _buildInputCard() {
return Card(
margin: EdgeInsets.zero,
child: Padding(
padding: const EdgeInsets.all(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Expanded(
child: TextField(
controller: _sourceController,
maxLines: null,
expands: true,
textAlignVertical: TextAlignVertical.top,
decoration: const InputDecoration(
border: InputBorder.none,
hintText: 'Çevrilecek metni yazın...',
),
),
),
if (_detectedLangLabel != null)
Padding(
padding: const EdgeInsets.only(top: 4),
child: Text(_detectedLangLabel!, style: Theme.of(context).textTheme.bodySmall),
),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
IconButton(
icon: const Icon(Icons.clear, size: 20),
tooltip: 'Temizle',
onPressed: () => _sourceController.clear(),
),
],
),
],
),
),
);
}
Widget _buildResultCard() {
return Card(
margin: EdgeInsets.zero,
color: Theme.of(context).colorScheme.surfaceContainerHighest,
child: Padding(
padding: const EdgeInsets.all(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Expanded(
child: _translating
? const Center(child: CircularProgressIndicator())
: SingleChildScrollView(
child: SelectableText(
_resultText,
style: Theme.of(context).textTheme.bodyLarge,
),
),
),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
IconButton(
icon: const Icon(Icons.copy, size: 20),
tooltip: 'Kopyala',
onPressed: _resultText.isEmpty
? null
: () {
Clipboard.setData(ClipboardData(text: _resultText));
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Kopyalandı'), duration: Duration(seconds: 1)),
);
},
),
],
),
],
),
),
);
}
}
+164
View File
@@ -0,0 +1,164 @@
import 'package:flutter/material.dart';
import '../services/settings_service.dart';
import '../services/translate_service.dart';
class SettingsScreen extends StatefulWidget {
final bool isFirstRun;
const SettingsScreen({super.key, this.isFirstRun = false});
@override
State<SettingsScreen> createState() => _SettingsScreenState();
}
class _SettingsScreenState extends State<SettingsScreen> {
final _settings = SettingsService();
final _urlController = TextEditingController();
final _apiKeyController = TextEditingController();
bool _testing = false;
String? _statusMessage;
bool? _statusSuccess;
@override
void initState() {
super.initState();
_load();
_urlController.addListener(() => setState(() {}));
}
Future<void> _load() async {
final url = await _settings.getServerUrl();
final key = await _settings.getApiKey();
if (!mounted) return;
setState(() {
_urlController.text = url ?? '';
_apiKeyController.text = key ?? '';
});
}
Future<void> _testConnection() async {
setState(() {
_testing = true;
_statusMessage = null;
});
// Test için önce girilen değerleri geçici olarak kaydediyoruz.
await _settings.save(
serverUrl: _urlController.text,
apiKey: _apiKeyController.text,
);
try {
final service = TranslateService(_settings);
final langs = await service.fetchLanguages();
setState(() {
_statusSuccess = true;
_statusMessage = 'Bağlantı başarılı${langs.length} dil bulundu.';
});
} catch (e) {
setState(() {
_statusSuccess = false;
_statusMessage = 'Bağlanamadı: $e';
});
} finally {
setState(() => _testing = false);
}
}
Future<void> _saveAndReturn() async {
await _settings.save(
serverUrl: _urlController.text,
apiKey: _apiKeyController.text,
);
if (!mounted) return;
Navigator.of(context).pop(true);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Sunucu Ayarları'),
automaticallyImplyLeading: !widget.isFirstRun,
),
body: SingleChildScrollView(
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
if (widget.isFirstRun) ...[
Text(
'LibreTranslate Sunucunuza Bağlanın',
style: Theme.of(context).textTheme.headlineSmall,
),
const SizedBox(height: 8),
Text(
'Uygulamanın çeviri yapabilmesi için kendi LibreTranslate sunucunuzun adresini girin.',
style: Theme.of(context).textTheme.bodyMedium,
),
const SizedBox(height: 24),
],
TextField(
controller: _urlController,
keyboardType: TextInputType.url,
decoration: const InputDecoration(
labelText: 'Sunucu Adresi',
hintText: 'https://translate.benimsunucum.com',
prefixIcon: Icon(Icons.dns_outlined),
),
),
const SizedBox(height: 14),
TextField(
controller: _apiKeyController,
obscureText: true,
decoration: const InputDecoration(
labelText: 'API Key (opsiyonel)',
hintText: 'Boş bırakılırsa gönderilmez',
prefixIcon: Icon(Icons.key_outlined),
),
),
const SizedBox(height: 20),
OutlinedButton.icon(
onPressed: _testing ? null : _testConnection,
icon: _testing
? const SizedBox(
width: 16,
height: 16,
child: CircularProgressIndicator(strokeWidth: 2),
)
: const Icon(Icons.wifi_tethering),
label: Text(_testing ? 'Test ediliyor...' : 'Bağlantıyı Test Et'),
),
if (_statusMessage != null) ...[
const SizedBox(height: 12),
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: (_statusSuccess == true
? Colors.green
: Colors.red)
.withOpacity(0.1),
borderRadius: BorderRadius.circular(10),
),
child: Text(
_statusMessage!,
style: TextStyle(
color: _statusSuccess == true
? Colors.green.shade700
: Colors.red.shade700,
),
),
),
],
const SizedBox(height: 24),
FilledButton.icon(
onPressed: _urlController.text.trim().isEmpty ? null : _saveAndReturn,
icon: const Icon(Icons.check),
label: const Text('Kaydet'),
),
],
),
),
);
}
}