17 Commits
Author SHA1 Message Date
Alhan 1a6de71ff6 chore: dist swap — track 1.9.0 .deb, untrack 1.8.0 (still on Gitea v1.8.0 release)
Build & Release / build-linux (push) Canceled after 0s
Build & Release / build-windows (push) Canceled after 0s
Build & Release / create-release (push) Canceled after 0s
2026-10-31 04:16:20 +03:00
Alhan a5969cf28f feat: taskbar title shows file name, icon sizes 16-256, settings order, fixed 4-col size grid (v1.9.0) 2026-10-31 04:16:19 +03:00
Alhan eed0646237 chore: dist swap — track 1.8.0 .deb, untrack 1.7.2 (still on Gitea v1.7.2 release)
Build & Release / build-linux (push) Canceled after 0s
Build & Release / build-windows (push) Canceled after 0s
Build & Release / create-release (push) Canceled after 0s
2026-10-31 02:11:25 +03:00
Alhan 9a7480cd68 feat: window size presets — 4x8 screen-ratio grid in settings (v1.8.0)
- Settings 'Size' section: 8 col x 4 row grid, 31 buttons labeled a:b
  (a splits H, b splits W); top-left cell empty placeholder (no 1:1 —
  title bar double click already maximizes)
- Click resizes the window to work.width/b x work.height/a of the
  display's work area (screen_retriever visibleSize, taskbar excluded);
  position unchanged, no selection state tracking
- minimumSize 400x300 -> 160x100 (4:8 cell needs it)
- Settings file-properties rows left-aligned (were centered under path)
2026-10-31 02:10:39 +03:00
Alhan 8a80cba116 chore: point README at Gitea releases; build-windows.ps1 mirrors pubspec version
Build & Release / build-linux (push) Canceled after 0s
Build & Release / build-windows (push) Canceled after 0s
Build & Release / create-release (push) Canceled after 0s
2026-10-31 00:14:47 +03:00
Alhan 3b1fad18fa fix: right-button double click no longer wipes rotation mid-drag (v1.7.2)
Build & Release / build-linux (push) Canceled after 0s
Build & Release / build-windows (push) Canceled after 0s
Build & Release / create-release (push) Canceled after 0s
Two rotate drags started quickly at nearly the same spot were treated as a
double click: the reset was detected on button down against the previous down
time/position, so it wiped all effects and ignored the second drag, leaving
Ctrl+S to save an unrotated crop.

- Double click is now only counted for actual clicks (movement <= 4px) and is
  decided on button up; a drag breaks the chain.
- Pointer up is matched by pointer id, so a left-drag pan never counts as a click.
- dist: 1.7.2 .deb replaces the tracked 1.7.1 package.
2026-10-25 20:48:59 +03:00
Alhan 32fb12c210 feat: zoom percentage badge at bottom center (v1.7.1)
Build & Release / build-linux (push) Canceled after 0s
Build & Release / build-windows (push) Canceled after 0s
Build & Release / create-release (push) Canceled after 0s
- Percentage shown between the prev/next buttons, together with the hover
  controls, on a translucent background.
- Value is the on-screen scale relative to the image's real pixel size
  (100% = 1:1); updated on wheel zoom, fill/fit toggle, image change and
  window resize.
- Pixel-size header parsing moved to lib/services/image_header.dart, shared
  with the settings file-properties panel.
- dist: 1.7.1 .deb replaces the tracked 1.7.0 package.
2026-10-25 18:53:59 +03:00
Alhan abbde04e50 chore: rebuild 1.7.0 .deb with F11 + file properties
Build & Release / build-linux (push) Has been cancelled
Build & Release / build-windows (push) Has been cancelled
Build & Release / create-release (push) Has been cancelled
2026-10-21 02:22:24 +03:00
Alhan e6808aaf8e feat: file properties in settings (resolution, size, format, modified, path)
Build & Release / build-linux (push) Has been cancelled
Build & Release / build-windows (push) Has been cancelled
Build & Release / create-release (push) Has been cancelled
2026-10-21 02:10:08 +03:00
Alhan 805c7eedf2 chore: regenerate l10n base class (F11 doc line)
Build & Release / build-linux (push) Has been cancelled
Build & Release / build-windows (push) Has been cancelled
Build & Release / create-release (push) Has been cancelled
2026-10-21 01:45:36 +03:00
Alhan 2ae32e351d feat: F11 fullscreen toggle (Esc still exits)
Build & Release / build-linux (push) Has been cancelled
Build & Release / build-windows (push) Has been cancelled
Build & Release / create-release (push) Has been cancelled
2026-10-21 01:45:21 +03:00
Alhan ee456773d4 chore: ship 1.7.0 .deb as latest in dist/ + changelog (v1.7.0)
Build & Release / build-linux (push) Has been cancelled
Build & Release / build-windows (push) Has been cancelled
Build & Release / create-release (push) Has been cancelled
2026-10-20 23:14:49 +03:00
Alhan 57fa3d92dd feat: settings version display, complete shortcut list, Ctrl fine zoom (v1.7.0)
Build & Release / build-linux (push) Has been cancelled
Build & Release / build-windows (push) Has been cancelled
Build & Release / create-release (push) Has been cancelled
2026-10-20 23:09:13 +03:00
Alhan 5f4d4ffe1a fix: window resize crash by building with Flutter 3.47.7 (v1.6.3)
Build & Release / build-linux (push) Has been cancelled
Build & Release / build-windows (push) Has been cancelled
Build & Release / create-release (push) Has been cancelled
The GTK embedder's OpenGL compositor uploaded the frame with the window's new
dimensions while its pixel buffer still held the previous size, so memcpy read
past the end of the buffer and the process died with SIGSEGV
(fl_compositor_opengl.cc). Upstream fix: flutter#187626, present in Flutter
3.47.7 but not in 3.44.7.

Linux builds now use /home/alhan/development/flutter-3.47.7; pubspec.lock was
re-resolved for Dart 3.13.5 and the new tool added its build/platform excludes
to analysis_options.yaml. dist/ carries the 1.6.3 package instead of 1.6.2.
2026-10-20 22:22:17 +03:00
Alhan a2d2c0d151 chore: crop save JPEG quality 93 -> 95 (v1.6.2)
Build & Release / build-linux (push) Has been cancelled
Build & Release / build-windows (push) Has been cancelled
Build & Release / create-release (push) Has been cancelled
2026-10-14 04:04:54 +03:00
Alhan 6e13892ed1 fix: Ctrl+R flip also matches the physical R key (v1.6.1)
Build & Release / build-linux (push) Has been cancelled
Build & Release / build-windows (push) Has been cancelled
Build & Release / create-release (push) Has been cancelled
On some keymaps the R key resolves to a different logical key (observed on a
Turkish remote-desktop setup: the R key arrives as logical T), so the Ctrl+R
shortcut never fired even though the key events reached the app. The shortcut
now matches PhysicalKeyboardKey.keyR as well; a regression test covers the
wrong-layout case.
2026-10-14 03:33:16 +03:00
Alhan bad48167c8 feat: horizontal flip with R key, baked into crop save (v1.6.0)
- R mirrors the view about the viewport centre and clears the active effects
  (same reset as double right click); pressing R again restores the original
  orientation. Pan/zoom are kept.
- Image switch, fill/fit toggle and right double click also clear the flip.
- Crop save (Ctrl+S) mirrors the output too, so the saved JPEG matches the
  on-screen view (verified in combination with rotation).
- Shortcut lists (tr/en), README and CHANGELOG updated; deb rebuilt as 1.6.0.
2026-10-14 02:28:24 +03:00
32 changed files with 854 additions and 90 deletions

No files matched your search

