Initial commit: imajViewer Flutter Linux image viewer

This commit is contained in:
Alhan
2026-07-22 03:59:14 +03:00
commit 8369ec2a5d
33 changed files with 2165 additions and 0 deletions

45
.gitignore vendored Normal file
View File

@@ -0,0 +1,45 @@
# Miscellaneous
*.class
*.log
*.pyc
*.swp
.DS_Store
.atom/
.build/
.buildlog/
.history
.svn/
.swiftpm/
migrate_working_dir/
# IntelliJ related
*.iml
*.ipr
*.iws
.idea/
# The .vscode folder contains launch configuration and tasks you configure in
# VS Code which you may wish to be included in version control, so this line
# is commented out by default.
#.vscode/
# Flutter/Dart/Pub related
**/doc/api/
**/ios/Flutter/.last_build_id
.dart_tool/
.flutter-plugins-dependencies
.pub-cache/
.pub/
/build/
/coverage/
# Symbolication related
app.*.symbols
# Obfuscation related
app.*.map.json
# Android Studio will place build artifacts here
/android/app/debug
/android/app/profile
/android/app/release

30
.metadata Normal file
View File

@@ -0,0 +1,30 @@
# This file tracks properties of this Flutter project.
# Used by Flutter tool to assess capabilities and perform upgrades etc.
#
# This file should be version controlled and should not be manually edited.
version:
revision: "84fc5cbb223bc12f83d65b647ff8a56caf779ffd"
channel: "stable"
project_type: app
# Tracks metadata for the flutter migrate command
migration:
platforms:
- platform: root
create_revision: 84fc5cbb223bc12f83d65b647ff8a56caf779ffd
base_revision: 84fc5cbb223bc12f83d65b647ff8a56caf779ffd
- platform: linux
create_revision: 84fc5cbb223bc12f83d65b647ff8a56caf779ffd
base_revision: 84fc5cbb223bc12f83d65b647ff8a56caf779ffd
# User provided section
# List of Local paths (relative to this file) that should be
# ignored by the migrate tool.
#
# Files that are not part of the templates will be ignored by default.
unmanaged_files:
- 'lib/main.dart'
- 'ios/Runner.xcodeproj/project.pbxproj'

209
Implementation.md Normal file
View File

