Initial commit: imajViewer Flutter Linux image viewer
This commit is contained in:
commit
8369ec2a5d
33 files changed
+2165
No files matched your search
@@ -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),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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