Initial commit: imajViewer Flutter Linux image viewer

This commit is contained in:
Alhan committed 2026-07-22 03:59:14 +03:00
commit 8369ec2a5d
33 files changed
+2165

No files matched your search

+78
View File
@@ -0,0 +1,78 @@
import 'package:flutter/material.dart';
import 'package:window_manager/window_manager.dart';
class CustomTitleBar extends StatelessWidget {
const CustomTitleBar({super.key});
@override
Widget build(BuildContext context) {
return Positioned(
top: 0,
left: 0,
right: 0,
child: DragToMoveArea(
child: SizedBox(
height: 40,
child: Stack(
children: [
Positioned.fill(
child: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Colors.black.withValues(alpha: 0.6),
Colors.black.withValues(alpha: 0.0),
],
),
),
),
),
Positioned(
right: 0,
top: 0,
bottom: 0,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
_BarButton(
icon: Icons.horizontal_rule,
onTap: () => windowManager.minimize(),
),
_BarButton(
icon: Icons.close,
onTap: () => windowManager.close(),
),
],
),
),
],
),
),
),
);
}
}
class _BarButton extends StatelessWidget {
final IconData icon;
final VoidCallback onTap;
const _BarButton({required this.icon, required this.onTap});
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: onTap,
child: MouseRegion(
cursor: SystemMouseCursors.click,
child: SizedBox(
width: 44,
height: 40,
child: Icon(icon, color: Colors.white54, size: 18),
),
),
);
}
}
+125
View File
@@ -0,0 +1,125 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter/gestures.dart';
class ImageCanvas extends StatefulWidget {
final String filePath;
const ImageCanvas({super.key, required this.filePath});
@override
State<ImageCanvas> createState() => _ImageCanvasState();
}
class _ImageCanvasState extends State<ImageCanvas> {
late TransformationController _controller;
double _currentScale = 1.0;
@override
void initState() {
super.initState();
_controller = TransformationController();
_controller.addListener(_onTransformChanged);
}
@override
void didUpdateWidget(ImageCanvas oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.filePath != widget.filePath) {
_resetView();
}
}
void _resetView() {
_controller.value = Matrix4.identity();
_currentScale = 1.0;
}
void _onTransformChanged() {
_currentScale = _controller.value.getMaxScaleOnAxis();
}
void _handleScroll(PointerScrollEvent event) {
final renderBox = context.findRenderObject() as RenderBox?;
if (renderBox == null) return;
final localPosition = renderBox.globalToLocal(event.position);
final delta = event.scrollDelta.dy;
final scaleFactor = delta < 0 ? 1.1 : 1 / 1.1;
final newScale = (_currentScale * scaleFactor).clamp(0.1, 10.0);
if (newScale <= 0.1 || newScale >= 10.0) return;
final actualScale = newScale / _currentScale;
// Decompose current matrix: widget_point = scale * child_point + translation
// So: child_point = (widget_point - translation) / scale
final matrix = _controller.value;
final tx = matrix.getTranslation().x;
final ty = matrix.getTranslation().y;
final childX = (localPosition.dx - tx) / _currentScale;
final childY = (localPosition.dy - ty) / _currentScale;
// Build new matrix: zoom around child_point, then re-apply
final newMatrix = Matrix4.identity()
// ignore: deprecated_member_use
..translate(childX, childY)
// ignore: deprecated_member_use
..scale(actualScale)
// ignore: deprecated_member_use
..translate(-childX, -childY)
..multiply(_controller.value);
_controller.value = newMatrix;
}
@override
void dispose() {
_controller.removeListener(_onTransformChanged);
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Listener(
onPointerSignal: (event) {
if (event is PointerScrollEvent) {
_handleScroll(event);
}
},
child: InteractiveViewer(
transformationController: _controller,
boundaryMargin: const EdgeInsets.all(double.infinity),
minScale: 0.1,
maxScale: 10.0,
panEnabled: true,
scaleEnabled: false,
child: Center(
child: Image.file(
File(widget.filePath),
fit: BoxFit.contain,
filterQuality: FilterQuality.high,
errorBuilder: (context, error, stackTrace) {
return const Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.broken_image, size: 48, color: Colors.white38),
SizedBox(height: 8),
Text(
'Resim yüklenemedi',
style: TextStyle(color: Colors.white38),
),
],
),
);
},
),
),
),
);
}
}
+118
View File
@@ -0,0 +1,118 @@
import 'package:flutter/material.dart';
import 'package:window_manager/window_manager.dart';
/// Resize edge zones for frameless windows.
class WindowResizeZones extends StatelessWidget {
final Widget child;
const WindowResizeZones({super.key, required this.child});
static const _edgeSize = 4.0;
static const _cornerSize = 10.0;
@override
Widget build(BuildContext context) {
return Stack(
children: [
child,
// Top edge
_ResizeEdge(
edge: ResizeEdge.top,
top: 0, left: _cornerSize, right: _cornerSize, height: _edgeSize,
),
// Bottom edge
_ResizeEdge(
edge: ResizeEdge.bottom,
bottom: 0, left: _cornerSize, right: _cornerSize, height: _edgeSize,
),
// Left edge
_ResizeEdge(
edge: ResizeEdge.left,
left: 0, top: _cornerSize, bottom: _cornerSize, width: _edgeSize,
),
// Right edge
_ResizeEdge(
edge: ResizeEdge.right,
right: 0, top: _cornerSize, bottom: _cornerSize, width: _edgeSize,
),
// Corners
_ResizeCorner(edge: ResizeEdge.topLeft, top: 0, left: 0),
_ResizeCorner(edge: ResizeEdge.topRight, top: 0, right: 0),
_ResizeCorner(edge: ResizeEdge.bottomLeft, bottom: 0, left: 0),
_ResizeCorner(edge: ResizeEdge.bottomRight, bottom: 0, right: 0),
],
);
}
}
class _ResizeEdge extends StatelessWidget {
final ResizeEdge edge;
final double? top, bottom, left, right, width, height;
const _ResizeEdge({
required this.edge,
this.top, this.bottom, this.left, this.right,
this.width, this.height,
});
MouseCursor _cursor(ResizeEdge e) {
return switch (e) {
ResizeEdge.top || ResizeEdge.bottom => SystemMouseCursors.resizeRow,
ResizeEdge.left || ResizeEdge.right => SystemMouseCursors.resizeColumn,
_ => SystemMouseCursors.basic,
};
}
@override
Widget build(BuildContext context) {
return Positioned(
top: top, bottom: bottom, left: left, right: right,
width: width, height: height,
child: MouseRegion(
cursor: _cursor(edge),
child: GestureDetector(
behavior: HitTestBehavior.opaque,
onPanStart: (_) => windowManager.startResizing(edge),
),
),
);
}
}
class _ResizeCorner extends StatelessWidget {
final ResizeEdge edge;
final double? top, bottom, left, right;
const _ResizeCorner({
required this.edge,
this.top, this.bottom, this.left, this.right,
});
MouseCursor _cursor(ResizeEdge e) {
return switch (e) {
ResizeEdge.topLeft || ResizeEdge.bottomRight =>
SystemMouseCursors.resizeDownLeft,
ResizeEdge.topRight || ResizeEdge.bottomLeft =>
SystemMouseCursors.resizeDownRight,
_ => SystemMouseCursors.basic,
};
}
@override
Widget build(BuildContext context) {
return Positioned(
top: top, bottom: bottom, left: left, right: right,
width: WindowResizeZones._cornerSize,
height: WindowResizeZones._cornerSize,
child: MouseRegion(
cursor: _cursor(edge),
child: GestureDetector(
behavior: HitTestBehavior.opaque,
onPanStart: (_) => windowManager.startResizing(edge),
),
),
);
}
}