@@ -0,0 +1,209 @@
# Implementation — imajViewer
> Teknik uygulama dokümanı
> Versiyon: 1.0
---
## 1. Mimari Genel Bakış
```
┌──────────────────────────────────────────────────┐
│ Flutter App │
│ ┌────────────┐ ┌────────────────────────────┐ │
│ │ window_mgr │ │ ViewerScreen │ │
│ │ (frameless)│ │ ┌──────────────────────┐ │ │
│ └────────────┘ │ │ CustomTitleBar │ │ │
│ │ │ (minimize, close) │ │ │
│ │ ├──────────────────────┤ │ │
│ │ │ ImageCanvas │ │ │
│ │ │ (InteractiveViewer) │ │ │
│ │ │ - Scroll → Zoom │ │ │
│ │ │ - Drag → Pan │ │ │
│ │ └──────────────────────┘ │ │
│ └────────────────────────────┘ │
│ ┌────────────────────────────────────────────┐ │
│ │ ImageCacheManager │ │
│ │ - PrecacheImage │ │
│ │ - LRU eviction (> N images) │ │
│ │ - File → Memory → GPU Texture │ │
│ └────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────┘
```
## 2. Bileşen Ağacı (Widget Tree)
```
MaterialApp (dark theme)
└── ViewerScreen (StatefulWidget)
├── Stack
│ ├── InteractiveViewer (zoom + pan)
│ │ └── RawImage (resim)
│ ├── CustomTitleBar (Positioned: top-right)
│ │ ├── IconButton (minimize)
│ │ └── IconButton (close)
│ └── (koyu arka plan)
└── DragTarget (sürükle-bırak dosya alanı)
```
## 3. Data Flow
```
Kullanıcı dosya bırakır / açar
ViewerScreen.dragResult / openFileDialog
ImageList (List<String> filePaths)
preloadImages() → ImageCache'e ekle
Indexed, currentIndex değişince
InteractiveViewer.builder → Image.file(path)
Flutter ImageCache → GPU Texture → Render
```
## 4. Adım Adım Uygulama
### Adım 1: Proje İskeleti
- `flutter create imajViewer`
- `--platforms=linux` ile oluştur
- Temele dark tema, frameless window ekle
**Dosyalar:** `main.dart`, `app.dart`
### Adım 2: window_manager Kurulumu
- pubspec.yaml: `window_manager: ^0.4.3`
- `main()` içinde `windowManager.ensureInitialized()`
- `waitUntilReadyToShow` → frameless ayarları:
- `setTitleBarStyle(TitleBarStyle.hidden)`
- `setSize(Size(1280, 800))`
- `setMinimumSize(Size(800, 600))`
- `setBackgroundColor(Colors.transparent)`
- Linux'ta `linux/my_app.cc`'de window_manager için ek ayar gerekebilir
**Dosyalar:** `lib/main.dart`, `pubspec.yaml`
### Adım 3: CustomTitleBar Widget
- Sağ üst köşede, sabit konumda
- Siyah yarı saydam arka plan
- minimize (—) ve kapat (X) butonları
- **Püf nokta:** Butonların üzerine gelince InteractiveViewer zoom yapmamalı
- `AbsorbPointer` veya `IgnorePointer` ile InteractiveViewer etkileşimini engelle
- Pencere boyutlandırma ve sürükleme kenarlıkları (window_manager'in `setResizable` özelliği)
**Dosya:** `lib/widgets/custom_title_bar.dart`
### Adım 4: ImageCanvas (Ana Görüntüleme)
- `InteractiveViewer` widget'ı
- `boundaryMargin: EdgeInsets.all(double.infinity)` → sınırsız kaydırma
- `minScale: 0.1`, `maxScale: 10.0`
- `onInteractionUpdate` → zoom oranını göstermek için (ops.)
- `transformationController` ile programatik zoom kontrolü
- Mouse wheel event: `Listener` ile `onPointerSignal` yakala
- `PointerScrollEvent` → transformationController ile zoom
- Fare imlecinin resim üzerindeki konumuna göre zoom odağı
**Püf nokta:**
`InteractiveViewer`'ın default scroll-to-zoom desteği vardır ama imleç odağında zoom
için `TransformationController`'a manuel matrix hesaplaması yapmak gerekir.
**Dosya:** `lib/widgets/image_canvas.dart`
### Adım 5: Resim Yönetimi (ImageManager)
- `ChangeNotifier` ile state yönetimi
- Dışarıdan dosya eklendiğinde `notifyListeners()` + `precacheImage()`
- `ImageCache.maximumSize` ve `maximumSizeBytes` ayarı
- **Performans stratejisi:**
- Sadece görünen resim bellekte tutulur
- 12+ resim için `ImageCache.maximumSizeBytes` = 512MB
- Resim değiştirince önceki resmin cache'ini düşürmeye gerek yok (Flutter LRU)
- Sürükle-bırak: `DragTarget<List<File>>` widget'ı
**Dosya:** `lib/services/image_manager.dart`
### Adım 6: Dosya Açma (Dialog + DragDrop)
- `file_picker` paketi ile `Ctrl+O` kısayolu
- `DragTarget` ile sürükle-bırak
- Desteklenen format filtreleri: PNG, JPEG, WebP, BMP, GIF
- **Dikkat:** Flutter Linux'ta DragTarget çalışıyor — ayrıca `window_manager`'dan `onDragFile` event'i de var
**Dosya:** `lib/services/file_handler.dart`
### Adım 7: Klavye Kısayolları
- Flutter'daki `Shortcuts` + `Actions` sistemi
- `← →` → resim değiştir
- `Ctrl+O` → dosya aç
- `F11` → tam ekran (fullscreen toggle)
- `Esc` → tam ekrandan çık
- `Ctrl+Q` → çıkış
**Dosya:** `lib/shortcuts/app_shortcuts.dart`
### Adım 8: Birleştirme ve Test
- Ana ekranı `ViewerScreen`'de topla
- `Listener(onPointerSignal)` + `InteractiveViewer` entegrasyonu
- `flutter run -d linux` ile test
- Performance profiling: `flutter run --profile`
### Adım 9: Derleme
- `flutter build linux --release`
- Binary: `build/linux/x64/release/bundle/imajViewer`
## 5. pubspec.yaml Bağımlılıkları
```yaml
dependencies:
flutter:
sdk: flutter
window_manager: ^0.4.3
file_picker: ^8.0.0
path: ^1.9.0
```
## 6. Önemli Dikkat Noktaları
### Zoom Odağı (Cursor-based Zoom)
InteractiveViewer varsayılan zoom odağı merkezdir. İmleç odağında zoom için
`TransformationController` ile manuel matrix çarpımı:
```
1. Scroll event yakala (PointerScrollEvent)
2. İmlecin resim üzerindeki koordinatını hesapla
3. Matrix4.translationValues(-txt) * Matrix4.diagonal3(values(scale)) * Matrix4.translationValues(txt)
```
Detaylı implementasyon `image_canvas.dart`'da yapılacak.
### Frameless Window Sürükleme
Window_manager zaten varsayılan olarak fare ile pencerenin herhangi bir
yerinden sürüklenmesine izin vermez. Bunun için:
- CustomTitleBar alanını `DragToMoveArea` ile sarmak
- Veya `windowManager.startDragging()` çağırmak
Biz `DragToMoveArea` widget'ını kullanacağız.
### Performans
- Flutter'ın `ImageCache`'i varsayılan olarak 50 MB ve 1000 entry ile sınırlıdır
- 12 adet 4K resim için `ImageCache.maximumSizeBytes` 1 GB'a çıkarılmalı
- `RepaintBoundary` ile sadece değişen bölgelerin yeniden çizilmesi sağlanır
- `Texture` bazlı render ile GPU'da işleme devam eder
## 7. Timeline
| Adım | İş | Tahmini Süre |
|---|---|---|
| 1-2 | Proje iskeleti + window_manager | 15 dk |
| 3 | CustomTitleBar | 15 dk |
| 4 | ImageCanvas + zoom | 30 dk |
| 5-6 | Resim yönetimi + dosya açma | 30 dk |
| 7 | Kısayollar | 10 dk |
| 8-9 | Birleştirme + build | 20 dk |
| **Toplam** | | **~2 saat** |

100
PRD.md Normal file
View File

@@ -0,0 +1,100 @@
# PRD — imajViewer
> Product Requirements Document
> Versiyon: 1.0
> Tarih: 2026-07-22
---
## 1. Ürün Özeti
**imajViewer**, Flutter ile geliştirilmiş, Linux masaüstünde çalışan ultra hafif bir görüntü izleyici uygulamasıdır.
12'den fazla yüksek çözünürlüklü resmi aynı anda açabilir, fare tekerleği ile anlık zoom yapabilir ve
tüm pencere alanını resim göstermek için kullanır.
## 2. Hedef Kitle
- Linux kullanıcıları
- Fotoğrafçılar, tasarımcılar (hızlı önizleme ihtiyacı)
- Yüksek sayıda resim arasında hızlıca gezinmek isteyen kullanıcılar
## 3. Fonksiyonel Gereksinimler
### F-01: Resim Açma
- Kullanıcı dosya yöneticisinden sürükle-bırak ile resim ekleyebilir
- Aynı anda 12+ resim açılabilir
- Desteklenen formatlar: PNG, JPEG, WebP, BMP, GIF (statik)
### F-02: Zoom
- Fare tekerleği (mouse scroll) ile kademesiz zoom
- Zoom odağı fare imlecinin bulunduğu nokta olmalı
- Zoom oranı: %10 - %1000 arası
### F-03: Frameless Pencere
- Native title bar yok, sadece sağ üstte küçült (minimize) ve kapat (close) butonları
- Pencere sürüklenebilir (üst kısımdan)
- Pencere boyutlandırılabilir (kenarlardan/köşelerden)
### F-04: Görüntüleme
- Scroll bar yok
- Resim pencereye sığacak şekilde başlangıçta yerleşir (fit)
- Siyah/zemin renkli arka plan
- Pencere yeniden boyutlandırılınca resim yeniden ortalanır
### F-05: Navigasyon (ileri versiyon)
- Klavye kısayolları (← → ile resim değiştirme)
- Alt kısımda thumbnail strip
## 4. Fonksiyonel Olmayan Gereksinimler
### NF-01: Performans
- 12 adet 4K (3840x2160) resim aynı anda açıkken akıcı zoom (en az 60 FPS)
- Bellek kullanımı 2 GB'ı geçmemeli
- Resimler GPU'da işlenmeli, CPU yeniden ölçekleme yapılmamalı
### NF-02: Başlangıç Süresi
- Uygulama 2 saniyeden kısa sürede açılmalı
### NF-03: Binary Boyutu
- Derlenmiş uygulama 50 MB'ı geçmemeli
### NF-04: Güvenilirlik
- Büyük resimler (10000x10000 pixel) uygulamayı çökertmemeli
- Bellek sınırıılınca en eski resim otomatik boşaltılmalı
## 5. UI Tasarım İlkeleri
- **Minimalist:** Sadece resim var, gereksiz hiçbir UI öğesi yok
- **Karanlık tema:** Siyah (#1a1a1a) zemin
- **Frameless:** Hiçbir pencere çerçevesi görünmez
- **Duyarlı:** Pencere boyutu değişince resim otomatik yeniden boyutlanır
## 6. Teknik Yığın
| Bileşen | Teknoloji |
|---|---|
| UI Framework | Flutter 3.44+ |
| Platform | Linux Desktop (GTK arkaplan) |
| Dil | Dart 3.12+ |
| Window Yönetimi | window_manager |
| State Yönetimi | ValueNotifier / ChangeNotifier |
| Resim İşleme | dart:ui Image + ImageCache |
| Render | Skia / Impeller (GPU) |
## 7. Kullanıcı Akışı
1. Uygulama açılır → boş siyah ekran
2. Kullanıcı dosyaları sürükler veya `Ctrl+O` ile açar
3. Resim(ler) pencereye sığacak şekilde görüntülenir
4. Fare tekerleği → zoom in/out (imleç odağında)
5. Fare sürükleme → resmi kaydırma (pan)
6. Pencere yeniden boyutlandırma → resim yeniden ortalanır
7. Sağ üst X → uygulama kapanır
## 8. Başarı Kriterleri
- [ ] 12 adet 4K resim açıkken zoom akıcı (60 FPS)
- [ ] Frameless pencere sorunsuz çalışıyor
- [ ] Scroll zoom imleç odağında çalışıyor
- [ ] Bellek kullanımı 2 GB altında
- [ ] Binary boyutu 50 MB altında

17
README.md Normal file
View File

@@ -0,0 +1,17 @@
# imajviewer
A new Flutter project.
## Getting Started
This project is a starting point for a Flutter application.
A few resources to get you started if this is your first Flutter project:
- [Learn Flutter](https://docs.flutter.dev/get-started/learn-flutter)
- [Write your first Flutter app](https://docs.flutter.dev/get-started/codelab)
- [Flutter learning resources](https://docs.flutter.dev/reference/learning-resources)
For help getting started with Flutter development, view the
[online documentation](https://docs.flutter.dev/), which offers tutorials,
samples, guidance on mobile development, and a full API reference.

28
analysis_options.yaml Normal file
View File

@@ -0,0 +1,28 @@
# This file configures the analyzer, which statically analyzes Dart code to
# check for errors, warnings, and lints.
#
# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
# invoked from the command line by running `flutter analyze`.
# The following line activates a set of recommended lints for Flutter apps,
# packages, and plugins designed to encourage good coding practices.
include: package:flutter_lints/flutter.yaml
linter:
# The lint rules applied to this project can be customized in the
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
# included above or to enable additional rules. A list of all available lints
# and their documentation is published at https://dart.dev/lints.
#
# Instead of disabling a lint rule for the entire project in the
# section below, it can also be suppressed for a single line of code
# or a specific dart file by using the `// ignore: name_of_lint` and
# `// ignore_for_file: name_of_lint` syntax on the line or in the file
# producing the lint.
rules:
# avoid_print: false # Uncomment to disable the `avoid_print` rule
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
# Additional information about this file can be found at
# https://dart.dev/guides/language/analysis-options

BIN
imajviewer-linux-x64.tar.gz Normal file

Binary file not shown.

10
imajviewer.desktop Normal file
View File

@@ -0,0 +1,10 @@
[Desktop Entry]
Type=Application
Name=imajViewer
Comment=Ultra hafif görüntü izleyici
Exec=imajviewer %F
Icon=imajviewer
Terminal=false
Categories=Graphics;Viewer;RasterGraphics;
MimeType=image/png;image/jpeg;image/webp;image/bmp;image/gif;image/jpg;
StartupNotify=false

BIN
imajviewer.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

103
install.sh Executable file
View File

@@ -0,0 +1,103 @@
#!/usr/bin/env bash
set -euo pipefail
APP=imajviewer
BUNDLE_DIR="$(cd "$(dirname "$0")/build/linux/x64/release/bundle" && pwd)"
INSTALL_DIR="${HOME}/.local/lib/${APP}"
BIN_LINK="${HOME}/.local/bin/${APP}"
DESKTOP_FILE="${HOME}/.local/share/applications/${APP}.desktop"
ICON_DIR="${HOME}/.local/share/icons/hicolor/64x64/apps"
ICON_FILE="${ICON_DIR}/${APP}.png"
# ── Renkler ──
RED='\033[0;31m'; GREEN='\033[0;32m'; CYAN='\033[0;36m'; NC='\033[0m'
info() { echo -e "${CYAN}${NC} $1"; }
ok() { echo -e "${GREEN}${NC} $1"; }
err() { echo -e "${RED}${NC} $1"; exit 1; }
# ── Ön koşul kontrol ──
command -v xdg-mime >/dev/null 2>&1 || err "xdg-mime bulunamadı (xdg-utils paketi gerekli)"
command -v xdg-desktop-menu >/dev/null 2>&1 || err "xdg-desktop-menu bulunamadı (xdg-utils paketi gerekli)"
if [ ! -f "$BUNDLE_DIR/${APP}" ]; then
info "Binary bulunamadı, Flutter build başlatılıyor…"
cd "$(dirname "$0")"
flutter build linux 2>&1 | tail -1
BUNDLE_DIR="$(cd "$(dirname "$0")/build/linux/x64/release/bundle" && pwd)"
[ -f "$BUNDLE_DIR/${APP}" ] || err "Build başarısız!"
fi
# ── Kurulum dizinleri ──
mkdir -p "$INSTALL_DIR" "$(dirname "$BIN_LINK")" "$(dirname "$DESKTOP_FILE")" "$ICON_DIR"
# ── Binary + kütüphaneler ──
info "Binary ve kütüphaneler kopyalanıyor…"
cp -r "$BUNDLE_DIR"/* "$INSTALL_DIR/"
chmod +x "$INSTALL_DIR/${APP}"
# ── Symlink ──
ln -sf "$INSTALL_DIR/${APP}" "$BIN_LINK"
ok "Symlink: $BIN_LINK$INSTALL_DIR/${APP}"
# ── İkon (64×64 PNG) ──
if [ ! -f "$ICON_FILE" ]; then
info "İkon oluşturuluyor…"
python3 -c "
import struct, zlib, sys
w=h=64; r=g=b=0xff
raw=b''
for y in range(h):
raw+=b'\\x00'
for x in range(w):
d=abs(x-32)+abs(y-32)
if d<24: r,g,b=100,140,255
elif d<30: r,g,b=200,220,255
else: r,g,b=40,45,50
raw+=bytes([r,g,b,255])
def c(t,d):
c=t+d
return struct.pack('>I',len(d))+c+struct.pack('>I',zlib.crc32(c)&0xffffffff)
with open('$ICON_FILE','wb') as f:
f.write(b'\\x89PNG\\r\\n\\x1a\\n')
f.write(c(b'IHDR',struct.pack('>IIBBBBB',w,h,8,6,0,0,0)))
f.write(c(b'IDAT',zlib.compress(raw)))
f.write(c(b'IEND',b''))
print(' İkon: $ICON_FILE')
"
fi
ok "İkon hazır"
# ── .desktop dosyası ──
info ".desktop dosyası oluşturuluyor…"
cat > "$DESKTOP_FILE" << EOF
[Desktop Entry]
Type=Application
Name=${APP}
Comment=Ultra hafif görüntü izleyici
Exec=${BIN_LINK} %F
Icon=${APP}
Terminal=false
Categories=Graphics;Viewer;RasterGraphics;
MimeType=image/png;image/jpeg;image/webp;image/bmp;image/gif;image/jpg;
StartupNotify=false
EOF
ok "${DESKTOP_FILE}"
# ── MIME default ──
info "Varsayılan görüntü izleyici olarak ayarlanıyor…"
for mime in image/png image/jpeg image/jpg image/webp image/bmp image/gif; do
xdg-mime default "${APP}.desktop" "$mime" 2>/dev/null || true
done
# ── Masaüstü veritabanını güncelle ──
which update-desktop-database &>/dev/null && update-desktop-database "${HOME}/.local/share/applications/" 2>/dev/null || true
which gtk-update-icon-cache &>/dev/null && gtk-update-icon-cache "${HOME}/.local/share/icons/hicolor/" 2>/dev/null || true
echo ""
echo -e "${GREEN}══════════════════════════════════════════${NC}"
echo -e "${GREEN} ${APP} kuruldu!${NC}"
echo -e "${GREEN} Bir PNG/JPG/WebP dosyasına çift tıklayın${NC}"
echo -e "${GREEN} veya 'xdg-open <dosya>' ile test edin${NC}"
echo -e "${GREEN}══════════════════════════════════════════${NC}"
echo ""
echo "Kaldırmak için: rm -rf ${INSTALL_DIR} ${BIN_LINK} ${DESKTOP_FILE} ${ICON_FILE}"

24
lib/app.dart Normal file
View File

@@ -0,0 +1,24 @@
import 'package:flutter/material.dart';
import 'screens/viewer_screen.dart';
class ImajViewerApp extends StatelessWidget {
final List<String> initialFiles;
const ImajViewerApp({super.key, this.initialFiles = const []});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'imajViewer',
debugShowCheckedModeBanner: false,
theme: ThemeData(
brightness: Brightness.dark,
scaffoldBackgroundColor: const Color(0xFF1a1a1a),
colorScheme: ColorScheme.fromSwatch(
brightness: Brightness.dark,
),
),
home: ViewerScreen(initialFiles: initialFiles),
);
}
}

42
lib/main.dart Normal file
View File

@@ -0,0 +1,42 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:window_manager/window_manager.dart';
import 'app.dart';
import 'services/image_manager.dart';
import 'services/window_persistence.dart';
void main(List<String> args) async {
WidgetsFlutterBinding.ensureInitialized();
ImageManager.configureCache();
await windowManager.ensureInitialized();
// Restore saved window position/size or use defaults
final saved = WindowPersistence.restore();
final defaultSize = const Size(1280, 800);
final windowOptions = WindowOptions(
size: saved?.size ?? defaultSize,
minimumSize: const Size(400, 300),
backgroundColor: Colors.transparent,
titleBarStyle: TitleBarStyle.hidden,
);
await windowManager.waitUntilReadyToShow(windowOptions, () async {
if (saved != null) {
await windowManager.setPosition(saved.position);
await windowManager.setSize(saved.size);
} else {
await windowManager.center();
}
await windowManager.setResizable(true);
await windowManager.show();
await windowManager.focus();
});
// Extract file paths from command-line arguments
final filePaths = args.where((a) => !a.startsWith('-') && a != Platform.script.path).toList();
runApp(ImajViewerApp(initialFiles: filePaths));
}

View File

@@ -0,0 +1,165 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:window_manager/window_manager.dart';
import 'package:desktop_drop/desktop_drop.dart';
import '../services/image_manager.dart';
import '../services/file_handler.dart';
import '../widgets/custom_title_bar.dart';
import '../widgets/image_canvas.dart';
import '../widgets/window_resize_zones.dart';
import '../shortcuts/app_shortcuts.dart';
import '../services/window_persistence.dart';
class ViewerScreen extends StatefulWidget {
final List<String> initialFiles;
const ViewerScreen({super.key, this.initialFiles = const []});
@override
State<ViewerScreen> createState() => _ViewerScreenState();
}
class _ViewerScreenState extends State<ViewerScreen> with WindowListener {
bool _hasImages = false;
bool _isDragging = false;
Timer? _saveDebounce;
Timer? _pollTimer;
Rect _lastBounds = Rect.zero;
@override
void initState() {
super.initState();
windowManager.addListener(this);
ImageManager.instance.addListener(_onImageManagerChanged);
if (widget.initialFiles.isNotEmpty) {
WidgetsBinding.instance.addPostFrameCallback((_) {
FileHandler.loadImagesFromPaths(widget.initialFiles);
});
}
// İlk kayıt + periyodik polling başlat
WidgetsBinding.instance.addPostFrameCallback((_) async {
await Future.delayed(const Duration(milliseconds: 200));
await _doSave();
_startPolling();
});
}
void _startPolling() {
_pollTimer?.cancel();
_pollTimer = Timer.periodic(const Duration(seconds: 2), (_) async {
try {
final bounds = await windowManager.getBounds();
if (bounds != _lastBounds) {
_lastBounds = bounds;
WindowPersistence.saveSync(
position: bounds.topLeft,
size: bounds.size,
);
}
} catch (_) {}
});
}
@override
void dispose() {
windowManager.removeListener(this);
ImageManager.instance.removeListener(_onImageManagerChanged);
_saveDebounce?.cancel();
_pollTimer?.cancel();
super.dispose();
}
/// Universal event handler
@override
void onWindowEvent(String eventName) {
switch (eventName) {
case 'close':
_doSave();
break;
case 'resize':
case 'move':
_saveDebounce?.cancel();
_saveDebounce = Timer(const Duration(milliseconds: 300), () {
_doSave();
});
break;
}
}
void _onImageManagerChanged() {
setState(() => _hasImages = ImageManager.instance.images.isNotEmpty);
}
Future<void> _doSave() async {
try {
final pos = await windowManager.getPosition();
final size = await windowManager.getSize();
_lastBounds = Rect.fromLTWH(pos.dx, pos.dy, size.width, size.height);
WindowPersistence.saveSync(position: pos, size: size);
} catch (_) {
// Silently ignore save errors
}
}
@override
Widget build(BuildContext context) {
ImageManager.instance.buildContext = context;
return AppShortcuts(
child: Scaffold(
backgroundColor: Colors.transparent,
body: WindowResizeZones(
child: DropTarget(
onDragEntered: (_) => setState(() => _isDragging = true),
onDragExited: (_) => setState(() => _isDragging = false),
onDragDone: (details) {
setState(() => _isDragging = false);
FileHandler.loadImagesFromPaths(
details.files.map((f) => f.path).toList());
},
child: Stack(
children: [
Container(color: const Color(0xFF1a1a1a)),
Container(
color: Colors.transparent,
child: _hasImages
? ImageCanvas(filePath: ImageManager.instance.currentImagePath)
: const 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',
textAlign: TextAlign.center,
style: TextStyle(color: Colors.white38, fontSize: 16)),
],
),
),
),
const CustomTitleBar(),
if (_isDragging)
Positioned.fill(
child: Container(
color: Colors.blue.withValues(alpha: 0.1),
child: Center(
child: Container(
padding: const EdgeInsets.all(32),
decoration: BoxDecoration(
color: Colors.black87,
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)),
),
),
),
),
],
),
),
),
),
);
}
}

View File

@@ -0,0 +1,28 @@
import 'package:file_picker/file_picker.dart';
import 'image_manager.dart';
class FileHandler {
/// Open file picker dialog and load images
static Future<void> openFileDialog() async {
final result = await FilePicker.platform.pickFiles(
type: FileType.custom,
allowedExtensions: ['png', 'jpg', 'jpeg', 'webp', 'bmp', 'gif'],
allowMultiple: true,
);
if (result != null && result.files.isNotEmpty) {
final paths = result.files
.where((f) => f.path != null)
.map((f) => f.path!)
.toList();
ImageManager.instance.addImages(paths);
}
}
/// Load images from given file paths
static void loadImagesFromPaths(List<String> paths) {
if (paths.isNotEmpty) {
ImageManager.instance.addImages(paths);
}
}
}

View File

@@ -0,0 +1,76 @@
import 'dart:io';
import 'package:flutter/material.dart';
class ImageManager extends ChangeNotifier {
static final ImageManager _instance = ImageManager._();
static ImageManager get instance => _instance;
ImageManager._();
final List<String> _images = [];
int _currentIndex = 0;
List<String> get images => List.unmodifiable(_images);
int get currentIndex => _currentIndex;
int get imageCount => _images.length;
String get currentImagePath {
if (_images.isEmpty) return '';
return _images[_currentIndex];
}
void addImages(List<String> paths) {
final validPaths = paths.where((p) {
final ext = p.toLowerCase().split('.').last;
const supported = ['png', 'jpg', 'jpeg', 'webp', 'bmp', 'gif'];
return supported.contains(ext) && File(p).existsSync();
}).toList();
if (validPaths.isEmpty) return;
_images.addAll(validPaths);
// Precache images
for (final path in validPaths) {
precacheImage(FileImage(File(path)), context!);
}
notifyListeners();
}
void setCurrentIndex(int index) {
if (index < 0 || index >= _images.length) return;
_currentIndex = index;
notifyListeners();
}
void nextImage() {
if (_images.isEmpty) return;
_currentIndex = (_currentIndex + 1) % _images.length;
notifyListeners();
}
void previousImage() {
if (_images.isEmpty) return;
_currentIndex = (_currentIndex - 1 + _images.length) % _images.length;
notifyListeners();
}
// Precache needs a BuildContext
BuildContext? context;
/// Configure image cache for high-res images
static void configureCache() {
PaintingBinding.instance.imageCache.maximumSize = 1000;
PaintingBinding.instance.imageCache.maximumSizeBytes = 1024 * 1024 * 1024; // 1 GB
}
set buildContext(BuildContext ctx) {
context = ctx;
}
void clear() {
_images.clear();
_currentIndex = 0;
notifyListeners();
}
}

View File

@@ -0,0 +1,49 @@
import 'dart:convert';
import 'dart:io';
import 'package:flutter/material.dart';
/// Persists and restores window position and size.
class WindowPersistence {
static File get _configFile {
final home = Platform.environment['HOME'] ?? '/tmp';
return File('$home/.config/imajviewer/window.json');
}
/// Save window rect synchronously (safe to call before destroy).
static void saveSync({
required Offset position,
required Size size,
}) {
final file = _configFile;
file.parent.createSync(recursive: true);
file.writeAsStringSync(jsonEncode({
'x': position.dx.toInt(),
'y': position.dy.toInt(),
'width': size.width.toInt(),
'height': size.height.toInt(),
}));
}
/// Restore saved window rect. Returns null if no saved config.
static ({Offset position, Size size})? restore() {
final file = _configFile;
if (!file.existsSync()) return null;
try {
final data = jsonDecode(file.readAsStringSync()) as Map<String, dynamic>;
final x = (data['x'] as num?)?.toDouble() ?? 0;
final y = (data['y'] as num?)?.toDouble() ?? 0;
final w = (data['width'] as num?)?.toDouble() ?? 1280;
final h = (data['height'] as num?)?.toDouble() ?? 800;
if (w < 200 || h < 200 || x < -1000 || y < -1000) return null;
return (
position: Offset(x, y),
size: Size(w, h),
);
} catch (_) {
return null;
}
}
}

View File

@@ -0,0 +1,60 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:window_manager/window_manager.dart';
import '../services/image_manager.dart';
import '../services/file_handler.dart';
class AppShortcuts extends StatelessWidget {
final Widget child;
const AppShortcuts({super.key, required this.child});
@override
Widget build(BuildContext context) {
return CallbackShortcuts(
bindings: {
// Navigate images
const SingleActivator(LogicalKeyboardKey.arrowLeft): () {
ImageManager.instance.previousImage();
},
const SingleActivator(LogicalKeyboardKey.arrowRight): () {
ImageManager.instance.nextImage();
},
// Open file dialog
const SingleActivator(
LogicalKeyboardKey.keyO,
control: true,
): () {
FileHandler.openFileDialog();
},
// Fullscreen toggle
const SingleActivator(LogicalKeyboardKey.f11): () async {
final isFullscreen = await windowManager.isFullScreen();
await windowManager.setFullScreen(!isFullscreen);
},
// Exit fullscreen
const SingleActivator(LogicalKeyboardKey.escape): () async {
final isFullscreen = await windowManager.isFullScreen();
if (isFullscreen) {
await windowManager.setFullScreen(false);
}
},
// Quit
const SingleActivator(
LogicalKeyboardKey.keyQ,
control: true,
): () {
windowManager.close();
},
},
child: Focus(
autofocus: true,
child: child,
),
);
}
}

View File

@@ -0,0 +1,78 @@
import 'package:flutter/material.dart';
import 'package:window_manager/window_manager.dart';
class CustomTitleBar extends StatelessWidget {
const CustomTitleBar({super.key});
@override
Widget build(BuildContext context) {
return Positioned(
top: 0,
left: 0,
right: 0,
child: DragToMoveArea(
child: SizedBox(
height: 40,
child: Stack(
children: [
Positioned.fill(
child: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Colors.black.withValues(alpha: 0.6),
Colors.black.withValues(alpha: 0.0),
],
),
),
),
),
Positioned(
right: 0,
top: 0,
bottom: 0,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
_BarButton(
icon: Icons.horizontal_rule,
onTap: () => windowManager.minimize(),
),
_BarButton(
icon: Icons.close,
onTap: () => windowManager.close(),
),
],
),
),
],
),
),
),
);
}
}
class _BarButton extends StatelessWidget {
final IconData icon;
final VoidCallback onTap;
const _BarButton({required this.icon, required this.onTap});
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: onTap,
child: MouseRegion(
cursor: SystemMouseCursors.click,
child: SizedBox(
width: 44,
height: 40,
child: Icon(icon, color: Colors.white54, size: 18),
),
),
);
}
}

View File

@@ -0,0 +1,125 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter/gestures.dart';
class ImageCanvas extends StatefulWidget {
final String filePath;
const ImageCanvas({super.key, required this.filePath});
@override
State<ImageCanvas> createState() => _ImageCanvasState();
}
class _ImageCanvasState extends State<ImageCanvas> {
late TransformationController _controller;
double _currentScale = 1.0;
@override
void initState() {
super.initState();
_controller = TransformationController();
_controller.addListener(_onTransformChanged);
}
@override
void didUpdateWidget(ImageCanvas oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.filePath != widget.filePath) {
_resetView();
}
}
void _resetView() {
_controller.value = Matrix4.identity();
_currentScale = 1.0;
}
void _onTransformChanged() {
_currentScale = _controller.value.getMaxScaleOnAxis();
}
void _handleScroll(PointerScrollEvent event) {
final renderBox = context.findRenderObject() as RenderBox?;
if (renderBox == null) return;
final localPosition = renderBox.globalToLocal(event.position);
final delta = event.scrollDelta.dy;
final scaleFactor = delta < 0 ? 1.1 : 1 / 1.1;
final newScale = (_currentScale * scaleFactor).clamp(0.1, 10.0);
if (newScale <= 0.1 || newScale >= 10.0) return;
final actualScale = newScale / _currentScale;
// Decompose current matrix: widget_point = scale * child_point + translation
// So: child_point = (widget_point - translation) / scale
final matrix = _controller.value;
final tx = matrix.getTranslation().x;
final ty = matrix.getTranslation().y;
final childX = (localPosition.dx - tx) / _currentScale;
final childY = (localPosition.dy - ty) / _currentScale;
// Build new matrix: zoom around child_point, then re-apply
final newMatrix = Matrix4.identity()
// ignore: deprecated_member_use
..translate(childX, childY)
// ignore: deprecated_member_use
..scale(actualScale)
// ignore: deprecated_member_use
..translate(-childX, -childY)
..multiply(_controller.value);
_controller.value = newMatrix;
}
@override
void dispose() {
_controller.removeListener(_onTransformChanged);
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Listener(
onPointerSignal: (event) {
if (event is PointerScrollEvent) {
_handleScroll(event);
}
},
child: InteractiveViewer(
transformationController: _controller,
boundaryMargin: const EdgeInsets.all(double.infinity),
minScale: 0.1,
maxScale: 10.0,
panEnabled: true,
scaleEnabled: false,
child: Center(
child: Image.file(
File(widget.filePath),
fit: BoxFit.contain,
filterQuality: FilterQuality.high,
errorBuilder: (context, error, stackTrace) {
return const 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),
),
],
),
);
},
),
),
),
);
}
}

View File

@@ -0,0 +1,118 @@
import 'package:flutter/material.dart';
import 'package:window_manager/window_manager.dart';
/// Resize edge zones for frameless windows.
class WindowResizeZones extends StatelessWidget {
final Widget child;
const WindowResizeZones({super.key, required this.child});
static const _edgeSize = 4.0;
static const _cornerSize = 10.0;
@override
Widget build(BuildContext context) {
return Stack(
children: [
child,
// Top edge
_ResizeEdge(
edge: ResizeEdge.top,
top: 0, left: _cornerSize, right: _cornerSize, height: _edgeSize,
),
// Bottom edge
_ResizeEdge(
edge: ResizeEdge.bottom,
bottom: 0, left: _cornerSize, right: _cornerSize, height: _edgeSize,
),
// Left edge
_ResizeEdge(
edge: ResizeEdge.left,
left: 0, top: _cornerSize, bottom: _cornerSize, width: _edgeSize,
),
// Right edge
_ResizeEdge(
edge: ResizeEdge.right,
right: 0, top: _cornerSize, bottom: _cornerSize, width: _edgeSize,
),
// Corners
_ResizeCorner(edge: ResizeEdge.topLeft, top: 0, left: 0),
_ResizeCorner(edge: ResizeEdge.topRight, top: 0, right: 0),
_ResizeCorner(edge: ResizeEdge.bottomLeft, bottom: 0, left: 0),
_ResizeCorner(edge: ResizeEdge.bottomRight, bottom: 0, right: 0),
],
);
}
}
class _ResizeEdge extends StatelessWidget {
final ResizeEdge edge;
final double? top, bottom, left, right, width, height;
const _ResizeEdge({
required this.edge,
this.top, this.bottom, this.left, this.right,
this.width, this.height,
});
MouseCursor _cursor(ResizeEdge e) {
return switch (e) {
ResizeEdge.top || ResizeEdge.bottom => SystemMouseCursors.resizeRow,
ResizeEdge.left || ResizeEdge.right => SystemMouseCursors.resizeColumn,
_ => SystemMouseCursors.basic,
};
}
@override
Widget build(BuildContext context) {
return Positioned(
top: top, bottom: bottom, left: left, right: right,
width: width, height: height,
child: MouseRegion(
cursor: _cursor(edge),
child: GestureDetector(
behavior: HitTestBehavior.opaque,
onPanStart: (_) => windowManager.startResizing(edge),
),
),
);
}
}
class _ResizeCorner extends StatelessWidget {
final ResizeEdge edge;
final double? top, bottom, left, right;
const _ResizeCorner({
required this.edge,
this.top, this.bottom, this.left, this.right,
});
MouseCursor _cursor(ResizeEdge e) {
return switch (e) {
ResizeEdge.topLeft || ResizeEdge.bottomRight =>
SystemMouseCursors.resizeDownLeft,
ResizeEdge.topRight || ResizeEdge.bottomLeft =>
SystemMouseCursors.resizeDownRight,
_ => SystemMouseCursors.basic,
};
}
@override
Widget build(BuildContext context) {
return Positioned(
top: top, bottom: bottom, left: left, right: right,
width: WindowResizeZones._cornerSize,
height: WindowResizeZones._cornerSize,
child: MouseRegion(
cursor: _cursor(edge),
child: GestureDetector(
behavior: HitTestBehavior.opaque,
onPanStart: (_) => windowManager.startResizing(edge),
),
),
);
}
}

1
linux/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
flutter/ephemeral

128
linux/CMakeLists.txt Normal file
View File

@@ -0,0 +1,128 @@
# Project-level configuration.
cmake_minimum_required(VERSION 3.13)
project(runner LANGUAGES CXX)
# The name of the executable created for the application. Change this to change
# the on-disk name of your application.
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")
# Explicitly opt in to modern CMake behaviors to avoid warnings with recent
# versions of CMake.
cmake_policy(SET CMP0063 NEW)
# Load bundled libraries from the lib/ directory relative to the binary.
set(CMAKE_INSTALL_RPATH "$ORIGIN/lib")
# Root filesystem for cross-building.
if(FLUTTER_TARGET_PLATFORM_SYSROOT)
set(CMAKE_SYSROOT ${FLUTTER_TARGET_PLATFORM_SYSROOT})
set(CMAKE_FIND_ROOT_PATH ${CMAKE_SYSROOT})
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
endif()
# Define build configuration options.
if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
set(CMAKE_BUILD_TYPE "Debug" CACHE
STRING "Flutter build mode" FORCE)
set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS
"Debug" "Profile" "Release")
endif()
# Compilation settings that should be applied to most targets.
#
# Be cautious about adding new options here, as plugins use this function by
# default. In most cases, you should add new options to specific targets instead
# of modifying this function.
function(APPLY_STANDARD_SETTINGS TARGET)
target_compile_features(${TARGET} PUBLIC cxx_std_14)
target_compile_options(${TARGET} PRIVATE -Wall -Werror)
target_compile_options(${TARGET} PRIVATE "$<$<NOT:$<CONFIG:Debug>>:-O3>")
target_compile_definitions(${TARGET} PRIVATE "$<$<NOT:$<CONFIG:Debug>>:NDEBUG>")
endfunction()
# Flutter library and tool build rules.
set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter")
add_subdirectory(${FLUTTER_MANAGED_DIR})
# System-level dependencies.
find_package(PkgConfig REQUIRED)
pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0)
# Application build; see runner/CMakeLists.txt.
add_subdirectory("runner")
# Run the Flutter tool portions of the build. This must not be removed.
add_dependencies(${BINARY_NAME} flutter_assemble)
# Only the install-generated bundle's copy of the executable will launch
# correctly, since the resources must in the right relative locations. To avoid
# people trying to run the unbundled copy, put it in a subdirectory instead of
# the default top-level location.
set_target_properties(${BINARY_NAME}
PROPERTIES
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/intermediates_do_not_run"
)
# Generated plugin build rules, which manage building the plugins and adding
# them to the application.
include(flutter/generated_plugins.cmake)
# === Installation ===
# By default, "installing" just makes a relocatable bundle in the build
# directory.
set(BUILD_BUNDLE_DIR "${PROJECT_BINARY_DIR}/bundle")
if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT)
set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE)
endif()
# Start with a clean build bundle directory every time.
install(CODE "
file(REMOVE_RECURSE \"${BUILD_BUNDLE_DIR}/\")
" COMPONENT Runtime)
set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data")
set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}/lib")
install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}"
COMPONENT Runtime)
install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}"
COMPONENT Runtime)
install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
COMPONENT Runtime)
foreach(bundled_library ${PLUGIN_BUNDLED_LIBRARIES})
install(FILES "${bundled_library}"
DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
COMPONENT Runtime)
endforeach(bundled_library)
# Copy the native assets provided by the build.dart from all packages.
set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/linux/")
install(DIRECTORY "${NATIVE_ASSETS_DIR}"
DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
COMPONENT Runtime)
# Fully re-copy the assets directory on each build to avoid having stale files
# from a previous install.
set(FLUTTER_ASSET_DIR_NAME "flutter_assets")
install(CODE "
file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\")
" COMPONENT Runtime)
install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}"
DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime)
# Install the AOT library on non-Debug builds only.
if(NOT CMAKE_BUILD_TYPE MATCHES "Debug")
install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
COMPONENT Runtime)
endif()

View File

@@ -0,0 +1,88 @@
# This file controls Flutter-level build steps. It should not be edited.
cmake_minimum_required(VERSION 3.10)
set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral")
# Configuration provided via flutter tool.
include(${EPHEMERAL_DIR}/generated_config.cmake)
# TODO: Move the rest of this into files in ephemeral. See
# https://github.com/flutter/flutter/issues/57146.
# Serves the same purpose as list(TRANSFORM ... PREPEND ...),
# which isn't available in 3.10.
function(list_prepend LIST_NAME PREFIX)
set(NEW_LIST "")
foreach(element ${${LIST_NAME}})
list(APPEND NEW_LIST "${PREFIX}${element}")
endforeach(element)
set(${LIST_NAME} "${NEW_LIST}" PARENT_SCOPE)
endfunction()
# === Flutter Library ===
# System-level dependencies.
find_package(PkgConfig REQUIRED)
pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0)
pkg_check_modules(GLIB REQUIRED IMPORTED_TARGET glib-2.0)
pkg_check_modules(GIO REQUIRED IMPORTED_TARGET gio-2.0)
set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/libflutter_linux_gtk.so")
# Published to parent scope for install step.
set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE)
set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE)
set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE)
set(AOT_LIBRARY "${PROJECT_DIR}/build/lib/libapp.so" PARENT_SCOPE)
list(APPEND FLUTTER_LIBRARY_HEADERS
"fl_basic_message_channel.h"
"fl_binary_codec.h"
"fl_binary_messenger.h"
"fl_dart_project.h"
"fl_engine.h"
"fl_json_message_codec.h"
"fl_json_method_codec.h"
"fl_message_codec.h"
"fl_method_call.h"
"fl_method_channel.h"
"fl_method_codec.h"
"fl_method_response.h"
"fl_plugin_registrar.h"
"fl_plugin_registry.h"
"fl_standard_message_codec.h"
"fl_standard_method_codec.h"
"fl_string_codec.h"
"fl_value.h"
"fl_view.h"
"flutter_linux.h"
)
list_prepend(FLUTTER_LIBRARY_HEADERS "${EPHEMERAL_DIR}/flutter_linux/")
add_library(flutter INTERFACE)
target_include_directories(flutter INTERFACE
"${EPHEMERAL_DIR}"
)
target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}")
target_link_libraries(flutter INTERFACE
PkgConfig::GTK
PkgConfig::GLIB
PkgConfig::GIO
)
add_dependencies(flutter flutter_assemble)
# === Flutter tool backend ===
# _phony_ is a non-existent file to force this command to run every time,
# since currently there's no way to get a full input/output list from the
# flutter tool.
add_custom_command(
OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS}
${CMAKE_CURRENT_BINARY_DIR}/_phony_
COMMAND ${CMAKE_COMMAND} -E env
${FLUTTER_TOOL_ENVIRONMENT}
"${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.sh"
${FLUTTER_TARGET_PLATFORM} ${CMAKE_BUILD_TYPE}
VERBATIM
)
add_custom_target(flutter_assemble DEPENDS
"${FLUTTER_LIBRARY}"
${FLUTTER_LIBRARY_HEADERS}
)

View File

@@ -0,0 +1,23 @@
//
// Generated file. Do not edit.
//
// clang-format off
#include "generated_plugin_registrant.h"
#include <desktop_drop/desktop_drop_plugin.h>
#include <screen_retriever_linux/screen_retriever_linux_plugin.h>
#include <window_manager/window_manager_plugin.h>
void fl_register_plugins(FlPluginRegistry* registry) {
g_autoptr(FlPluginRegistrar) desktop_drop_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "DesktopDropPlugin");
desktop_drop_plugin_register_with_registrar(desktop_drop_registrar);
g_autoptr(FlPluginRegistrar) screen_retriever_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "ScreenRetrieverLinuxPlugin");
screen_retriever_linux_plugin_register_with_registrar(screen_retriever_linux_registrar);
g_autoptr(FlPluginRegistrar) window_manager_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "WindowManagerPlugin");
window_manager_plugin_register_with_registrar(window_manager_registrar);
}

View File

@@ -0,0 +1,15 @@
//
// Generated file. Do not edit.
//
// clang-format off
#ifndef GENERATED_PLUGIN_REGISTRANT_
#define GENERATED_PLUGIN_REGISTRANT_
#include <flutter_linux/flutter_linux.h>
// Registers Flutter plugins.
void fl_register_plugins(FlPluginRegistry* registry);
#endif // GENERATED_PLUGIN_REGISTRANT_

View File

@@ -0,0 +1,26 @@
#
# Generated file, do not edit.
#
list(APPEND FLUTTER_PLUGIN_LIST
desktop_drop
screen_retriever_linux
window_manager
)
list(APPEND FLUTTER_FFI_PLUGIN_LIST
)
set(PLUGIN_BUNDLED_LIBRARIES)
foreach(plugin ${FLUTTER_PLUGIN_LIST})
add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/linux plugins/${plugin})
target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin)
list(APPEND PLUGIN_BUNDLED_LIBRARIES $<TARGET_FILE:${plugin}_plugin>)
list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries})
endforeach(plugin)
foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST})
add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin})
list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries})
endforeach(ffi_plugin)

View File

@@ -0,0 +1,26 @@
cmake_minimum_required(VERSION 3.13)
project(runner LANGUAGES CXX)
# Define the application target. To change its name, change BINARY_NAME in the
# top-level CMakeLists.txt, not the value here, or `flutter run` will no longer
# work.
#
# Any new source files that you add to the application should be added here.
add_executable(${BINARY_NAME}
"main.cc"
"my_application.cc"
"${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc"
)
# Apply the standard set of build settings. This can be removed for applications
# that need different build settings.
apply_standard_settings(${BINARY_NAME})
# Add preprocessor definitions for the application ID.
add_definitions(-DAPPLICATION_ID="${APPLICATION_ID}")
# Add dependency libraries. Add any application-specific dependencies here.
target_link_libraries(${BINARY_NAME} PRIVATE flutter)
target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK)
target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}")

6
linux/runner/main.cc Normal file
View File

@@ -0,0 +1,6 @@
#include "my_application.h"
int main(int argc, char** argv) {
g_autoptr(MyApplication) app = my_application_new();
return g_application_run(G_APPLICATION(app), argc, argv);
}

View File

@@ -0,0 +1,148 @@
#include "my_application.h"
#include <flutter_linux/flutter_linux.h>
#ifdef GDK_WINDOWING_X11
#include <gdk/gdkx.h>
#endif
#include "flutter/generated_plugin_registrant.h"
struct _MyApplication {
GtkApplication parent_instance;
char** dart_entrypoint_arguments;
};
G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION)
// Called when first Flutter frame received.
static void first_frame_cb(MyApplication* self, FlView* view) {
gtk_widget_show(gtk_widget_get_toplevel(GTK_WIDGET(view)));
}
// Implements GApplication::activate.
static void my_application_activate(GApplication* application) {
MyApplication* self = MY_APPLICATION(application);
GtkWindow* window =
GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application)));
// Use a header bar when running in GNOME as this is the common style used
// by applications and is the setup most users will be using (e.g. Ubuntu
// desktop).
// If running on X and not using GNOME then just use a traditional title bar
// in case the window manager does more exotic layout, e.g. tiling.
// If running on Wayland assume the header bar will work (may need changing
// if future cases occur).
gboolean use_header_bar = TRUE;
#ifdef GDK_WINDOWING_X11
GdkScreen* screen = gtk_window_get_screen(window);
if (GDK_IS_X11_SCREEN(screen)) {
const gchar* wm_name = gdk_x11_screen_get_window_manager_name(screen);
if (g_strcmp0(wm_name, "GNOME Shell") != 0) {
use_header_bar = FALSE;
}
}
#endif
if (use_header_bar) {
GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new());
gtk_widget_show(GTK_WIDGET(header_bar));
gtk_header_bar_set_title(header_bar, "imajviewer");
gtk_header_bar_set_show_close_button(header_bar, TRUE);
gtk_window_set_titlebar(window, GTK_WIDGET(header_bar));
} else {
gtk_window_set_title(window, "imajviewer");
}
gtk_window_set_default_size(window, 1280, 720);
g_autoptr(FlDartProject) project = fl_dart_project_new();
fl_dart_project_set_dart_entrypoint_arguments(
project, self->dart_entrypoint_arguments);
FlView* view = fl_view_new(project);
GdkRGBA background_color;
// Background defaults to black, override it here if necessary, e.g. #00000000
// for transparent.
gdk_rgba_parse(&background_color, "#000000");
fl_view_set_background_color(view, &background_color);
gtk_widget_show(GTK_WIDGET(view));
gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view));
// Show the window when Flutter renders.
// Requires the view to be realized so we can start rendering.
g_signal_connect_swapped(view, "first-frame", G_CALLBACK(first_frame_cb),
self);
gtk_widget_realize(GTK_WIDGET(view));
fl_register_plugins(FL_PLUGIN_REGISTRY(view));
gtk_widget_grab_focus(GTK_WIDGET(view));
}
// Implements GApplication::local_command_line.
static gboolean my_application_local_command_line(GApplication* application,
gchar*** arguments,
int* exit_status) {
MyApplication* self = MY_APPLICATION(application);
// Strip out the first argument as it is the binary name.
self->dart_entrypoint_arguments = g_strdupv(*arguments + 1);
g_autoptr(GError) error = nullptr;
if (!g_application_register(application, nullptr, &error)) {
g_warning("Failed to register: %s", error->message);
*exit_status = 1;
return TRUE;
}
g_application_activate(application);
*exit_status = 0;
return TRUE;
}
// Implements GApplication::startup.
static void my_application_startup(GApplication* application) {
// MyApplication* self = MY_APPLICATION(object);
// Perform any actions required at application startup.
G_APPLICATION_CLASS(my_application_parent_class)->startup(application);
}
// Implements GApplication::shutdown.
static void my_application_shutdown(GApplication* application) {
// MyApplication* self = MY_APPLICATION(object);
// Perform any actions required at application shutdown.
G_APPLICATION_CLASS(my_application_parent_class)->shutdown(application);
}
// Implements GObject::dispose.
static void my_application_dispose(GObject* object) {
MyApplication* self = MY_APPLICATION(object);
g_clear_pointer(&self->dart_entrypoint_arguments, g_strfreev);
G_OBJECT_CLASS(my_application_parent_class)->dispose(object);
}
static void my_application_class_init(MyApplicationClass* klass) {
G_APPLICATION_CLASS(klass)->activate = my_application_activate;
G_APPLICATION_CLASS(klass)->local_command_line =
my_application_local_command_line;
G_APPLICATION_CLASS(klass)->startup = my_application_startup;
G_APPLICATION_CLASS(klass)->shutdown = my_application_shutdown;
G_OBJECT_CLASS(klass)->dispose = my_application_dispose;
}
static void my_application_init(MyApplication* self) {}
MyApplication* my_application_new() {
// Set the program name to the application ID, which helps various systems
// like GTK and desktop environments map this running application to its
// corresponding .desktop file. This ensures better integration by allowing
// the application to be recognized beyond its binary name.
g_set_prgname(APPLICATION_ID);
return MY_APPLICATION(g_object_new(my_application_get_type(),
"application-id", APPLICATION_ID, "flags",
G_APPLICATION_NON_UNIQUE, nullptr));
}

View File

@@ -0,0 +1,21 @@
#ifndef FLUTTER_MY_APPLICATION_H_
#define FLUTTER_MY_APPLICATION_H_
#include <gtk/gtk.h>
G_DECLARE_FINAL_TYPE(MyApplication,
my_application,
MY,
APPLICATION,
GtkApplication)
/**
* my_application_new:
*
* Creates a new Flutter-based application.
*
* Returns: a new #MyApplication.
*/
MyApplication* my_application_new();
#endif // FLUTTER_MY_APPLICATION_H_

338
pubspec.lock Normal file
View File

@@ -0,0 +1,338 @@
# Generated by pub
# See https://dart.dev/tools/pub/glossary#lockfile
packages:
async:
dependency: transitive
description:
name: async
sha256: e2eb0491ba5ddb6177742d2da23904574082139b07c1e33b8503b9f46f3e1a37
url: "https://pub.dev"
source: hosted
version: "2.13.1"
boolean_selector:
dependency: transitive
description:
name: boolean_selector
sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea"
url: "https://pub.dev"
source: hosted
version: "2.1.2"
characters:
dependency: transitive
description:
name: characters
sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b
url: "https://pub.dev"
source: hosted
version: "1.4.1"
clock:
dependency: transitive
description:
name: clock
sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b
url: "https://pub.dev"
source: hosted
version: "1.1.2"
collection:
dependency: transitive
description:
name: collection
sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76"
url: "https://pub.dev"
source: hosted
version: "1.19.1"
cross_file:
dependency: transitive
description:
name: cross_file
sha256: "92c9c43c383bfa1c32079d3bc492d55d6d4318044b7b47edaff8971cbb555c51"
url: "https://pub.dev"
source: hosted
version: "0.3.5+4"
desktop_drop:
dependency: "direct main"
description:
name: desktop_drop
sha256: aa1e797255bfbc76f9eb5aa4f61e5b68dbf69962ab1be6495816d2f251bc0d1f
url: "https://pub.dev"
source: hosted
version: "0.7.1"
fake_async:
dependency: transitive
description:
name: fake_async
sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44"
url: "https://pub.dev"
source: hosted
version: "1.3.3"
ffi:
dependency: transitive
description:
name: ffi
sha256: "6d7fd89431262d8f3125e81b50d3847a091d846eafcd4fdb88dd06f36d705a45"
url: "https://pub.dev"
source: hosted
version: "2.2.0"
file_picker:
dependency: "direct main"
description:
name: file_picker
sha256: ab13ae8ef5580a411c458d6207b6774a6c237d77ac37011b13994879f68a8810
url: "https://pub.dev"
source: hosted
version: "8.3.7"
flutter:
dependency: "direct main"
description: flutter
source: sdk
version: "0.0.0"
flutter_lints:
dependency: "direct dev"
description:
name: flutter_lints
sha256: "3105dc8492f6183fb076ccf1f351ac3d60564bff92e20bfc4af9cc1651f4e7e1"
url: "https://pub.dev"
source: hosted
version: "6.0.0"
flutter_plugin_android_lifecycle:
dependency: transitive
description:
name: flutter_plugin_android_lifecycle
sha256: "3854fe5e3bff0b113c658f260b90c95dea17c92db0f2addeac2e343dd9969785"
url: "https://pub.dev"
source: hosted
version: "2.0.35"
flutter_test:
dependency: "direct dev"
description: flutter
source: sdk
version: "0.0.0"
flutter_web_plugins:
dependency: transitive
description: flutter
source: sdk
version: "0.0.0"
json_annotation:
dependency: transitive
description:
name: json_annotation
sha256: "2a743920d81b7910627f68ee2c9ac1fc0bfee32b9fc3403587d7c6791ca12f80"
url: "https://pub.dev"
source: hosted
version: "4.12.0"
leak_tracker:
dependency: transitive
description:
name: leak_tracker
sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de"
url: "https://pub.dev"
source: hosted
version: "11.0.2"
leak_tracker_flutter_testing:
dependency: transitive
description:
name: leak_tracker_flutter_testing
sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1"
url: "https://pub.dev"
source: hosted
version: "3.0.10"
leak_tracker_testing:
dependency: transitive
description:
name: leak_tracker_testing
sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1"
url: "https://pub.dev"
source: hosted
version: "3.0.2"
lints:
dependency: transitive
description:
name: lints
sha256: "12f842a479589fea194fe5c5a3095abc7be0c1f2ddfa9a0e76aed1dbd26a87df"
url: "https://pub.dev"
source: hosted
version: "6.1.0"
matcher:
dependency: transitive
description:
name: matcher
sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861
url: "https://pub.dev"
source: hosted
version: "0.12.19"
material_color_utilities:
dependency: transitive
description:
name: material_color_utilities
sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b"
url: "https://pub.dev"
source: hosted
version: "0.13.0"
meta:
dependency: transitive
description:
name: meta
sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349"
url: "https://pub.dev"
source: hosted
version: "1.18.0"
path:
dependency: "direct main"
description:
name: path
sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5"
url: "https://pub.dev"
source: hosted
version: "1.9.1"
plugin_platform_interface:
dependency: transitive
description:
name: plugin_platform_interface
sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02"
url: "https://pub.dev"
source: hosted
version: "2.1.8"
screen_retriever:
dependency: transitive
description:
name: screen_retriever
sha256: ace919117a7520c13a50a6259e60c4a0d4cbe98809468792a91b5c5adada2aa6
url: "https://pub.dev"
source: hosted
version: "0.2.2"
screen_retriever_linux:
dependency: transitive
description:
name: screen_retriever_linux
sha256: "7b52006a5ceae1f3d5af7f77188c3290d6e7d8ded16d99809bea84967c65c257"
url: "https://pub.dev"
source: hosted
version: "0.2.2"
screen_retriever_macos:
dependency: transitive
description:
name: screen_retriever_macos
sha256: a1489b99cce597c45a54b9aae1cd94c8d4705353b7e0bb2457a6e4de44e0ad8a
url: "https://pub.dev"
source: hosted
version: "0.2.2"
screen_retriever_platform_interface:
dependency: transitive
description:
name: screen_retriever_platform_interface
sha256: "94a5535277510a63184ca178ce12a1449bc0b38618879aa1c18bf57369c5064a"
url: "https://pub.dev"
source: hosted
version: "0.2.2"
screen_retriever_windows:
dependency: transitive
description:
name: screen_retriever_windows
sha256: dafc6922b0bfbf1d48cf3ccbf519b4fff47bdcb820da1728ea6db675fecc9324
url: "https://pub.dev"
source: hosted
version: "0.2.2"
sky_engine:
dependency: transitive
description: flutter
source: sdk
version: "0.0.0"
source_span:
dependency: transitive
description:
name: source_span
sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab"
url: "https://pub.dev"
source: hosted
version: "1.10.2"
stack_trace:
dependency: transitive
description:
name: stack_trace
sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1"
url: "https://pub.dev"
source: hosted
version: "1.12.1"
stream_channel:
dependency: transitive
description:
name: stream_channel
sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d"
url: "https://pub.dev"
source: hosted
version: "2.1.4"
string_scanner:
dependency: transitive
description:
name: string_scanner
sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43"
url: "https://pub.dev"
source: hosted
version: "1.4.1"
term_glyph:
dependency: transitive
description:
name: term_glyph
sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e"
url: "https://pub.dev"
source: hosted
version: "1.2.2"
test_api:
dependency: transitive
description:
name: test_api
sha256: "949a932224383300f01be9221c39180316445ecb8e7547f70a41a35bf421fb9e"
url: "https://pub.dev"
source: hosted
version: "0.7.11"
universal_platform:
dependency: transitive
description:
name: universal_platform
sha256: "64e16458a0ea9b99260ceb5467a214c1f298d647c659af1bff6d3bf82536b1ec"
url: "https://pub.dev"
source: hosted
version: "1.1.0"
vector_math:
dependency: transitive
description:
name: vector_math
sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b
url: "https://pub.dev"
source: hosted
version: "2.2.0"
vm_service:
dependency: transitive
description:
name: vm_service
sha256: "0016aef94fc66495ac78af5859181e3f3bf2026bd8eecc72b9565601e19ab360"
url: "https://pub.dev"
source: hosted
version: "15.2.0"
web:
dependency: transitive
description:
name: web
sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a"
url: "https://pub.dev"
source: hosted
version: "1.1.1"
win32:
dependency: transitive
description:
name: win32
sha256: d7cb55e04cd34096cd3a79b3330245f54cb96a370a1c27adb3c84b917de8b08e
url: "https://pub.dev"
source: hosted
version: "5.15.0"
window_manager:
dependency: "direct main"
description:
name: window_manager
sha256: "732896e1416297c63c9e3fb95aea72d0355f61390263982a47fd519169dc5059"
url: "https://pub.dev"
source: hosted
version: "0.4.3"
sdks:
dart: ">=3.12.2 <4.0.0"
flutter: ">=3.38.0"

23
pubspec.yaml Normal file
View File

@@ -0,0 +1,23 @@
name: imajviewer
description: "Ultra hafif Linux görüntü izleyici"
publish_to: 'none'
version: 1.0.0+1
environment:
sdk: ^3.12.2
dependencies:
flutter:
sdk: flutter
window_manager: ^0.4.3
file_picker: ^8.0.0
path: ^1.9.0
desktop_drop: ^0.7.1
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^6.0.0
flutter:
uses-material-design: true

15
test/widget_test.dart Normal file
View File

@@ -0,0 +1,15 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:imajviewer/app.dart';
void main() {
testWidgets('App renders empty state', (WidgetTester tester) async {
await tester.pumpWidget(const ImajViewerApp());
// 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);
});
}