feat: i18n (en/tr), zoom orijinal cozunurluk + max 20x + gaplessPlayback, app id com.softmediadesign.imajviewer, CI analyze+test
Some checks failed
Build & Release / build-linux (push) Has been cancelled
Build & Release / build-windows (push) Has been cancelled
Build & Release / create-release (push) Has been cancelled

This commit is contained in:
Alhan
2026-08-07 00:16:48 +03:00
parent b9e433d032
commit 504d84f6f6
19 changed files with 408 additions and 48 deletions

View File

@@ -25,6 +25,12 @@ jobs:
- name: Get dependencies
run: flutter pub get
- name: Analyze
run: flutter analyze --no-fatal-infos
- name: Test
run: flutter test
- name: Build Linux release
run: flutter build linux --release

View File

@@ -8,4 +8,4 @@ Terminal=false
Categories=Graphics;Viewer;RasterGraphics;
MimeType=image/png;image/jpeg;image/webp;image/bmp;image/gif;image/jpg;image/vnd.wap.wbmp;
StartupNotify=false
StartupWMClass=Com.example.imajviewer
StartupWMClass=Com.softmediadesign.imajviewer

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 912 KiB

View File

@@ -92,7 +92,7 @@ Terminal=false
Categories=Graphics;Viewer;RasterGraphics;
MimeType=image/png;image/jpeg;image/webp;image/bmp;image/gif;image/jpg;image/vnd.wap.wbmp;
StartupNotify=false
StartupWMClass=Com.example.imajviewer
StartupWMClass=Com.softmediadesign.imajviewer
EOF
ok "${DESKTOP_FILE}"

3
l10n.yaml Normal file
View File

@@ -0,0 +1,3 @@
arb-dir: lib/l10n
template-arb-file: app_en.arb
output-localization-file: app_localizations.dart

View File

