feat: klasor tabanli galeri, brightness efekti (Ctrl+Shift+sag tik), .deb guncellendi
This commit is contained in:
@@ -86,7 +86,6 @@ class _ViewerScreenState extends State<ViewerScreen> with WindowListener {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
ImageManager.instance.buildContext = context;
|
||||
return AppShortcuts(
|
||||
child: Scaffold(
|
||||
backgroundColor: Colors.transparent,
|
||||
|
||||
@@ -15,14 +15,14 @@ class FileHandler {
|
||||
.where((f) => f.path != null)
|
||||
.map((f) => f.path!)
|
||||
.toList();
|
||||
ImageManager.instance.addImages(paths);
|
||||
ImageManager.instance.setGalleryFromPaths(paths);
|
||||
}
|
||||
}
|
||||
|
||||
/// Load images from given file paths
|
||||
static void loadImagesFromPaths(List<String> paths) {
|
||||
if (paths.isNotEmpty) {
|
||||
ImageManager.instance.addImages(paths);
|
||||
ImageManager.instance.setGalleryFromPaths(paths);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,10 @@ class ImageManager extends ChangeNotifier {
|
||||
static ImageManager get instance => _instance;
|
||||
ImageManager._();
|
||||
|
||||
static const _supportedExtensions = [
|
||||
'png', 'jpg', 'jpeg', 'jfif', 'jpe', 'webp', 'bmp', 'gif', 'wbmp',
|
||||
];
|
||||
|
||||
final List<String> _images = [];
|
||||
int _currentIndex = 0;
|
||||
|
||||
@@ -18,22 +22,47 @@ class ImageManager extends ChangeNotifier {
|
||||
return _images[_currentIndex];
|
||||
}
|
||||
|
||||
void addImages(List<String> paths) {
|
||||
final validPaths = paths.where((p) {
|
||||
/// Açılan resmin bulunduğu klasörü galeri olarak kurar (en yeni önce).
|
||||
void setGalleryFromPaths(List<String> paths) {
|
||||
String? anchor;
|
||||
for (final p in paths) {
|
||||
final ext = p.toLowerCase().split('.').last;
|
||||
const supported = ['png', 'jpg', 'jpeg', 'jfif', 'jpe', 'webp', 'bmp', 'gif', 'wbmp'];
|
||||
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!);
|
||||
if (_supportedExtensions.contains(ext) && File(p).existsSync()) {
|
||||
anchor = p;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (anchor == null) return;
|
||||
|
||||
final dir = File(anchor).parent;
|
||||
if (!dir.existsSync()) return;
|
||||
|
||||
final List<(String, DateTime)> entries = [];
|
||||
for (final entity in dir.listSync()) {
|
||||
if (entity is! File) continue;
|
||||
final p = entity.path;
|
||||
final ext = p.toLowerCase().split('.').last;
|
||||
if (!_supportedExtensions.contains(ext)) continue;
|
||||
try {
|
||||
entries.add((p, entity.lastModifiedSync()));
|
||||
} catch (_) {}
|
||||
}
|
||||
entries.sort((a, b) {
|
||||
final byDate = b.$2.compareTo(a.$2);
|
||||
return byDate != 0 ? byDate : a.$1.compareTo(b.$1);
|
||||
});
|
||||
|
||||
_images
|
||||
..clear()
|
||||
..addAll(entries.map((e) => e.$1));
|
||||
|
||||
_currentIndex = _images.indexOf(anchor);
|
||||
if (_currentIndex < 0) {
|
||||
final base = anchor.split(Platform.pathSeparator).last;
|
||||
_currentIndex = _images
|
||||
.indexWhere((p) => p.split(Platform.pathSeparator).last == base);
|
||||
}
|
||||
if (_currentIndex < 0) _currentIndex = 0;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
@@ -55,19 +84,12 @@ class ImageManager extends ChangeNotifier {
|
||||
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;
|
||||
|
||||
@@ -21,13 +21,15 @@ class _ImageCanvasState extends State<ImageCanvas> {
|
||||
double _angle = 0;
|
||||
double _contrast = 1.0;
|
||||
double _saturation = 1.0;
|
||||
double _brightness = 0;
|
||||
|
||||
// ── Gesture tracking ──
|
||||
String _interactionMode = 'none'; // 'contrast' | 'rotate' | 'saturation'
|
||||
String _interactionMode = 'none'; // 'contrast' | 'rotate' | 'saturation' | 'brightness'
|
||||
Offset _pointerDownPos = Offset.zero;
|
||||
double _startAngle = 0;
|
||||
double _startContrast = 1.0;
|
||||
double _startSaturation = 1.0;
|
||||
double _startBrightness = 0;
|
||||
DateTime? _lastRightTapTime;
|
||||
Offset _lastRightTapPos = Offset.zero;
|
||||
|
||||
@@ -139,14 +141,14 @@ class _ImageCanvasState extends State<ImageCanvas> {
|
||||
void _resetView() {
|
||||
setState(() {
|
||||
_tx = 0; _ty = 0; _sc = 1.0; _isFilled = true; _vpSize = Size.zero;
|
||||
_angle = 0; _contrast = 1.0; _saturation = 1.0;
|
||||
_angle = 0; _contrast = 1.0; _saturation = 1.0; _brightness = 0;
|
||||
});
|
||||
}
|
||||
|
||||
void _toggleFillFit() {
|
||||
setState(() {
|
||||
_isFilled = !_isFilled; _tx = 0; _ty = 0; _sc = 1.0; _vpSize = Size.zero;
|
||||
_angle = 0; _contrast = 1.0; _saturation = 1.0;
|
||||
_angle = 0; _contrast = 1.0; _saturation = 1.0; _brightness = 0;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -196,6 +198,7 @@ class _ImageCanvasState extends State<ImageCanvas> {
|
||||
_angle = 0;
|
||||
_contrast = 1.0;
|
||||
_saturation = 1.0;
|
||||
_brightness = 0;
|
||||
_interactionMode = 'none';
|
||||
});
|
||||
_lastRightTapTime = null;
|
||||
@@ -204,7 +207,10 @@ class _ImageCanvasState extends State<ImageCanvas> {
|
||||
_lastRightTapTime = now;
|
||||
_lastRightTapPos = e.localPosition;
|
||||
|
||||
if (_ctrlPressed) {
|
||||
if (_ctrlPressed && _shiftPressed) {
|
||||
_interactionMode = 'brightness';
|
||||
_startBrightness = _brightness;
|
||||
} else if (_ctrlPressed) {
|
||||
_interactionMode = 'saturation';
|
||||
_startSaturation = _saturation;
|
||||
} else if (_shiftPressed) {
|
||||
@@ -237,6 +243,11 @@ class _ImageCanvasState extends State<ImageCanvas> {
|
||||
_saturation = (_startSaturation + dy * 0.005).clamp(0.0, 2.0);
|
||||
});
|
||||
break;
|
||||
case 'brightness':
|
||||
setState(() {
|
||||
_brightness = (_startBrightness + dy * 0.5).clamp(-100.0, 100.0);
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -244,7 +255,7 @@ class _ImageCanvasState extends State<ImageCanvas> {
|
||||
_interactionMode = 'none';
|
||||
}
|
||||
|
||||
// ── Combined color filter (saturation × contrast) ──
|
||||
// ── Combined color filter (brightness × saturation × contrast) ──
|
||||
|
||||
static const double _rLum = 0.2126;
|
||||
static const double _gLum = 0.7152;
|
||||
@@ -253,29 +264,72 @@ class _ImageCanvasState extends State<ImageCanvas> {
|
||||
ColorFilter? _cachedFilter;
|
||||
double _cachedSat = -1;
|
||||
double _cachedCon = -1;
|
||||
double _cachedBri = double.nan;
|
||||
|
||||
List<double> _multiply4x5(List<double> a, List<double> b) {
|
||||
final result = List<double>.filled(20, 0.0);
|
||||
for (int row = 0; row < 4; row++) {
|
||||
for (int col = 0; col < 4; col++) {
|
||||
double sum = 0.0;
|
||||
for (int k = 0; k < 4; k++) {
|
||||
sum += a[row * 5 + k] * b[k * 5 + col];
|
||||
}
|
||||
result[row * 5 + col] = sum;
|
||||
}
|
||||
double offsetSum = a[row * 5 + 4];
|
||||
for (int k = 0; k < 4; k++) {
|
||||
offsetSum += a[row * 5 + k] * b[k * 5 + 4];
|
||||
}
|
||||
result[row * 5 + 4] = offsetSum;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
ColorFilter _buildCombinedFilter() {
|
||||
if (_cachedFilter != null && _cachedSat == _saturation && _cachedCon == _contrast) {
|
||||
if (_cachedFilter != null &&
|
||||
_cachedSat == _saturation &&
|
||||
_cachedCon == _contrast &&
|
||||
_cachedBri == _brightness) {
|
||||
return _cachedFilter!;
|
||||
}
|
||||
_cachedSat = _saturation;
|
||||
_cachedCon = _contrast;
|
||||
_cachedBri = _brightness;
|
||||
|
||||
final s = _saturation;
|
||||
final c = _contrast;
|
||||
final t = (1.0 - c) / 2.0;
|
||||
final b = _brightness;
|
||||
final invS = 1.0 - s;
|
||||
|
||||
final sR_R = invS * _rLum + s, sR_G = invS * _gLum, sR_B = invS * _bLum;
|
||||
final sG_R = invS * _rLum, sG_G = invS * _gLum + s, sG_B = invS * _bLum;
|
||||
final sB_R = invS * _rLum, sB_G = invS * _gLum, sB_B = invS * _bLum + s;
|
||||
|
||||
_cachedFilter = ColorFilter.matrix(<double>[
|
||||
c * sR_R, c * sR_G, c * sR_B, 0, t,
|
||||
c * sG_R, c * sG_G, c * sG_B, 0, t,
|
||||
c * sB_R, c * sB_G, c * sB_B, 0, t,
|
||||
0, 0, 0, 1, 0,
|
||||
]);
|
||||
final saturationMatrix = <double>[
|
||||
sR_R, sR_G, sR_B, 0, 0,
|
||||
sG_R, sG_G, sG_B, 0, 0,
|
||||
sB_R, sB_G, sB_B, 0, 0,
|
||||
0, 0, 0, 1, 0,
|
||||
];
|
||||
|
||||
final brightnessMatrix = <double>[
|
||||
1, 0, 0, 0, b,
|
||||
0, 1, 0, 0, b,
|
||||
0, 0, 1, 0, b,
|
||||
0, 0, 0, 1, 0,
|
||||
];
|
||||
|
||||
final offset = 128.0 * (1.0 - c);
|
||||
final contrastMatrix = <double>[
|
||||
c, 0, 0, 0, offset,
|
||||
0, c, 0, 0, offset,
|
||||
0, 0, c, 0, offset,
|
||||
0, 0, 0, 1, 0,
|
||||
];
|
||||
|
||||
// M_final = M_contrast * M_brightness * M_saturation
|
||||
_cachedFilter = ColorFilter.matrix(
|
||||
_multiply4x5(_multiply4x5(contrastMatrix, brightnessMatrix), saturationMatrix));
|
||||
return _cachedFilter!;
|
||||
}
|
||||
|
||||
@@ -302,7 +356,7 @@ class _ImageCanvasState extends State<ImageCanvas> {
|
||||
|
||||
Widget imageContent = Builder(
|
||||
builder: (context) {
|
||||
final hasFilter = _saturation != 1.0 || _contrast != 1.0;
|
||||
final hasFilter = _saturation != 1.0 || _contrast != 1.0 || _brightness != 0.0;
|
||||
final image = _isFilled
|
||||
? SizedBox.expand(child: _buildImage())
|
||||
: Center(child: _buildImage());
|
||||
|
||||
Reference in New Issue
Block a user