Initial commit: imajViewer Flutter Linux image viewer
This commit is contained in:
24
lib/app.dart
Normal file
24
lib/app.dart
Normal 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
42
lib/main.dart
Normal 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));
|
||||
}
|
||||
165
lib/screens/viewer_screen.dart
Normal file
165
lib/screens/viewer_screen.dart
Normal 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)),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
28
lib/services/file_handler.dart
Normal file
28
lib/services/file_handler.dart
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
76
lib/services/image_manager.dart
Normal file
76
lib/services/image_manager.dart
Normal 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();
|
||||
}
|
||||
}
|
||||
49
lib/services/window_persistence.dart
Normal file
49
lib/services/window_persistence.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
60
lib/shortcuts/app_shortcuts.dart
Normal file
60
lib/shortcuts/app_shortcuts.dart
Normal 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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
78
lib/widgets/custom_title_bar.dart
Normal file
78
lib/widgets/custom_title_bar.dart
Normal 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),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
125
lib/widgets/image_canvas.dart
Normal file
125
lib/widgets/image_canvas.dart
Normal 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),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
118
lib/widgets/window_resize_zones.dart
Normal file
118
lib/widgets/window_resize_zones.dart
Normal 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),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user