@@ -1,5 +1,7 @@
import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'screens/viewer_screen.dart';
import 'l10n/app_localizations.dart';
class ImajViewerApp extends StatelessWidget {
final List<String> initialFiles;
@@ -11,6 +13,8 @@ class ImajViewerApp extends StatelessWidget {
return MaterialApp(
title: 'imajViewer',
debugShowCheckedModeBanner: false,
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
theme: ThemeData(
brightness: Brightness.dark,
scaffoldBackgroundColor: const Color(0xFF1a1a1a),

15
lib/l10n/app_en.arb Normal file
View File

@@ -0,0 +1,15 @@
{
"@@locale": "en",
"settingsTitle": "Settings",
"sortBy": "Sort by",
"sortByTime": "By time",
"sortByName": "By name",
"appDescription": "Ultra-lightweight and fast image viewer. Open images and use arrows or keyboard keys to navigate.",
"shortcuts": "Shortcuts",
"shortcutsList": "• Left/Right arrow — previous/next image\n• Ctrl+Q — close window\n• Ctrl+Shift+Q — close all windows\n• Double click — toggle fill/fit\n• Right-drag — contrast\n• Shift+right-drag — rotate\n• Ctrl+right-drag — saturation\n• Ctrl+Shift+right-drag — brightness",
"print": "Print",
"close": "Close",
"dragDropHint": "Drag images here\nor press Ctrl+O to open",
"dropHere": "Drop",
"imageLoadError": "Failed to load image"
}

View File

@@ -0,0 +1,206 @@
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:intl/intl.dart' as intl;
import 'app_localizations_en.dart';
import 'app_localizations_tr.dart';
// ignore_for_file: type=lint
/// Callers can lookup localized strings with an instance of AppLocalizations
/// returned by `AppLocalizations.of(context)`.
///
/// Applications need to include `AppLocalizations.delegate()` in their app's
/// `localizationDelegates` list, and the locales they support in the app's
/// `supportedLocales` list. For example:
///
/// ```dart
/// import 'l10n/app_localizations.dart';
///
/// return MaterialApp(
/// localizationsDelegates: AppLocalizations.localizationsDelegates,
/// supportedLocales: AppLocalizations.supportedLocales,
/// home: MyApplicationHome(),
/// );
/// ```
///
/// ## Update pubspec.yaml
///
/// Please make sure to update your pubspec.yaml to include the following
/// packages:
///
/// ```yaml
/// dependencies:
/// # Internationalization support.
/// flutter_localizations:
/// sdk: flutter
/// intl: any # Use the pinned version from flutter_localizations
///
/// # Rest of dependencies
/// ```
///
/// ## iOS Applications
///
/// iOS applications define key application metadata, including supported
/// locales, in an Info.plist file that is built into the application bundle.
/// To configure the locales supported by your app, youll need to edit this
/// file.
///
/// First, open your projects ios/Runner.xcworkspace Xcode workspace file.
/// Then, in the Project Navigator, open the Info.plist file under the Runner
/// projects Runner folder.
///
/// Next, select the Information Property List item, select Add Item from the
/// Editor menu, then select Localizations from the pop-up menu.
///
/// Select and expand the newly-created Localizations item then, for each
/// locale your application supports, add a new item and select the locale
/// you wish to add from the pop-up menu in the Value field. This list should
/// be consistent with the languages listed in the AppLocalizations.supportedLocales
/// property.
abstract class AppLocalizations {
AppLocalizations(String locale)
: localeName = intl.Intl.canonicalizedLocale(locale.toString());
final String localeName;
static AppLocalizations? of(BuildContext context) {
return Localizations.of<AppLocalizations>(context, AppLocalizations);
}
static const LocalizationsDelegate<AppLocalizations> delegate =
_AppLocalizationsDelegate();
/// A list of this localizations delegate along with the default localizations
/// delegates.
///
/// Returns a list of localizations delegates containing this delegate along with
/// GlobalMaterialLocalizations.delegate, GlobalCupertinoLocalizations.delegate,
/// and GlobalWidgetsLocalizations.delegate.
///
/// Additional delegates can be added by appending to this list in
/// MaterialApp. This list does not have to be used at all if a custom list
/// of delegates is preferred or required.
static const List<LocalizationsDelegate<dynamic>> localizationsDelegates =
<LocalizationsDelegate<dynamic>>[
delegate,
GlobalMaterialLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
];
/// A list of this localizations delegate's supported locales.
static const List<Locale> supportedLocales = <Locale>[
Locale('en'),
Locale('tr'),
];
/// No description provided for @settingsTitle.
///
/// In en, this message translates to:
/// **'Settings'**
String get settingsTitle;
/// No description provided for @sortBy.
///
/// In en, this message translates to:
/// **'Sort by'**
String get sortBy;
/// No description provided for @sortByTime.
///
/// In en, this message translates to:
/// **'By time'**
String get sortByTime;
/// No description provided for @sortByName.
///
/// In en, this message translates to:
/// **'By name'**
String get sortByName;
/// No description provided for @appDescription.
///
/// In en, this message translates to:
/// **'Ultra-lightweight and fast image viewer. Open images and use arrows or keyboard keys to navigate.'**
String get appDescription;
/// No description provided for @shortcuts.
///
/// In en, this message translates to:
/// **'Shortcuts'**
String get shortcuts;
/// No description provided for @shortcutsList.
///
/// In en, this message translates to:
/// **'• Left/Right arrow — previous/next image\n• Ctrl+Q — close window\n• Ctrl+Shift+Q — close all windows\n• Double click — toggle fill/fit\n• Right-drag — contrast\n• Shift+right-drag — rotate\n• Ctrl+right-drag — saturation\n• Ctrl+Shift+right-drag — brightness'**
String get shortcutsList;
/// No description provided for @print.
///
/// In en, this message translates to:
/// **'Print'**
String get print;
/// No description provided for @close.
///
/// In en, this message translates to:
/// **'Close'**
String get close;
/// No description provided for @dragDropHint.
///
/// In en, this message translates to:
/// **'Drag images here\nor press Ctrl+O to open'**
String get dragDropHint;
/// No description provided for @dropHere.
///
/// In en, this message translates to:
/// **'Drop'**
String get dropHere;
/// No description provided for @imageLoadError.
///
/// In en, this message translates to:
/// **'Failed to load image'**
String get imageLoadError;
}
class _AppLocalizationsDelegate
extends LocalizationsDelegate<AppLocalizations> {
const _AppLocalizationsDelegate();
@override
Future<AppLocalizations> load(Locale locale) {
return SynchronousFuture<AppLocalizations>(lookupAppLocalizations(locale));
}
@override
bool isSupported(Locale locale) =>
<String>['en', 'tr'].contains(locale.languageCode);
@override
bool shouldReload(_AppLocalizationsDelegate old) => false;
}
AppLocalizations lookupAppLocalizations(Locale locale) {
// Lookup logic when only language code is specified.
switch (locale.languageCode) {
case 'en':
return AppLocalizationsEn();
case 'tr':
return AppLocalizationsTr();
}
throw FlutterError(
'AppLocalizations.delegate failed to load unsupported locale "$locale". This is likely '
'an issue with the localizations generation tool. Please file an issue '
'on GitHub with a reproducible sample app and the gen-l10n configuration '
'that was used.',
);
}

View File

@@ -0,0 +1,48 @@
// ignore: unused_import
import 'package:intl/intl.dart' as intl;
import 'app_localizations.dart';
// ignore_for_file: type=lint
/// The translations for English (`en`).
class AppLocalizationsEn extends AppLocalizations {
AppLocalizationsEn([String locale = 'en']) : super(locale);
@override
String get settingsTitle => 'Settings';
@override
String get sortBy => 'Sort by';
@override
String get sortByTime => 'By time';
@override
String get sortByName => 'By name';
@override
String get appDescription =>
'Ultra-lightweight and fast image viewer. Open images and use arrows or keyboard keys to navigate.';
@override
String get shortcuts => 'Shortcuts';
@override
String get shortcutsList =>
'• Left/Right arrow — previous/next image\n• Ctrl+Q — close window\n• Ctrl+Shift+Q — close all windows\n• Double click — toggle fill/fit\n• Right-drag — contrast\n• Shift+right-drag — rotate\n• Ctrl+right-drag — saturation\n• Ctrl+Shift+right-drag — brightness';
@override
String get print => 'Print';
@override
String get close => 'Close';
@override
String get dragDropHint => 'Drag images here\nor press Ctrl+O to open';
@override
String get dropHere => 'Drop';
@override
String get imageLoadError => 'Failed to load image';
}

View File

@@ -0,0 +1,49 @@
// ignore: unused_import
import 'package:intl/intl.dart' as intl;
import 'app_localizations.dart';
// ignore_for_file: type=lint
/// The translations for Turkish (`tr`).
class AppLocalizationsTr extends AppLocalizations {
AppLocalizationsTr([String locale = 'tr']) : super(locale);
@override
String get settingsTitle => 'Ayarlar';
@override
String get sortBy => 'Sıralama';
@override
String get sortByTime => 'Zamana göre';
@override
String get sortByName => 'İsme göre';
@override
String get appDescription =>
'Ultra hafif ve hızlı resim görüntüleyici. Görselleri açın, gezinti için okları veya klavye tuşlarını kullanın.';
@override
String get shortcuts => 'Kısayollar';
@override
String get shortcutsList =>
'• Sol/Sağ ok — önceki/sonraki resim\n• Ctrl+Q — pencereyi kapat\n• Ctrl+Shift+Q — tüm pencereleri kapat\n• Çift tık — doldur/sığdır geçişi\n• Sağ tık sürükle — kontrast\n• Shift+sağ tık — döndür\n• Ctrl+sağ tık — doygunluk\n• Ctrl+Shift+sağ tık — parlaklık';
@override
String get print => 'Yazdır';
@override
String get close => 'Kapat';
@override
String get dragDropHint =>
'Resimleri buraya sürükleyin\nveya Ctrl+O ile açın';
@override
String get dropHere => 'Bırakın';
@override
String get imageLoadError => 'Resim yüklenemedi';
}

15
lib/l10n/app_tr.arb Normal file
View File

@@ -0,0 +1,15 @@
{
"@@locale": "tr",
"settingsTitle": "Ayarlar",
"sortBy": "Sıralama",
"sortByTime": "Zamana göre",
"sortByName": "İsme göre",
"appDescription": "Ultra hafif ve hızlı resim görüntüleyici. Görselleri açın, gezinti için okları veya klavye tuşlarını kullanın.",
"shortcuts": "Kısayollar",
"shortcutsList": "• Sol/Sağ ok — önceki/sonraki resim\n• Ctrl+Q — pencereyi kapat\n• Ctrl+Shift+Q — tüm pencereleri kapat\n• Çift tık — doldur/sığdır geçişi\n• Sağ tık sürükle — kontrast\n• Shift+sağ tık — döndür\n• Ctrl+sağ tık — doygunluk\n• Ctrl+Shift+sağ tık — parlaklık",
"print": "Yazdır",
"close": "Kapat",
"dragDropHint": "Resimleri buraya sürükleyin\nveya Ctrl+O ile açın",
"dropHere": "Bırakın",
"imageLoadError": "Resim yüklenemedi"
}

View File

@@ -13,6 +13,7 @@ import '../widgets/image_canvas.dart';
import '../widgets/window_resize_zones.dart';
import '../shortcuts/app_shortcuts.dart';
import '../services/window_persistence.dart';
import '../l10n/app_localizations.dart';
class ViewerScreen extends StatefulWidget {
final List<String> initialFiles;
@@ -95,22 +96,22 @@ class _ViewerScreenState extends State<ViewerScreen> with WindowListener {
builder: (context) => StatefulBuilder(
builder: (context, setDialogState) => AlertDialog(
backgroundColor: const Color(0xFF2a2a2a),
title: const Text('Ayarlar',
style: TextStyle(color: Colors.white, fontSize: 18)),
title: Text(AppLocalizations.of(context)!.settingsTitle,
style: const TextStyle(color: Colors.white, fontSize: 18)),
content: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('Sıralama',
style: TextStyle(color: Colors.white70, fontSize: 13)),
Text(AppLocalizations.of(context)!.sortBy,
style: const TextStyle(color: Colors.white70, fontSize: 13)),
RadioListTile<bool>(
value: false,
groupValue: ImageManager.instance.sortByName,
dense: true,
activeColor: Colors.blueAccent,
title: const Text('Zamana göre',
style: TextStyle(color: Colors.white, fontSize: 14)),
title: Text(AppLocalizations.of(context)!.sortByTime,
style: const TextStyle(color: Colors.white, fontSize: 14)),
onChanged: (v) {
ImageManager.instance.setSortByName(v!);
setDialogState(() {});
@@ -121,8 +122,8 @@ class _ViewerScreenState extends State<ViewerScreen> with WindowListener {
groupValue: ImageManager.instance.sortByName,
dense: true,
activeColor: Colors.blueAccent,
title: const Text('İsme göre',
style: TextStyle(color: Colors.white, fontSize: 14)),
title: Text(AppLocalizations.of(context)!.sortByName,
style: const TextStyle(color: Colors.white, fontSize: 14)),
onChanged: (v) {
ImageManager.instance.setSortByName(v!);
setDialogState(() {});
@@ -131,25 +132,17 @@ class _ViewerScreenState extends State<ViewerScreen> with WindowListener {
const Divider(color: Colors.white24, height: 24),
const Text('imajViewer', style: TextStyle(color: Colors.white, fontSize: 15)),
const SizedBox(height: 4),
const Text(
'Ultra hafif ve hızlı resim görüntüleyici. '
'Görselleri açın, gezinti için okları veya klavye tuşlarını kullanın.',
style: TextStyle(color: Colors.white70, fontSize: 13, height: 1.5),
Text(
AppLocalizations.of(context)!.appDescription,
style: const TextStyle(color: Colors.white70, fontSize: 13, height: 1.5),
),
const SizedBox(height: 12),
const Text('Kısayollar',
style: TextStyle(color: Colors.white70, fontSize: 13)),
Text(AppLocalizations.of(context)!.shortcuts,
style: const TextStyle(color: Colors.white70, fontSize: 13)),
const SizedBox(height: 4),
const Text(
'• Sol/Sağ ok — önceki/sonraki resim\n'
'• Ctrl+Q — pencereyi kapat\n'
'• Ctrl+Shift+Q — tüm pencereleri kapat\n'
'• Çift tık — doldur/sığdır geçişi\n'
'• Sağ tık sürükle — kontrast\n'
'• Shift+sağ tık — döndür\n'
'• Ctrl+sağ tık — doygunluk\n'
'• Ctrl+Shift+sağ tık — parlaklık',
style: TextStyle(color: Colors.white70, fontSize: 13, height: 1.6),
Text(
AppLocalizations.of(context)!.shortcutsList,
style: const TextStyle(color: Colors.white70, fontSize: 13, height: 1.6),
),
],
),
@@ -161,12 +154,12 @@ class _ViewerScreenState extends State<ViewerScreen> with WindowListener {
_printImage();
},
icon: const Icon(Icons.print, color: Colors.white70, size: 18),
label: const Text('Yazdır',
style: TextStyle(color: Colors.white70)),
label: Text(AppLocalizations.of(context)!.print,
style: const TextStyle(color: Colors.white70)),
),
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Kapat', style: TextStyle(color: Colors.white70)),
child: Text(AppLocalizations.of(context)!.close, style: const TextStyle(color: Colors.white70)),
),
],
),
@@ -232,15 +225,16 @@ class _ViewerScreenState extends State<ViewerScreen> with WindowListener {
? ImageCanvas(
filePath: ImageManager.instance.currentImagePath,
)
: const Center(
: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.image, size: 64, color: Colors.white24),
SizedBox(height: 16),
Text('Resimleri buraya sürükleyin\nveya Ctrl+O ile açın',
const Icon(Icons.image, size: 64, color: Colors.white24),
const SizedBox(height: 16),
Text(AppLocalizations.of(context)!.dragDropHint,
key: const Key('dragDropHint'),
textAlign: TextAlign.center,
style: TextStyle(color: Colors.white38, fontSize: 16)),
style: const TextStyle(color: Colors.white38, fontSize: 16)),
],
),
),
@@ -280,8 +274,8 @@ class _ViewerScreenState extends State<ViewerScreen> with WindowListener {
borderRadius: BorderRadius.circular(16),
border: Border.all(color: Colors.blueAccent, width: 2),
),
child: const Text('Bırakın',
style: TextStyle(color: Colors.white, fontSize: 24)),
child: Text(AppLocalizations.of(context)!.dropHere,
style: const TextStyle(color: Colors.white, fontSize: 24)),
),
),
),

View File

@@ -4,6 +4,7 @@ import 'package:flutter/material.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter/services.dart';
import '../l10n/app_localizations.dart';
class ImageCanvas extends StatefulWidget {
final String filePath;
@@ -203,7 +204,7 @@ class _ImageCanvasState extends State<ImageCanvas> {
final double cy = (vy - _ty) / _sc;
const double factor = 1.1;
final double rawSc = _sc * (e.scrollDelta.dy < 0 ? factor : 1.0 / factor);
final double newSc = rawSc.clamp(1.0, 10.0);
final double newSc = rawSc.clamp(1.0, 20.0);
if (_angle == 0) {
_tx = _tx + cx * _sc - cx * newSc;
_ty = _ty + cy * _sc - cy * newSc;
@@ -373,7 +374,7 @@ class _ImageCanvasState extends State<ImageCanvas> {
Widget _buildImage() {
final longest = _vpSize.longestSide;
final cacheWidth = longest > 0 ? (longest * 2).round() : null;
final cacheWidth = longest > 0 ? (longest * _sc * 2).round() : null;
if (_cachedPath != widget.filePath || _cachedCacheWidth != cacheWidth) {
_cachedPath = widget.filePath;
_cachedCacheWidth = cacheWidth;
@@ -388,6 +389,7 @@ class _ImageCanvasState extends State<ImageCanvas> {
image: provider,
fit: _isFilled ? BoxFit.cover : BoxFit.contain,
filterQuality: FilterQuality.high,
gaplessPlayback: true,
errorBuilder: (_, _, _) => _errorWidget(),
);
}
@@ -494,13 +496,14 @@ class _ImageCanvasState extends State<ImageCanvas> {
}
Widget _errorWidget() {
return const Center(
return Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.broken_image, size: 48, color: Colors.white38),
SizedBox(height: 8),
Text('Resim yüklenemedi', style: TextStyle(color: Colors.white38)),
const Icon(Icons.broken_image, size: 48, color: Colors.white38),
const SizedBox(height: 8),
Text(AppLocalizations.of(context)!.imageLoadError,
style: const TextStyle(color: Colors.white38)),
],
),
);

View File

@@ -7,7 +7,7 @@ project(runner LANGUAGES CXX)
set(BINARY_NAME "imajviewer")
# The unique GTK application identifier for this application. See:
# https://wiki.gnome.org/HowDoI/ChooseApplicationID
set(APPLICATION_ID "com.example.imajviewer")
set(APPLICATION_ID "com.softmediadesign.imajviewer")
# Explicitly opt in to modern CMake behaviors to avoid warnings with recent
# versions of CMake.

View File

@@ -150,6 +150,11 @@ packages:
url: "https://pub.dev"
source: hosted
version: "6.0.0"
flutter_localizations:
dependency: "direct main"
description: flutter
source: sdk
version: "0.0.0"
flutter_plugin_android_lifecycle:
dependency: transitive
description:
@@ -200,6 +205,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "4.9.1"
intl:
dependency: transitive
description:
name: intl
sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5"
url: "https://pub.dev"
source: hosted
version: "0.20.2"
json_annotation:
dependency: transitive
description:

View File

@@ -1,5 +1,5 @@
name: imajviewer
description: "Ultra hafif görüntü izleyici"
description: "Ultra-lightweight image viewer for Linux and Windows."
publish_to: 'none'
version: 1.1.0+3
@@ -17,6 +17,8 @@ environment:
dependencies:
flutter:
sdk: flutter
flutter_localizations:
sdk: flutter
window_manager: ^0.4.3
screen_retriever: ^0.2.2
file_picker: ^8.0.0
@@ -33,3 +35,4 @@ dev_dependencies:
flutter:
uses-material-design: true
generate: true

View File

@@ -1,3 +1,4 @@
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:imajviewer/app.dart';
@@ -9,7 +10,7 @@ void main() {
// Pump pending timers (Future.delayed in initState + polling)
await tester.pump(const Duration(milliseconds: 500));
// Should show drag-drop hint text
expect(find.textContaining('sürükleyin'), findsOneWidget);
// Should show drag-drop hint (Key-based, locale independent)
expect(find.byKey(const Key('dragDropHint')), findsOneWidget);
});
}

View File

@@ -89,11 +89,11 @@ BEGIN
BEGIN
BLOCK "040904e4"
BEGIN
VALUE "CompanyName", "com.example" "\0"
VALUE "CompanyName", "com.softmediadesign" "\0"
VALUE "FileDescription", "imajviewer" "\0"
VALUE "FileVersion", VERSION_AS_STRING "\0"
VALUE "InternalName", "imajviewer" "\0"
VALUE "LegalCopyright", "Copyright (C) 2026 com.example. All rights reserved." "\0"
VALUE "LegalCopyright", "Copyright (C) 2026 com.softmediadesign. All rights reserved." "\0"
VALUE "OriginalFilename", "imajviewer.exe" "\0"
VALUE "ProductName", "imajviewer" "\0"
VALUE "ProductVersion", VERSION_AS_STRING "\0"