+1 -1
View File
@@ -49,7 +49,7 @@ app.*.map.json
*.deb *.deb
*.tar.gz *.tar.gz
*.exe *.exe
!dist/imajviewer_1.5.0_amd64.deb !dist/imajviewer_1.9.0_amd64.deb
!dist/imajviewer_setup_1.1.0.exe !dist/imajviewer_setup_1.1.0.exe
deb/usr/lib/imajviewer/ deb/usr/lib/imajviewer/
release/ release/
+90
View File
@@ -4,6 +4,96 @@ All notable changes to imajViewer are documented in this file.
## [Unreleased] ## [Unreleased]
## [1.9.0] - 2026-10-31
### Added
- Taskbar/alt-tab title now shows the opened image's file name (falls back to
"imajViewer" when no image is open).
- Full hicolor icon size set (16–256 px) instead of only 64 px, so small-icon
contexts (Thunar "Open With", Xfce app menu) keep the symbol visible.
### Changed
- `install.sh` generates the full icon size set and rewrites the hicolor
index; uninstall hint updated.
- Settings size grid is now a fixed 4-column layout (the wide/narrow
transposition introduced in this release was removed).
## [1.8.0] - 2026-10-31
### Added
- Window size presets: 4×8 screen-ratio grid in settings; clicking a preset
resizes the window to that fraction of the screen work area (taskbar
excluded). Minimum window size lowered to 160×100.
### Fixed
- Settings file-properties rows were centered under the longest row (path);
now left-aligned.
## [1.7.2] - 2026-10-25
### Fixed
- Rotating sometimes vanished before saving: the right-button double-click reset was
detected on button *down* against the previous down time and position, so two rotate
drags started quickly at nearly the same spot were treated as a double click — all
effects (rotation included) were wiped and the second drag was ignored, so `Ctrl+S`
saved an unrotated crop. A double click is now only counted for actual clicks
(movement ≤ 4 px, decided on button up), a drag breaks the chain, and
`pointer up` is matched by pointer id so a left-drag pan never counts as a click.
## [1.7.1] - 2026-10-25
### Added
- Zoom percentage badge at the bottom center, between the previous/next buttons: shown
together with the hover controls, translucent background, live while wheeling
(100% = one image pixel per screen pixel), recomputed on fill/fit toggle, image
change and window resize.
### Changed
- Image header parsing (pixel size without decoding) moved from the viewer screen into
`lib/services/image_header.dart`; the settings file-properties panel and the zoom badge
use the same code.
## [1.7.0] - 2026-10-20
### Added
- Settings dialog shows the app version next to the name.
- Fine zoom: `Ctrl`+wheel zooms 1% per step (plain wheel stays 10% per step).
- Settings shortcut list completed: `Ctrl+O` (open), `Esc` (exit fullscreen), scroll/zoom,
`Ctrl`+scroll fine zoom, left-drag pan, double right click (reset effects).
### Changed
- README: zoom feature line and shortcut table updated to match.
## [1.6.3] - 2026-10-20
### Fixed
- `SIGSEGV` while resizing the window. The GTK embedder's OpenGL compositor uploaded the frame using
the window's new dimensions while its pixel buffer still held the previous size, so `memcpy` read
past the end of the buffer (`engine/.../linux/fl_compositor_opengl.cc`). Fixed upstream in
flutter#187626, which is not part of Flutter 3.44.7 — the Linux build now uses Flutter 3.47.7,
where the compositor derives the texture size from the framebuffer itself.
## [1.6.2] - 2026-10-14
### Changed
- Crop save JPEG quality raised from 93 to 95.
## [1.6.1] - 2026-10-14
### Fixed
- Horizontal flip shortcut (`Ctrl+R`) never fired when the keyboard layout resolves the R key to a
different logical key (reported with a Turkish/remote-desktop setup where the R key arrives as
logical `T`). The shortcut now matches the physical R key position as well.
## [1.6.0] - 2026-10-14
### Added
- Horizontal flip (`Ctrl+R`): mirrors the image left-right about the viewport centre and clears
the active effects (same reset as double right click); pressing `Ctrl+R` again restores the
original orientation. Zoom/pan are kept.
- Crop save (`Ctrl+S`) now bakes the flip into the output as well, so the saved JPEG matches
the mirrored view (also in combination with rotation).
## [1.5.0] - 2026-10-12 ## [1.5.0] - 2026-10-12
### Added ### Added
+25 -14
View File
@@ -1,8 +1,8 @@
# imajViewer # imajViewer
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
[![Build](https://github.com/alhan/imajviewer/actions/workflows/build.yml/badge.svg)](https://github.com/alhan/imajviewer/actions/workflows/build.yml)
[![Release](https://img.shields.io/github/v/release/alhan/imajviewer)](https://github.com/alhan/imajviewer/releases/latest) [Releases](https://git.softmediadesign.com/git_alhan/imajviewer/releases)
Ultra-lightweight image viewer written in Flutter — Linux & Windows. Ultra-lightweight image viewer written in Flutter — Linux & Windows.
@@ -12,20 +12,24 @@ Ultra-lightweight image viewer written in Flutter — Linux & Windows.
- **Formats:** PNG, JPEG, JFIF, WebP, BMP, GIF, WBMP - **Formats:** PNG, JPEG, JFIF, WebP, BMP, GIF, WBMP
- **Pan:** Left drag - **Pan:** Left drag
- **Zoom:** Mouse wheel (cursor-centered; viewport-centered in rotate mode) - **Zoom:** Mouse wheel — 10% per step, cursor-centered (viewport-centered in rotate mode);
hold `Ctrl` for fine zoom (1% per step)
- **Fill/Fit toggle:** Double left click - **Fill/Fit toggle:** Double left click
- **Contrast:** Right drag + vertical move - **Contrast:** Right drag + vertical move
- **Saturation:** Ctrl + Right drag + vertical move - **Saturation:** Ctrl + Right drag + vertical move
- **Brightness:** Ctrl+Shift + Right drag + vertical move - **Brightness:** Ctrl+Shift + Right drag + vertical move
- **Rotate:** Shift + Right drag + vertical move (~100px = 90°) - **Rotate:** Shift + Right drag + vertical move (~100px = 90°)
- **Flip horizontally:** `Ctrl+R` — mirrors the view and clears the active effects (same reset as
double right click); press `Ctrl+R` again to undo. Also matches the physical R key position, so
keymaps that report the R key as a different character still work
- **Reset (all effects):** Double right click - **Reset (all effects):** Double right click
- **Navigation arrows:** bottom corners, visible on hover - **Navigation arrows:** bottom corners, visible on hover
- **Settings (gear):** sort by time/name, print, shortcut info - **Settings (gear):** sort by time/name, print, shortcut info
- **Print:** send the image to a printer (A4, fit) - **Print:** send the image to a printer (A4, fit)
- **Save visible area:** title bar save icon or `Ctrl+S` — the on-screen region is written at - **Save visible area:** title bar save icon or `Ctrl+S` — the on-screen region is written at
original resolution as JPEG (quality 93) next to the source file (`photo.jpg` → original resolution as JPEG (quality 95) next to the source file (`photo.jpg` →
`photo_143025.jpg`); active effects (contrast/saturation/brightness) and rotation are baked `photo_143025.jpg`); active effects (contrast/saturation/brightness), rotation and horizontal
into the output; if that name already exists, nothing is written and a warning is shown flip are baked into the output; if that name already exists, nothing is written and a warning is shown
- Drag & drop to open images - Drag & drop to open images
- Window size/position memory (JSON persistence) - Window size/position memory (JSON persistence)
- Set as default image viewer - Set as default image viewer
@@ -37,7 +41,7 @@ Ultra-lightweight image viewer written in Flutter — Linux & Windows.
### Linux (.deb, recommended) ### Linux (.deb, recommended)
No Flutter SDK needed — grab the latest `.deb` from the No Flutter SDK needed — grab the latest `.deb` from the
[Releases page](https://github.com/alhan/imajviewer/releases/latest): [Releases page](https://git.softmediadesign.com/git_alhan/imajviewer/releases):
```bash ```bash
sudo dpkg -i imajviewer_*_amd64.deb sudo dpkg -i imajviewer_*_amd64.deb
@@ -49,7 +53,7 @@ After install, open an image with `xdg-open <file>` or double-click it.
### Windows (installer) ### Windows (installer)
Download `imajviewer_setup_*.exe` from the Download `imajviewer_setup_*.exe` from the
[Releases page](https://github.com/alhan/imajviewer/releases/latest) and run it. [Releases page](https://git.softmediadesign.com/git_alhan/imajviewer/releases) and run it.
The installer: The installer:
- Installs to Program Files - Installs to Program Files
@@ -80,23 +84,30 @@ folder including `imajviewer.exe` and all DLLs).
| Interaction | Action | | Interaction | Action |
|---|---| |---|---|
| Left drag | Pan | | Left drag | Pan |
| Mouse wheel | Zoom (cursor-centered) | | Mouse wheel | Zoom (cursor-centered, 10% per step) |
| Ctrl + Mouse wheel | Fine zoom (1% per step) |
| Double left click | Fill / Fit toggle | | Double left click | Fill / Fit toggle |
| Right drag + vertical | Contrast (0.0 – 2.0) | | Right drag + vertical | Contrast (0.0 – 2.0) |
| Ctrl + Right drag + vertical | Saturation (0.0 – 2.0) | | Ctrl + Right drag + vertical | Saturation (0.0 – 2.0) |
| Ctrl+Shift + Right drag + vertical | Brightness (-100 – 100) | | Ctrl+Shift + Right drag + vertical | Brightness (-100 – 100) |
| Shift + Right drag + vertical | Rotate (free) | | Shift + Right drag + vertical | Rotate (free) |
| Ctrl + R | Flip horizontally (clears effects; press again to undo) |
| Double right click | Reset (all effects) | | Double right click | Reset (all effects) |
| Left / Right arrow | Previous / next image | | Left / Right arrow | Previous / next image |
| Ctrl + S | Save visible area (JPEG 93, original resolution) | | Ctrl + O | Open file |
| Ctrl + S | Save visible area (JPEG 95, original resolution) |
| Esc | Exit fullscreen |
| Ctrl + Q | Close window | | Ctrl + Q | Close window |
| Ctrl + Shift + Q | Close all windows | | Ctrl + Shift + Q | Close all windows |
## CI / Releases ## Releases
GitHub Actions builds Linux `.deb` and Windows installer on every push to Installers are published on the
`main`. Pushing a `v*` tag creates a GitHub Release with both installers as [Gitea Releases page](https://git.softmediadesign.com/git_alhan/imajviewer/releases):
assets.
- Linux `.deb` — `flutter build linux --release` + `bash dist/rebuild-deb.sh`
- Windows installer — on a Windows machine: `.\build-windows.ps1`
(Flutter + VS 2022 Build Tools + Inno Setup 6)
## Development ## Development
+5
View File
@@ -7,6 +7,11 @@
# The following line activates a set of recommended lints for Flutter apps, # The following line activates a set of recommended lints for Flutter apps,
# packages, and plugins designed to encourage good coding practices. # packages, and plugins designed to encourage good coding practices.
analyzer:
exclude:
- build/**
- windows/**
- linux/**
include: package:flutter_lints/flutter.yaml include: package:flutter_lints/flutter.yaml
linter: linter:
+5
View File
@@ -136,6 +136,11 @@ if (-not $SkipInno) {
Write-Err "Installer scripti bulunamadi: $installerScript" Write-Err "Installer scripti bulunamadi: $installerScript"
} }
# Installer surumunu pubspec.yaml'dan aynala (CI'daki adimla ayni)
$pubVersion = (Select-String -Path (Join-Path $ProjectRoot 'pubspec.yaml') -Pattern '^version:\s*([0-9]+\.[0-9]+\.[0-9]+)').Matches[0].Groups[1].Value
(Get-Content $installerScript -Raw) -replace '#define MyAppVersion "[^"]+"', "#define MyAppVersion `"$pubVersion`"" | Set-Content $installerScript
Write-Ok "Inno Setup surumu pubspec'ten: $pubVersion"
$distDir = Join-Path $ProjectRoot "dist" $distDir = Join-Path $ProjectRoot "dist"
New-Item -ItemType Directory -Force -Path $distDir | Out-Null New-Item -ItemType Directory -Force -Path $distDir | Out-Null
Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 796 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.7 KiB

After

Width:  |  Height:  |  Size: 7.0 KiB

+30 -28
View File
@@ -11,8 +11,8 @@ fi
INSTALL_DIR="${HOME}/.local/lib/${APP}" INSTALL_DIR="${HOME}/.local/lib/${APP}"
BIN_LINK="${HOME}/.local/bin/${APP}" BIN_LINK="${HOME}/.local/bin/${APP}"
DESKTOP_FILE="${HOME}/.local/share/applications/${APP}.desktop" DESKTOP_FILE="${HOME}/.local/share/applications/${APP}.desktop"
ICON_DIR="${HOME}/.local/share/icons/hicolor/64x64/apps" ICON_BASE="${HOME}/.local/share/icons/hicolor"
ICON_FILE="${ICON_DIR}/${APP}.png" ICON_SIZES="16 22 24 32 48 64 128 256"
# ── Renkler ── # ── Renkler ──
RED='\033[0;31m'; GREEN='\033[0;32m'; CYAN='\033[0;36m'; NC='\033[0m' RED='\033[0;31m'; GREEN='\033[0;32m'; CYAN='\033[0;36m'; NC='\033[0m'
@@ -36,7 +36,7 @@ if [ ! -f "$BUNDLE_DIR/${APP}" ]; then
fi fi
# ── Kurulum dizinleri ── # ── Kurulum dizinleri ──
mkdir -p "$INSTALL_DIR" "$(dirname "$BIN_LINK")" "$(dirname "$DESKTOP_FILE")" "$ICON_DIR" mkdir -p "$INSTALL_DIR" "$(dirname "$BIN_LINK")" "$(dirname "$DESKTOP_FILE")"
# ── Binary + kütüphaneler ── # ── Binary + kütüphaneler ──
info "Binary ve kütüphaneler kopyalanıyor…" info "Binary ve kütüphaneler kopyalanıyor…"
@@ -47,37 +47,39 @@ chmod +x "$INSTALL_DIR/${APP}"
ln -sf "$INSTALL_DIR/${APP}" "$BIN_LINK" ln -sf "$INSTALL_DIR/${APP}" "$BIN_LINK"
ok "Symlink: $BIN_LINK → $INSTALL_DIR/${APP}" ok "Symlink: $BIN_LINK → $INSTALL_DIR/${APP}"
# ── İkon (64×64 PNG) ── # ── İkon (hicolor boyut seti: 16–256) ──
if [ ! -f "$ICON_FILE" ]; then if [ -f "$SCRIPT_DIR/imajviewericon.png" ]; then
if [ -f "$SCRIPT_DIR/imajviewericon.png" ]; then info "İkon boyutları üretiliyor (16–256)…"
info "İkon kopyalanıyor (64x64)…" for s in $ICON_SIZES; do
d="${ICON_BASE}/${s}x${s}/apps"
mkdir -p "$d"
python3 -c " python3 -c "
from PIL import Image from PIL import Image
im = Image.open('$SCRIPT_DIR/imajviewericon.png') im = Image.open('$SCRIPT_DIR/imajviewericon.png')
im.resize((64,64), Image.LANCZOS).save('$ICON_FILE') (im if $s == 256 else im.resize(($s, $s), Image.LANCZOS)).save('$d/${APP}.png')
" && ok "İkon: $ICON_FILE" || err "İkon dönüştürme başarısız!" " || err "İkon dönüştürme başarısız! ($s)"
else done
err "imajviewericon.png bulunamadı!" ok "İkon: ${ICON_BASE} (${ICON_SIZES} px)"
fi
else else
ok "İkon zaten mevcut" err "imajviewericon.png bulunamadı!"
fi fi
# ── Hicolor tema indeksi (GTK ikon temasını tanısın) ── # ── Hicolor tema indeksi (GTK ikon temasını tanısın) ──
if [ ! -f "${HOME}/.local/share/icons/hicolor/index.theme" ]; then {
mkdir -p "${HOME}/.local/share/icons/hicolor/64x64/apps" echo "[Icon Theme]"
cat > "${HOME}/.local/share/icons/hicolor/index.theme" << 'EOF' echo "Name=Hicolor"
[Icon Theme] echo "Comment=Fallback icon theme"
Name=Hicolor dirs=""
Comment=Fallback icon theme for s in $ICON_SIZES; do dirs="${dirs:+$dirs,}${s}x${s}/apps"; done
Directories=64x64/apps echo "Directories=$dirs"
for s in $ICON_SIZES; do
[64x64/apps] echo ""
Size=64 echo "[${s}x${s}/apps]"
Context=Applications echo "Size=$s"
Type=Fixed echo "Context=Applications"
EOF echo "Type=Fixed"
fi done
} > "${ICON_BASE}/index.theme"
# ── .desktop dosyası ── # ── .desktop dosyası ──
info ".desktop dosyası oluşturuluyor…" info ".desktop dosyası oluşturuluyor…"
@@ -113,4 +115,4 @@ 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} veya 'xdg-open <dosya>' ile test edin${NC}"
echo -e "${GREEN}══════════════════════════════════════════${NC}" echo -e "${GREEN}══════════════════════════════════════════${NC}"
echo "" echo ""
echo "Kaldırmak için: rm -rf ${INSTALL_DIR} ${BIN_LINK} ${DESKTOP_FILE} ${ICON_FILE}" echo "Kaldırmak için: rm -rf \"$INSTALL_DIR\" \"$BIN_LINK\" \"$DESKTOP_FILE\" && rm -f ${ICON_BASE}/*x*/apps/${APP}.png"
+9 -2
View File
@@ -4,12 +4,19 @@
"sortBy": "Sort by", "sortBy": "Sort by",
"sortByTime": "By time", "sortByTime": "By time",
"sortByName": "By name", "sortByName": "By name",
"sizeSection": "Size",
"appDescription": "Ultra-lightweight and fast image viewer. Open images and use arrows or keyboard keys to navigate.", "appDescription": "Ultra-lightweight and fast image viewer. Open images and use arrows or keyboard keys to navigate.",
"shortcuts": "Shortcuts", "shortcuts": "Shortcuts",
"shortcutsList": "• Left/Right arrow — previous/next image\n• Ctrl+S — save visible area (JPEG 93)\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", "shortcutsList": "• Left/Right arrow — previous/next image\n• Ctrl+O — open file\n• Ctrl+S — save visible area (JPEG 95)\n• Ctrl+R — flip horizontally (clears effects, press again to undo)\n• Ctrl+Q — close window\n• Ctrl+Shift+Q — close all windows\n• F11 — toggle fullscreen\n• Esc — exit fullscreen\n• Scroll — zoom (10% per step)\n• Ctrl+scroll — fine zoom (1% per step)\n• Left-drag — pan\n• Double click — toggle fill/fit\n• Right double click — reset effects\n• Right-drag — contrast\n• Shift+right-drag — rotate\n• Ctrl+right-drag — saturation\n• Ctrl+Shift+right-drag — brightness",
"print": "Print", "print": "Print",
"fileProperties": "File properties",
"propResolution": "Resolution",
"propFileSize": "File size",
"propFormat": "Format",
"propModified": "Modified",
"propPath": "Path",
"close": "Close", "close": "Close",
"saveCrop": "Save visible area (JPEG 93)", "saveCrop": "Save visible area (JPEG 95)",
"cropSaved": "Saved: {path}", "cropSaved": "Saved: {path}",
"@cropSaved": { "@cropSaved": {
"placeholders": { "placeholders": {
+44 -2
View File
@@ -122,6 +122,12 @@ abstract class AppLocalizations {
/// **'By name'** /// **'By name'**
String get sortByName; String get sortByName;
/// No description provided for @sizeSection.
///
/// In en, this message translates to:
/// **'Size'**
String get sizeSection;
/// No description provided for @appDescription. /// No description provided for @appDescription.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
@@ -137,7 +143,7 @@ abstract class AppLocalizations {
/// No description provided for @shortcutsList. /// No description provided for @shortcutsList.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'• Left/Right arrow — previous/next image\n• Ctrl+S — save visible area (JPEG 93)\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'** /// **'• Left/Right arrow — previous/next image\n• Ctrl+O — open file\n• Ctrl+S — save visible area (JPEG 95)\n• Ctrl+R — flip horizontally (clears effects, press again to undo)\n• Ctrl+Q — close window\n• Ctrl+Shift+Q — close all windows\n• F11 — toggle fullscreen\n• Esc — exit fullscreen\n• Scroll — zoom (10% per step)\n• Ctrl+scroll — fine zoom (1% per step)\n• Left-drag — pan\n• Double click — toggle fill/fit\n• Right double click — reset effects\n• Right-drag — contrast\n• Shift+right-drag — rotate\n• Ctrl+right-drag — saturation\n• Ctrl+Shift+right-drag — brightness'**
String get shortcutsList; String get shortcutsList;
/// No description provided for @print. /// No description provided for @print.
@@ -146,6 +152,42 @@ abstract class AppLocalizations {
/// **'Print'** /// **'Print'**
String get print; String get print;
/// No description provided for @fileProperties.
///
/// In en, this message translates to:
/// **'File properties'**
String get fileProperties;
/// No description provided for @propResolution.
///
/// In en, this message translates to:
/// **'Resolution'**
String get propResolution;
/// No description provided for @propFileSize.
///
/// In en, this message translates to:
/// **'File size'**
String get propFileSize;
/// No description provided for @propFormat.
///
/// In en, this message translates to:
/// **'Format'**
String get propFormat;
/// No description provided for @propModified.
///
/// In en, this message translates to:
/// **'Modified'**
String get propModified;
/// No description provided for @propPath.
///
/// In en, this message translates to:
/// **'Path'**
String get propPath;
/// No description provided for @close. /// No description provided for @close.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
@@ -155,7 +197,7 @@ abstract class AppLocalizations {
/// No description provided for @saveCrop. /// No description provided for @saveCrop.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'Save visible area (JPEG 93)'** /// **'Save visible area (JPEG 95)'**
String get saveCrop; String get saveCrop;
/// No description provided for @cropSaved. /// No description provided for @cropSaved.
+23 -2
View File
@@ -20,6 +20,9 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get sortByName => 'By name'; String get sortByName => 'By name';
@override
String get sizeSection => 'Size';
@override @override
String get appDescription => String get appDescription =>
'Ultra-lightweight and fast image viewer. Open images and use arrows or keyboard keys to navigate.'; 'Ultra-lightweight and fast image viewer. Open images and use arrows or keyboard keys to navigate.';
@@ -29,16 +32,34 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get shortcutsList => String get shortcutsList =>
'• Left/Right arrow — previous/next image\n• Ctrl+S — save visible area (JPEG 93)\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'; '• Left/Right arrow — previous/next image\n• Ctrl+O — open file\n• Ctrl+S — save visible area (JPEG 95)\n• Ctrl+R — flip horizontally (clears effects, press again to undo)\n• Ctrl+Q — close window\n• Ctrl+Shift+Q — close all windows\n• F11 — toggle fullscreen\n• Esc — exit fullscreen\n• Scroll — zoom (10% per step)\n• Ctrl+scroll — fine zoom (1% per step)\n• Left-drag — pan\n• Double click — toggle fill/fit\n• Right double click — reset effects\n• Right-drag — contrast\n• Shift+right-drag — rotate\n• Ctrl+right-drag — saturation\n• Ctrl+Shift+right-drag — brightness';
@override @override
String get print => 'Print'; String get print => 'Print';
@override
String get fileProperties => 'File properties';
@override
String get propResolution => 'Resolution';
@override
String get propFileSize => 'File size';
@override
String get propFormat => 'Format';
@override
String get propModified => 'Modified';
@override
String get propPath => 'Path';
@override @override
String get close => 'Close'; String get close => 'Close';
@override @override
String get saveCrop => 'Save visible area (JPEG 93)'; String get saveCrop => 'Save visible area (JPEG 95)';
@override @override
String cropSaved(String path) { String cropSaved(String path) {
+23 -2
View File
@@ -20,6 +20,9 @@ class AppLocalizationsTr extends AppLocalizations {
@override @override
String get sortByName => 'İsme göre'; String get sortByName => 'İsme göre';
@override
String get sizeSection => 'Boyut';
@override @override
String get appDescription => 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.'; '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.';
@@ -29,16 +32,34 @@ class AppLocalizationsTr extends AppLocalizations {
@override @override
String get shortcutsList => String get shortcutsList =>
'• Sol/Sağ ok — önceki/sonraki resim\n• Ctrl+S — görünen alanı kaydet (JPEG 93)\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'; '• Sol/Sağ ok — önceki/sonraki resim\n• Ctrl+O — dosya aç\n• Ctrl+S — görünen alanı kaydet (JPEG 95)\n• Ctrl+R — yatay aynala (efektleri sıfırlar, tekrar basınca geri alır)\n• Ctrl+Q — pencereyi kapat\n• Ctrl+Shift+Q — tüm pencereleri kapat\n• F11 — tam ekran / normal geçişi\n• Esc — tam ekrandan çık\n• Kaydırma — yakınlaştır (tık başına %10)\n• Ctrl+kaydırma — ince yakınlaştırma (tık başına %1)\n• Sol tık sürükle — kaydır\n• Çift tık — doldur/sığdır geçişi\n• Sağ çift tık — efektleri sıfırla\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 @override
String get print => 'Yazdır'; String get print => 'Yazdır';
@override
String get fileProperties => 'Dosya özellikleri';
@override
String get propResolution => 'Çözünürlük';
@override
String get propFileSize => 'Dosya boyutu';
@override
String get propFormat => 'Biçim';
@override
String get propModified => 'Değiştirilme';
@override
String get propPath => 'Yol';
@override @override
String get close => 'Kapat'; String get close => 'Kapat';
@override @override
String get saveCrop => 'Görünen alanı kaydet (JPEG 93)'; String get saveCrop => 'Görünen alanı kaydet (JPEG 95)';
@override @override
String cropSaved(String path) { String cropSaved(String path) {
+9 -2
View File
@@ -4,12 +4,19 @@
"sortBy": "Sıralama", "sortBy": "Sıralama",
"sortByTime": "Zamana göre", "sortByTime": "Zamana göre",
"sortByName": "İsme göre", "sortByName": "İsme göre",
"sizeSection": "Boyut",
"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.", "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", "shortcuts": "Kısayollar",
"shortcutsList": "• Sol/Sağ ok — önceki/sonraki resim\n• Ctrl+S — görünen alanı kaydet (JPEG 93)\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", "shortcutsList": "• Sol/Sağ ok — önceki/sonraki resim\n• Ctrl+O — dosya aç\n• Ctrl+S — görünen alanı kaydet (JPEG 95)\n• Ctrl+R — yatay aynala (efektleri sıfırlar, tekrar basınca geri alır)\n• Ctrl+Q — pencereyi kapat\n• Ctrl+Shift+Q — tüm pencereleri kapat\n• F11 — tam ekran / normal geçişi\n• Esc — tam ekrandan çık\n• Kaydırma — yakınlaştır (tık başına %10)\n• Ctrl+kaydırma — ince yakınlaştırma (tık başına %1)\n• Sol tık sürükle — kaydır\n• Çift tık — doldur/sığdır geçişi\n• Sağ çift tık — efektleri sıfırla\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", "print": "Yazdır",
"fileProperties": "Dosya özellikleri",
"propResolution": "Çözünürlük",
"propFileSize": "Dosya boyutu",
"propFormat": "Biçim",
"propModified": "Değiştirilme",
"propPath": "Yol",
"close": "Kapat", "close": "Kapat",
"saveCrop": "Görünen alanı kaydet (JPEG 93)", "saveCrop": "Görünen alanı kaydet (JPEG 95)",
"cropSaved": "Kaydedildi: {path}", "cropSaved": "Kaydedildi: {path}",
"@cropSaved": { "@cropSaved": {
"placeholders": { "placeholders": {
+1 -1
View File
@@ -18,7 +18,7 @@ void main(List<String> args) async {
final windowOptions = WindowOptions( final windowOptions = WindowOptions(
size: saved?.size ?? defaultSize, size: saved?.size ?? defaultSize,
minimumSize: const Size(400, 300), minimumSize: const Size(160, 100),
backgroundColor: Colors.transparent, backgroundColor: Colors.transparent,
titleBarStyle: TitleBarStyle.hidden, titleBarStyle: TitleBarStyle.hidden,
); );
+139 -2
View File
@@ -1,21 +1,26 @@
import 'dart:async'; import 'dart:async';
import 'dart:io'; import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:path/path.dart' as p;
import 'package:window_manager/window_manager.dart'; import 'package:window_manager/window_manager.dart';
import 'package:desktop_drop/desktop_drop.dart'; import 'package:desktop_drop/desktop_drop.dart';
import 'package:pdf/pdf.dart'; import 'package:pdf/pdf.dart';
import 'package:pdf/widgets.dart' as pw; import 'package:pdf/widgets.dart' as pw;
import 'package:printing/printing.dart'; import 'package:printing/printing.dart';
import '../services/image_header.dart';
import '../services/image_manager.dart'; import '../services/image_manager.dart';
import '../services/file_handler.dart'; import '../services/file_handler.dart';
import '../services/crop_save_service.dart'; import '../services/crop_save_service.dart';
import '../widgets/custom_title_bar.dart'; import '../widgets/custom_title_bar.dart';
import '../widgets/image_canvas.dart'; import '../widgets/image_canvas.dart';
import '../widgets/window_resize_zones.dart'; import '../widgets/window_resize_zones.dart';
import '../widgets/window_size_selector.dart';
import '../shortcuts/app_shortcuts.dart'; import '../shortcuts/app_shortcuts.dart';
import '../services/window_persistence.dart'; import '../services/window_persistence.dart';
import '../l10n/app_localizations.dart'; import '../l10n/app_localizations.dart';
const appVersion = '1.9.0';
class ViewerScreen extends StatefulWidget { class ViewerScreen extends StatefulWidget {
final List<String> initialFiles; final List<String> initialFiles;
const ViewerScreen({super.key, this.initialFiles = const []}); const ViewerScreen({super.key, this.initialFiles = const []});
@@ -29,6 +34,7 @@ class _ViewerScreenState extends State<ViewerScreen> with WindowListener {
bool _controlsVisible = false; bool _controlsVisible = false;
Timer? _saveDebounce; Timer? _saveDebounce;
Rect _lastBounds = Rect.zero; Rect _lastBounds = Rect.zero;
final ValueNotifier<double> _zoomPercent = ValueNotifier<double>(100);
final GlobalKey<ImageCanvasState> _canvasKey = GlobalKey<ImageCanvasState>(); final GlobalKey<ImageCanvasState> _canvasKey = GlobalKey<ImageCanvasState>();
@override @override
@@ -40,7 +46,10 @@ class _ViewerScreenState extends State<ViewerScreen> with WindowListener {
if (widget.initialFiles.isNotEmpty) { if (widget.initialFiles.isNotEmpty) {
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
FileHandler.loadImagesFromPaths(widget.initialFiles); FileHandler.loadImagesFromPaths(widget.initialFiles);
_updateTitle();
}); });
} else {
WidgetsBinding.instance.addPostFrameCallback((_) => _updateTitle());
} }
} }
@@ -49,6 +58,7 @@ class _ViewerScreenState extends State<ViewerScreen> with WindowListener {
windowManager.removeListener(this); windowManager.removeListener(this);
ImageManager.instance.removeListener(_onImageManagerChanged); ImageManager.instance.removeListener(_onImageManagerChanged);
_saveDebounce?.cancel(); _saveDebounce?.cancel();
_zoomPercent.dispose();
super.dispose(); super.dispose();
} }
@@ -79,6 +89,18 @@ class _ViewerScreenState extends State<ViewerScreen> with WindowListener {
void _onImageManagerChanged() { void _onImageManagerChanged() {
setState(() => _hasImages = ImageManager.instance.images.isNotEmpty); setState(() => _hasImages = ImageManager.instance.images.isNotEmpty);
_updateTitle();
}
/// Taskbar/alt-tab başlığı = açılan dosyanın adı (görsel yoksa uygulama adı).
Future<void> _updateTitle() async {
final image = ImageManager.instance.currentImagePath;
final name = image.isEmpty ? 'imajViewer' : p.basename(image);
try {
await windowManager.setTitle(name);
} catch (_) {
// Başlık güncellenemezse sessizce geç.
}
} }
Future<void> _doSave() async { Future<void> _doSave() async {
@@ -105,6 +127,36 @@ class _ViewerScreenState extends State<ViewerScreen> with WindowListener {
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
if (_hasImages) ...[
Text(AppLocalizations.of(context)!.fileProperties,
style: const TextStyle(color: Colors.white70, fontSize: 13)),
const SizedBox(height: 4),
FutureBuilder<List<(String, String)>>(
future: _fileProperties(ImageManager.instance.currentImagePath),
builder: (context, snap) {
if (!snap.hasData) return const SizedBox(height: 20);
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: snap.data!.map((r) => Padding(
padding: const EdgeInsets.only(bottom: 2),
child: Text.rich(
TextSpan(children: [
TextSpan(text: '${r.$1}: ', style: const TextStyle(color: Colors.white70, fontSize: 13)),
TextSpan(text: r.$2, style: const TextStyle(color: Colors.white, fontSize: 13)),
]),
),
)).toList(),
);
},
),
const SizedBox(height: 12),
const Divider(color: Colors.white24, height: 24),
],
WindowSizeSelector(
workArea: _workArea,
onPick: _applyWindowSize,
),
const Divider(color: Colors.white24, height: 24),
Text(AppLocalizations.of(context)!.sortBy, Text(AppLocalizations.of(context)!.sortBy,
style: const TextStyle(color: Colors.white70, fontSize: 13)), style: const TextStyle(color: Colors.white70, fontSize: 13)),
RadioListTile<bool>( RadioListTile<bool>(
@@ -131,8 +183,8 @@ class _ViewerScreenState extends State<ViewerScreen> with WindowListener {
setDialogState(() {}); setDialogState(() {});
}, },
), ),
const Divider(color: Colors.white24, height: 24), Text('imajViewer $appVersion',
const Text('imajViewer', style: TextStyle(color: Colors.white, fontSize: 15)), style: const TextStyle(color: Colors.white, fontSize: 15)),
const SizedBox(height: 4), const SizedBox(height: 4),
Text( Text(
AppLocalizations.of(context)!.appDescription, AppLocalizations.of(context)!.appDescription,
@@ -190,6 +242,59 @@ class _ViewerScreenState extends State<ViewerScreen> with WindowListener {
); );
} }
Future<List<(String, String)>> _fileProperties(String path) async {
final l10n = AppLocalizations.of(context)!;
final file = File(path);
final rows = <(String, String)>[];
final Size? size = pixelSizeOfFile(path);
rows.add((l10n.propResolution, size == null ? '—' : '${size.width.toInt()} × ${size.height.toInt()}'));
try {
rows.add((l10n.propFileSize, _fmtSize(file.statSync().size)));
} catch (_) {
rows.add((l10n.propFileSize, '—'));
}
rows.add((l10n.propFormat, path.toLowerCase().split('.').last.toUpperCase()));
try {
rows.add((l10n.propModified, _fmtDate(file.statSync().modified)));
} catch (_) {
rows.add((l10n.propModified, '—'));
}
rows.add((l10n.propPath, path));
return rows;
}
String _fmtSize(int bytes) {
if (bytes < 1024) return '$bytes B';
const units = ['KB', 'MB', 'GB', 'TB'];
double v = bytes / 1024;
var i = 0;
while (v >= 1024 && i < units.length - 1) {
v /= 1024;
i++;
}
return '${v.toStringAsFixed(v >= 100 ? 0 : 1)} ${units[i]}';
}
String _fmtDate(DateTime t) {
String two(int n) => n.toString().padLeft(2, '0');
return '${t.year}-${two(t.month)}-${two(t.day)} ${two(t.hour)}:${two(t.minute)}';
}
void _flipHorizontal() => _canvasKey.currentState?.toggleFlipH();
Future<Size?> _workArea() async {
final pos = await windowManager.getPosition();
return WindowPersistence.workAreaSize(windowPosition: pos);
}
Future<void> _applyWindowSize(int rows, int cols, Size work) async {
try {
await windowManager.setSize(Size(work.width / cols, work.height / rows));
} catch (_) {
// Boyutlandırma başarısız olursa sessizce geç (buton saf eylem).
}
}
Future<void> _cropSave() async { Future<void> _cropSave() async {
final path = ImageManager.instance.currentImagePath; final path = ImageManager.instance.currentImagePath;
final state = _canvasKey.currentState; final state = _canvasKey.currentState;
@@ -237,6 +342,7 @@ class _ViewerScreenState extends State<ViewerScreen> with WindowListener {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return AppShortcuts( return AppShortcuts(
onCropSave: _hasImages ? _cropSave : null, onCropSave: _hasImages ? _cropSave : null,
onFlipH: _hasImages ? _flipHorizontal : null,
child: Scaffold( child: Scaffold(
backgroundColor: Colors.transparent, backgroundColor: Colors.transparent,
body: WindowResizeZones( body: WindowResizeZones(
@@ -271,6 +377,7 @@ class _ViewerScreenState extends State<ViewerScreen> with WindowListener {
? ImageCanvas( ? ImageCanvas(
key: _canvasKey, key: _canvasKey,
filePath: ImageManager.instance.currentImagePath, filePath: ImageManager.instance.currentImagePath,
zoomPercent: _zoomPercent,
) )
: Center( : Center(
child: Column( child: Column(
@@ -309,6 +416,36 @@ class _ViewerScreenState extends State<ViewerScreen> with WindowListener {
onTap: () => ImageManager.instance.nextImage(), onTap: () => ImageManager.instance.nextImage(),
), ),
), ),
Positioned(
left: 0,
right: 0,
bottom: 12,
child: IgnorePointer(
child: Center(
child: ValueListenableBuilder<double>(
valueListenable: _zoomPercent,
builder: (context, zoom, _) {
final pct = zoom.round();
return Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
decoration: BoxDecoration(
color: Colors.black.withValues(alpha: 0.35),
borderRadius: BorderRadius.circular(13),
),
child: Text(
pct < 1 ? '<1%' : '$pct%',
style: const TextStyle(
color: Colors.white,
fontSize: 12,
fontWeight: FontWeight.w500,
),
),
);
},
),
),
),
),
], ],
if (_isDragging) if (_isDragging)
Positioned.fill( Positioned.fill(
+5 -1
View File
@@ -17,6 +17,7 @@ class CropViewParams {
final double vw, vh; // viewport boyutu final double vw, vh; // viewport boyutu
final double tx, ty, sc; // pan/zoom final double tx, ty, sc; // pan/zoom
final double angleDeg; // donus (derece) final double angleDeg; // donus (derece)
final bool flipH; // yatay aynalama (goruntunun kendisi degil, gorunum)
final double cx, cy; // donus merkezi (viewport ortasi) final double cx, cy; // donus merkezi (viewport ortasi)
final double k; // kaynak -> layout olcegi final double k; // kaynak -> layout olcegi
final double ox, oy; // layout offset final double ox, oy; // layout offset
@@ -29,6 +30,7 @@ class CropViewParams {
required this.ty, required this.ty,
required this.sc, required this.sc,
required this.angleDeg, required this.angleDeg,
this.flipH = false,
required this.cx, required this.cx,
required this.cy, required this.cy,
required this.k, required this.k,
@@ -65,7 +67,7 @@ class CropSaveService {
required img.Image source, required img.Image source,
required CropViewParams vp, required CropViewParams vp,
List<double>? matrix, List<double>? matrix,
int jpegQuality = 93, int jpegQuality = 95,
}) async { }) async {
final outPath = buildOutputPath(srcPath); final outPath = buildOutputPath(srcPath);
if (outPath == null) throw const CropSaveError('file_exists'); if (outPath == null) throw const CropSaveError('file_exists');
@@ -73,6 +75,8 @@ class CropSaveService {
final out = vp.angleDeg == 0 final out = vp.angleDeg == 0
? _cropAxisAligned(source, vp) ? _cropAxisAligned(source, vp)
: _cropRotated(source, vp); : _cropRotated(source, vp);
// Ekranda yatay aynalama acikken cikti da aynalanir (cikti = viewport'un birebir eslemesi).
if (vp.flipH) img.flipHorizontal(out);
// Çıktıda yön fiziksel — EXIF tag'ini temizle (çift dönüşü önler). // Çıktıda yön fiziksel — EXIF tag'ini temizle (çift dönüşü önler).
out.exif.imageIfd.orientation = null; out.exif.imageIfd.orientation = null;
+80
View File
@@ -0,0 +1,80 @@
import 'dart:io';
import 'dart:typed_data';
import 'package:flutter/widgets.dart' show Size;
import 'package:image/image.dart' as img;
/// Görüntü dosyasının gerçek piksel boyutu — tam decode yapmadan.
///
/// PNG/GIF/BMP/WebP: boyut ilk 33 baytta → elle parse.
/// JPEG: SOF büyük APP/EXIF segmentlerinin ardından gelebilir → package:image startDecode.
Size? pixelSizeOfFile(String path) {
try {
final file = File(path);
final stat = file.statSync();
const maxHead = 512 * 1024;
final raf = file.openSync();
final header = raf.readSync(stat.size < maxHead ? stat.size : maxHead);
raf.closeSync();
return _headerPixelSize(header);
} catch (_) {}
return null;
}
Size? _headerPixelSize(Uint8List b) {
try {
if (b.length >= 24 && b[0] == 0x89 && b[1] == 0x50) {
// PNG
final w = (b[16] << 24) | (b[17] << 16) | (b[18] << 8) | b[19];
final h = (b[20] << 24) | (b[21] << 16) | (b[22] << 8) | b[23];
return Size(w.toDouble(), h.toDouble());
}
if (b.length >= 10 && b[0] == 0x47 && b[1] == 0x49 && b[2] == 0x46) {
// GIF (WBMP da GIF tabanlı olabilir)
final w = b[6] | (b[7] << 8);
final h = b[8] | (b[9] << 8);
return Size(w.toDouble(), h.toDouble());
}
if (b.length >= 26 && b[0] == 0x42 && b[1] == 0x4D) {
// BMP
final w = b[18] | (b[19] << 8) | (b[20] << 16) | (b[21] << 24);
final h = b[22] | (b[23] << 8) | (b[24] << 16) | (b[25] << 24);
return Size(w.toDouble(), h.abs().toDouble());
}
if (b.length >= 30 &&
b[0] == 0x52 &&
b[1] == 0x49 &&
b[2] == 0x46 &&
b[3] == 0x46 &&
b[8] == 0x57 &&
b[9] == 0x45 &&
b[10] == 0x42 &&
b[11] == 0x50) {
// WebP
if (b[12] == 0x56 && b[13] == 0x50 && b[15] == 0x58) {
// VP8X
final w = b[24] | (b[25] << 8) | (b[26] << 16);
final h = b[27] | (b[28] << 8) | (b[29] << 16);
return Size((w + 1).toDouble(), (h + 1).toDouble());
}
if (b.length >= 25 && b[12] == 0x56 && b[13] == 0x50 && b[15] == 0x4C && b[20] == 0x2F) {
// VP8L
final w = (b[21] & 0x3F) | ((b[22] & 0x0F) << 6);
final h = (b[23] & 0x3F) | ((b[24] & 0x0F) << 6);
return Size((w + 1).toDouble(), (h + 1).toDouble());
}
if (b[12] == 0x56 && b[13] == 0x50 && b[15] == 0x20) {
// VP8 (lossy)
final w = (b[26] | (b[27] << 8)) & 0x3FFF;
final h = (b[28] | (b[29] << 8)) & 0x3FFF;
return Size(w.toDouble(), h.toDouble());
}
}
} catch (_) {}
try {
// JPEG
final info = img.JpegDecoder().startDecode(b);
if (info != null) return Size(info.width.toDouble(), info.height.toDouble());
} catch (_) {}
return null;
}
+30
View File
@@ -84,6 +84,36 @@ class WindowPersistence {
} }
} }
/// Work area (taskbar excluded) of the display containing the window,
/// in logical px. Returns null if screen info is unavailable.
static Future<Size?> workAreaSize({
required Offset windowPosition,
}) async {
try {
final displays = await screenRetriever.getAllDisplays();
// Pencerenin bulunduğu monitörü bul (çoklu monitör desteği)
Display display = await screenRetriever.getPrimaryDisplay();
for (final d in displays) {
final pos = d.visiblePosition;
final size = d.visibleSize ?? d.size;
if (pos != null &&
windowPosition.dx >= pos.dx &&
windowPosition.dx < pos.dx + size.width &&
windowPosition.dy >= pos.dy &&
windowPosition.dy < pos.dy + size.height) {
display = d;
break;
}
}
return display.visibleSize ?? display.size;
} catch (_) {
// Fallback: screen info unavailable → caller silently no-ops.
return null;
}
}
/// Cascade: place new window to the right of the previous one. /// Cascade: place new window to the right of the previous one.
/// Falls to the next row (row start) when it would overflow the screen, /// Falls to the next row (row start) when it would overflow the screen,
/// and wraps to the top-left if the next row also overflows. /// and wraps to the top-left if the next row also overflows.
+20 -2
View File
@@ -8,8 +8,14 @@ import '../services/file_handler.dart';
class AppShortcuts extends StatelessWidget { class AppShortcuts extends StatelessWidget {
final Widget child; final Widget child;
final VoidCallback? onCropSave; final VoidCallback? onCropSave;
final VoidCallback? onFlipH;
const AppShortcuts({super.key, required this.child, this.onCropSave}); const AppShortcuts({
super.key,
required this.child,
this.onCropSave,
this.onFlipH,
});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@@ -44,6 +50,17 @@ class AppShortcuts extends StatelessWidget {
} }
} }
// Ctrl+R: yatay aynala / geri al (fiziksel R: bazi klavye haritalarinda
// mantiksal tus T gibi farkli cozuluyor)
if (ctrl &&
(event.logicalKey == LogicalKeyboardKey.keyR ||
event.physicalKey == PhysicalKeyboardKey.keyR)) {
if (onFlipH != null) {
onFlipH!();
return KeyEventResult.handled;
}
}
switch (event.logicalKey) { switch (event.logicalKey) {
case LogicalKeyboardKey.arrowLeft: case LogicalKeyboardKey.arrowLeft:
ImageManager.instance.previousImage(); ImageManager.instance.previousImage();
@@ -51,9 +68,10 @@ class AppShortcuts extends StatelessWidget {
case LogicalKeyboardKey.arrowRight: case LogicalKeyboardKey.arrowRight:
ImageManager.instance.nextImage(); ImageManager.instance.nextImage();
return KeyEventResult.handled; return KeyEventResult.handled;
case LogicalKeyboardKey.f11:
case LogicalKeyboardKey.escape: case LogicalKeyboardKey.escape:
windowManager.isFullScreen().then((isFullscreen) { windowManager.isFullScreen().then((isFullscreen) {
if (isFullscreen) windowManager.setFullScreen(false); windowManager.setFullScreen(event.logicalKey == LogicalKeyboardKey.f11 ? !isFullscreen : false);
}); });
return KeyEventResult.handled; return KeyEventResult.handled;
} }
+102 -20
View File
@@ -6,11 +6,15 @@ import 'package:flutter/scheduler.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import '../l10n/app_localizations.dart'; import '../l10n/app_localizations.dart';
import '../services/crop_save_service.dart'; import '../services/crop_save_service.dart';
import '../services/image_header.dart';
import '../services/window_persistence.dart'; import '../services/window_persistence.dart';
class ImageCanvas extends StatefulWidget { class ImageCanvas extends StatefulWidget {
final String filePath; final String filePath;
const ImageCanvas({super.key, required this.filePath});
/// Görüntünün gerçek piksel boyutuna göre ölçek yüzdesi (100% = 1:1).
final ValueNotifier<double>? zoomPercent;
const ImageCanvas({super.key, required this.filePath, this.zoomPercent});
@override @override
State<ImageCanvas> createState() => ImageCanvasState(); State<ImageCanvas> createState() => ImageCanvasState();
} }
@@ -26,6 +30,7 @@ class ImageCanvasState extends State<ImageCanvas> {
double _contrast = 1.0; double _contrast = 1.0;
double _saturation = 1.0; double _saturation = 1.0;
double _brightness = 0; double _brightness = 0;
bool _flipH = false;
// ── Gesture tracking ── // ── Gesture tracking ──
String _interactionMode = 'none'; // 'contrast' | 'rotate' | 'saturation' | 'brightness' String _interactionMode = 'none'; // 'contrast' | 'rotate' | 'saturation' | 'brightness'
@@ -36,6 +41,11 @@ class ImageCanvasState extends State<ImageCanvas> {
double _startBrightness = 0; double _startBrightness = 0;
DateTime? _lastRightTapTime; DateTime? _lastRightTapTime;
Offset _lastRightTapPos = Offset.zero; Offset _lastRightTapPos = Offset.zero;
/// Sağ tuş basılıyken anlamlı hareket oldu mu? (tıklama mı sürükleme mi)
bool _rightDragMoved = false;
int? _rightPointerId;
/// Sürükleme sayılma eşiği (px) — rotate/contrast ince ayarında el titremesi.
static const double _dragClickSlop = 4;
// ── Klavye modifier tracking ── // ── Klavye modifier tracking ──
bool _ctrlPressed = false; bool _ctrlPressed = false;
@@ -46,6 +56,44 @@ class ImageCanvasState extends State<ImageCanvas> {
Size _imageSize = Size.zero; Size _imageSize = Size.zero;
static const int _downsampleTarget = 2048; static const int _downsampleTarget = 2048;
// ── Gerçek piksel boyutu + zoom yüzdesi ──
Size? _origSize;
bool _zoomSyncScheduled = false;
/// Yüzdeyi bir sonraki kare sonunda yayınla (build sırasında setState tetiklememek için).
void _syncZoom() {
if (_zoomSyncScheduled) return;
_zoomSyncScheduled = true;
WidgetsBinding.instance.addPostFrameCallback((_) {
_zoomSyncScheduled = false;
if (!mounted) return;
final n = widget.zoomPercent;
if (n == null) return;
final v = _zoomPercentValue();
if (n.value != v) n.value = v;
});
}
/// Ekrandaki ölçeğin görüntünün gerçek piksel boyutuna oranı (100% = 1:1).
double _zoomPercentValue() {
final o = _origSize;
if (o == null || o.isEmpty || _vpSize.isEmpty) return _sc * 100;
final double ratio;
if (_isFilled) {
// cover: kutu viewport, boya ölçeği max oran
ratio = math.max(_vpSize.width / o.width, _vpSize.height / o.height);
} else {
// contain: yerleşmiş görüntü kutusu (downsample sınırı dahil) → gerçek ölçek
final box = _imageKey.currentContext?.findRenderObject() as RenderBox?;
if (box != null && box.hasSize && box.size.width > 0) {
ratio = box.size.width / o.width;
} else {
ratio = math.min(_vpSize.width / o.width, _vpSize.height / o.height);
}
}
return _sc * ratio * 100;
}
// ── Kare hızına eşitlenmiş render tick ── // ── Kare hızına eşitlenmiş render tick ──
final ValueNotifier<int> _renderTick = ValueNotifier(0); final ValueNotifier<int> _renderTick = ValueNotifier(0);
bool _frameScheduled = false; bool _frameScheduled = false;
@@ -127,8 +175,10 @@ class ImageCanvasState extends State<ImageCanvas> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_origSize = pixelSizeOfFile(widget.filePath);
HardwareKeyboard.instance.addHandler(_onHardwareKey); HardwareKeyboard.instance.addHandler(_onHardwareKey);
_startWarmup(); _startWarmup();
_syncZoom();
} }
/// İlk kullanım shader derlemesini açılışa çek: gizli katmanda sırayla /// İlk kullanım shader derlemesini açılışa çek: gizli katmanda sırayla
@@ -188,6 +238,7 @@ class ImageCanvasState extends State<ImageCanvas> {
void didUpdateWidget(ImageCanvas old) { void didUpdateWidget(ImageCanvas old) {
super.didUpdateWidget(old); super.didUpdateWidget(old);
if (old.filePath != widget.filePath) { if (old.filePath != widget.filePath) {
_origSize = pixelSizeOfFile(widget.filePath);
_cachedFileImage?.evict(); _cachedFileImage?.evict();
_cachedProvider?.evict(); _cachedProvider?.evict();
_cachedFileImage = null; _cachedFileImage = null;
@@ -202,16 +253,28 @@ class ImageCanvasState extends State<ImageCanvas> {
void _resetView() { void _resetView() {
_tx = 0; _ty = 0; _sc = 1.0; _isFilled = WindowPersistence.filled; _tx = 0; _ty = 0; _sc = 1.0; _isFilled = WindowPersistence.filled;
_angle = 0; _contrast = 1.0; _saturation = 1.0; _brightness = 0; _angle = 0; _contrast = 1.0; _saturation = 1.0; _brightness = 0;
_flipH = false;
_usingFullRes = false; _usingFullRes = false;
_imageSize = Size.zero; _imageSize = Size.zero;
_markRenderDirty(); _markRenderDirty();
_syncZoom();
} }
void _toggleFillFit() { void _toggleFillFit() {
_isFilled = !_isFilled; _tx = 0; _ty = 0; _sc = 1.0; _isFilled = !_isFilled; _tx = 0; _ty = 0; _sc = 1.0;
_angle = 0; _contrast = 1.0; _saturation = 1.0; _brightness = 0; _angle = 0; _contrast = 1.0; _saturation = 1.0; _brightness = 0;
_flipH = false;
WindowPersistence.saveFilled(_isFilled); WindowPersistence.saveFilled(_isFilled);
_markRenderDirty(); _markRenderDirty();
_syncZoom();
}
/// Ctrl+R: sağ çift tık gibi efektleri sıfırlar ve yatay aynalar; tekrar basınca geri alır.
void toggleFlipH() {
_flipH = !_flipH;
_angle = 0; _contrast = 1.0; _saturation = 1.0; _brightness = 0;
_interactionMode = 'none';
_markRenderDirty();
} }
void _handleDoubleTap() => _toggleFillFit(); void _handleDoubleTap() => _toggleFillFit();
@@ -222,7 +285,7 @@ class ImageCanvasState extends State<ImageCanvas> {
final double vy = _angle == 0 ? e.localPosition.dy : _vpSize.height / 2; final double vy = _angle == 0 ? e.localPosition.dy : _vpSize.height / 2;
final double cx = (vx - _tx) / _sc; final double cx = (vx - _tx) / _sc;
final double cy = (vy - _ty) / _sc; final double cy = (vy - _ty) / _sc;
const double factor = 1.1; final double factor = _ctrlPressed ? 1.01 : 1.1;
final double rawSc = _sc * (e.scrollDelta.dy < 0 ? factor : 1.0 / factor); final double rawSc = _sc * (e.scrollDelta.dy < 0 ? factor : 1.0 / factor);
final double newSc = rawSc.clamp(1.0, 20.0); final double newSc = rawSc.clamp(1.0, 20.0);
if (_angle == 0) { if (_angle == 0) {
@@ -239,6 +302,7 @@ class ImageCanvasState extends State<ImageCanvas> {
_checkFullResSwap(); _checkFullResSwap();
_clamp(); _clamp();
_markRenderDirty(); _markRenderDirty();
_syncZoom();
} }
void _onPanUpdate(DragUpdateDetails d) { void _onPanUpdate(DragUpdateDetails d) {
@@ -252,22 +316,8 @@ class ImageCanvasState extends State<ImageCanvas> {
if (e.buttons != kSecondaryMouseButton) return; if (e.buttons != kSecondaryMouseButton) return;
_pointerDownPos = e.localPosition; _pointerDownPos = e.localPosition;
_rightDragMoved = false;
final now = DateTime.now(); _rightPointerId = e.pointer;
if (_lastRightTapTime != null &&
now.difference(_lastRightTapTime!) < const Duration(milliseconds: 400) &&
(e.localPosition - _lastRightTapPos).distance < 20) {
_angle = 0;
_contrast = 1.0;
_saturation = 1.0;
_brightness = 0;
_interactionMode = 'none';
_markRenderDirty();
_lastRightTapTime = null;
return;
}
_lastRightTapTime = now;
_lastRightTapPos = e.localPosition;
if (_ctrlPressed && _shiftPressed) { if (_ctrlPressed && _shiftPressed) {
_interactionMode = 'brightness'; _interactionMode = 'brightness';
@@ -286,6 +336,10 @@ class ImageCanvasState extends State<ImageCanvas> {
void _onPointerMove(PointerMoveEvent e) { void _onPointerMove(PointerMoveEvent e) {
if (e.buttons != kSecondaryMouseButton) return; if (e.buttons != kSecondaryMouseButton) return;
if (!_rightDragMoved &&
(e.localPosition - _pointerDownPos).distance > _dragClickSlop) {
_rightDragMoved = true;
}
final dy = e.localPosition.dy - _pointerDownPos.dy; final dy = e.localPosition.dy - _pointerDownPos.dy;
@@ -307,7 +361,30 @@ class ImageCanvasState extends State<ImageCanvas> {
} }
void _onPointerUp(PointerUpEvent e) { void _onPointerUp(PointerUpEvent e) {
if (e.pointer != _rightPointerId) return;
_rightPointerId = null;
_interactionMode = 'none'; _interactionMode = 'none';
// Sürükleme ile biten etkileşim sağ çift tık zincirini keser; yalnızca
// gerçek tıklamalar (hareketsiz bas-bırak) çift tık sayılır.
if (_rightDragMoved) {
_lastRightTapTime = null;
_rightDragMoved = false;
return;
}
final now = DateTime.now();
final isDouble = _lastRightTapTime != null &&
now.difference(_lastRightTapTime!) < const Duration(milliseconds: 400) &&
(e.localPosition - _lastRightTapPos).distance < 20;
_lastRightTapTime = isDouble ? null : now;
_lastRightTapPos = e.localPosition;
if (isDouble) {
_angle = 0;
_contrast = 1.0;
_saturation = 1.0;
_brightness = 0;
_flipH = false;
_markRenderDirty();
}
} }
// ── Combined color filter (brightness × saturation × contrast) ── // ── Combined color filter (brightness × saturation × contrast) ──
@@ -416,6 +493,7 @@ class ImageCanvasState extends State<ImageCanvas> {
ty: _ty, ty: _ty,
sc: _sc, sc: _sc,
angleDeg: _angle, angleDeg: _angle,
flipH: _flipH,
cx: _vpSize.width / 2, cx: _vpSize.width / 2,
cy: _vpSize.height / 2, cy: _vpSize.height / 2,
k: k, k: k,
@@ -489,7 +567,10 @@ class ImageCanvasState extends State<ImageCanvas> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return LayoutBuilder( return LayoutBuilder(
builder: (context, constraints) { builder: (context, constraints) {
_vpSize = constraints.biggest; if (constraints.biggest != _vpSize) {
_vpSize = constraints.biggest;
_syncZoom();
}
_clamp(); _clamp();
return RepaintBoundary( return RepaintBoundary(
child: ValueListenableBuilder<int>( child: ValueListenableBuilder<int>(
@@ -518,12 +599,13 @@ class ImageCanvasState extends State<ImageCanvas> {
}, },
); );
if (_angle != 0) { if (_angle != 0 || _flipH) {
imageContent = Transform( imageContent = Transform(
alignment: Alignment.topLeft, alignment: Alignment.topLeft,
transform: Matrix4.identity() transform: Matrix4.identity()
..translate(vpCenterX, vpCenterY) ..translate(vpCenterX, vpCenterY)
..rotateZ(angleRad) ..rotateZ(angleRad)
..scale(_flipH ? -1.0 : 1.0, 1.0)
..translate(-vpCenterX, -vpCenterY), ..translate(-vpCenterX, -vpCenterY),
child: imageContent, child: imageContent,
); );
+77
View File
@@ -0,0 +1,77 @@
import 'package:flutter/material.dart';
import '../l10n/app_localizations.dart';
/// Ekran oranı buton ızgarası: 31 buton `a:b` (a dikeyi, b yatayı böler;
/// hedef boyut `W/b × H/a`). Sol-üst hücre (1:1) boş placeholder — 1:1 yok
/// (title bar çift tık zaten tam ekran).
///
/// Yerleşim: sabit 8 satır × 4 sütun (31-10: geniş/dar transpozesi
/// kaldırıldı — kullanıcı kararı).
class WindowSizeSelector extends StatelessWidget {
final Future<Size?> Function() workArea;
final void Function(int rows, int cols, Size work) onPick;
const WindowSizeSelector({
super.key,
required this.workArea,
required this.onPick,
});
@override
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(AppLocalizations.of(context)!.sizeSection,
style: const TextStyle(color: Colors.white70, fontSize: 13)),
const SizedBox(height: 4),
SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [for (var b = 1; b <= 8; b++) _row(b)],
),
),
],
);
}
/// Satır = `b` (1..8, yatay bölün); satır içinde `a` (1..4, dikey bölün).
Widget _row(int b) {
return Row(
mainAxisSize: MainAxisSize.min,
children: [
for (var a = 1; a <= 4; a++) _cell(a, b),
],
);
}
Widget _cell(int a, int b) {
return Padding(
padding: const EdgeInsets.only(right: 4, bottom: 4),
child: a == 1 && b == 1
? const SizedBox(width: 46, height: 26)
: GestureDetector(
onTap: () async {
final work = await workArea();
if (work != null) onPick(a, b, work);
},
child: Container(
width: 46,
height: 26,
decoration: BoxDecoration(
color: Colors.white10,
borderRadius: BorderRadius.circular(4),
),
alignment: Alignment.center,
child: Text(
'$a:$b',
style: const TextStyle(color: Colors.white, fontSize: 11),
),
),
),
);
}
}
+10 -10
View File
@@ -209,10 +209,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: intl name: intl
sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5" sha256: "1ca20c894b1717686a2319b8548763d812bc0aabdac580420a44c5178c57a867"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.20.2" version: "0.20.3"
json_annotation: json_annotation:
dependency: transitive dependency: transitive
description: description:
@@ -257,10 +257,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: matcher name: matcher
sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861 sha256: "31bd099b47c10cd1aeb55146a2d46ce0277630ecef3f7dae54ad7873f36696cd"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.12.19" version: "0.12.20"
material_color_utilities: material_color_utilities:
dependency: transitive dependency: transitive
description: description:
@@ -273,10 +273,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: meta name: meta
sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349" sha256: "307249ce4ff29d58a18e97f6345f539382eb9c9c29ecda628900f31de0443dd9"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.18.0" version: "1.19.0"
msix: msix:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -462,10 +462,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: test_api name: test_api
sha256: "949a932224383300f01be9221c39180316445ecb8e7547f70a41a35bf421fb9e" sha256: "2a122cbe059f8b610d3a5415f42e255b6c17b1f21eee1d960f31080237fb4f11"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.7.11" version: "0.7.12"
typed_data: typed_data:
dependency: transitive dependency: transitive
description: description:
@@ -486,10 +486,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: vector_math name: vector_math
sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b sha256: "92b9910f66ed1057fd4da7b040ae7c74cafacf885bdc81be496928d5049b032d"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.2.0" version: "2.4.3"
vm_service: vm_service:
dependency: transitive dependency: transitive
description: description:
+1 -1
View File
@@ -1,7 +1,7 @@
name: imajviewer name: imajviewer
description: "Ultra-lightweight image viewer for Linux and Windows." description: "Ultra-lightweight image viewer for Linux and Windows."
publish_to: 'none' publish_to: 'none'
version: 1.5.0+11 version: 1.9.0+20
msix_config: msix_config:
display_name: ImajViewer display_name: ImajViewer
+93
View File
@@ -0,0 +1,93 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:image/image.dart' as img;
import 'package:imajviewer/shortcuts/app_shortcuts.dart';
import 'package:imajviewer/widgets/image_canvas.dart';
/// Aynalama uygulandiginda render zincirinde negatif x olcekli bir Transform bulunur.
bool _mirrored(WidgetTester tester) => tester
.widgetList<Transform>(find.byType(Transform))
.any((t) => t.transform.entry(0, 0) < 0);
Future<void> _pressCtrlR(WidgetTester tester) async {
await tester.sendKeyDownEvent(LogicalKeyboardKey.controlLeft);
await tester.sendKeyDownEvent(LogicalKeyboardKey.keyR);
await tester.pump();
await tester.sendKeyUpEvent(LogicalKeyboardKey.keyR);
await tester.sendKeyUpEvent(LogicalKeyboardKey.controlLeft);
await tester.pump();
await tester.pump();
}
Future<void> _pressCtrlRWrongLayout(WidgetTester tester) async {
await tester.sendKeyDownEvent(LogicalKeyboardKey.controlLeft);
await tester.sendKeyDownEvent(LogicalKeyboardKey.keyT,
physicalKey: PhysicalKeyboardKey.keyR);
await tester.pump();
await tester.sendKeyUpEvent(LogicalKeyboardKey.keyT,
physicalKey: PhysicalKeyboardKey.keyR);
await tester.sendKeyUpEvent(LogicalKeyboardKey.controlLeft);
await tester.pump();
await tester.pump();
}
Widget _app(GlobalKey<ImageCanvasState> key, String path, double w, double h) {
return MaterialApp(
home: AppShortcuts(
onFlipH: () => key.currentState?.toggleFlipH(),
child: SizedBox(width: w, height: h, child: ImageCanvas(key: key, filePath: path)),
),
);
}
void main() {
late Directory tmp;
late String pngPath;
setUpAll(() {
tmp = Directory.systemTemp.createTempSync('flip_test');
pngPath = '${tmp.path}/a.png';
File(pngPath).writeAsBytesSync(img.encodePng(img.Image(width: 4, height: 2))!);
});
tearDownAll(() => tmp.deleteSync(recursive: true));
testWidgets('Ctrl+R yatay aynalamayi acar, tekrar basinca kapatir', (tester) async {
final key = GlobalKey<ImageCanvasState>();
await tester.pumpWidget(_app(key, pngPath, 400, 300));
await tester.pump();
expect(_mirrored(tester), isFalse, reason: 'baslangicta aynalama olmamali');
await _pressCtrlR(tester);
expect(_mirrored(tester), isTrue, reason: 'Ctrl+R sonrasi aynalama acik olmali');
await _pressCtrlR(tester);
expect(_mirrored(tester), isFalse, reason: 'ikinci Ctrl+R sonrasi eski haline donmeli');
});
testWidgets('Ctrl+R crop-save parametrelerine aynalamayi tasir', (tester) async {
final key = GlobalKey<ImageCanvasState>();
await tester.pumpWidget(_app(key, pngPath, 100, 50));
await tester.pump();
await _pressCtrlR(tester);
final vp = key.currentState!.cropViewParams(4, 2);
expect(vp?.flipH, isTrue, reason: 'crop-save aynalamayi almali');
});
testWidgets('fiziksel R tusu mantiksal T olarak gelse de Ctrl+R calisir', (tester) async {
final key = GlobalKey<ImageCanvasState>();
await tester.pumpWidget(_app(key, pngPath, 400, 300));
await tester.pump();
await _pressCtrlRWrongLayout(tester);
expect(_mirrored(tester), isTrue,
reason: 'klavye haritasi R tusunu T olarak cozse bile aynalama acilmali');
await _pressCtrlRWrongLayout(tester);
expect(_mirrored(tester), isFalse, reason: 'tekrar basinca geri donmeli');
});
}
+32
View File
@@ -67,6 +67,38 @@ Future<void> main() async {
print('3) dims ${r3.width}x${r3.height} (25x25 beklenir: zoom x2 + fill k=2 -> 4 src px/ekran px)'); print('3) dims ${r3.width}x${r3.height} (25x25 beklenir: zoom x2 + fill k=2 -> 4 src px/ekran px)');
if (r3.width != 25 || r3.height != 25) fails++; if (r3.width != 25 || r3.height != 25) fails++;
// 4) Yatay aynalama (flipH): gorunum aynalanir, cikti da aynali olmali
final out4 = '${tmp.path}/d';
Directory(out4).createSync();
final f4 = '$out4/src.jpg';
File(f4).writeAsBytesSync(File(srcPath).readAsBytesSync());
final p4 = CropViewParams(w: 100, h: 50, vw: 100, vh: 50, tx: 0, ty: 0, sc: 1,
angleDeg: 0, flipH: true, cx: 50, cy: 25, k: 1, ox: 0, oy: 0);
final path4 = await CropSaveService.saveCrop(srcPath: f4, source: await CropSaveService.load(f4), vp: p4);
final r4 = img.decodeImage(File(path4).readAsBytesSync())!;
print('4) dims ${r4.width}x${r4.height} (100x50 beklenir)');
if (r4.width != 100 || r4.height != 50) fails++;
checkPix('4) sol-ust (orij sag-ust YESIL)', imgPx(r4, 2, 2), 0, 255, 0);
checkPix('4) sag-ust (orij sol-ust KIRMIZI)', imgPx(r4, 97, 2), 255, 0, 0);
checkPix('4) sol-alt (orij sag-alt SARI)', imgPx(r4, 2, 47), 255, 255, 0);
checkPix('4) sag-alt (orij sol-alt MAVI)', imgPx(r4, 97, 47), 0, 0, 255);
// 5) Donus + aynalama: 2. senaryonun aynasi
final out5 = '${tmp.path}/e';
Directory(out5).createSync();
final f5 = '$out5/src.jpg';
File(f5).writeAsBytesSync(File(srcPath).readAsBytesSync());
final p5 = CropViewParams(w: 100, h: 50, vw: 50, vh: 100, tx: 0, ty: 0, sc: 1,
angleDeg: 90, flipH: true, cx: 25, cy: 50, k: 1, ox: -25, oy: 25);
final path5 = await CropSaveService.saveCrop(srcPath: f5, source: await CropSaveService.load(f5), vp: p5);
final r5 = img.decodeImage(File(path5).readAsBytesSync())!;
print('5) dims ${r5.width}x${r5.height} (50x100 beklenir)');
if (r5.width != 50 || r5.height != 100) fails++;
checkPix('5) sol-ust (orij sol-ust KIRMIZI)', imgPx(r5, 2, 2), 255, 0, 0);
checkPix('5) sag-ust (orij sol-alt MAVI)', imgPx(r5, 47, 2), 0, 0, 255);
checkPix('5) sol-alt (orij sag-ust YESIL)', imgPx(r5, 2, 97), 0, 255, 0);
checkPix('5) sag-alt (orij sag-alt SARI)', imgPx(r5, 47, 97), 255, 255, 0);
tmp.deleteSync(recursive: true); tmp.deleteSync(recursive: true);
print(fails == 0 ? 'ALL PASS' : '$fails FAILURES'); print(fails == 0 ? 'ALL PASS' : '$fails FAILURES');
exitCode = fails == 0 ? 0 : 1; exitCode = fails == 0 ? 0 : 1;