chore: Added easy screenshot taker and new screenshots

This commit is contained in:
PartyDonut 2025-10-30 19:24:21 +01:00
parent 4483e4a1f5
commit 6212e06404
30 changed files with 254 additions and 3 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 MiB

After

Width:  |  Height:  |  Size: 639 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 MiB

After

Width:  |  Height:  |  Size: 579 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 MiB

After

Width:  |  Height:  |  Size: 462 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 MiB

After

Width:  |  Height:  |  Size: 412 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 MiB

After

Width:  |  Height:  |  Size: 534 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 MiB

After

Width:  |  Height:  |  Size: 524 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 MiB

After

Width:  |  Height:  |  Size: 166 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 MiB

After

Width:  |  Height:  |  Size: 515 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 98 KiB

After

Width:  |  Height:  |  Size: 93 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 930 KiB

After

Width:  |  Height:  |  Size: 251 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.3 MiB

After

Width:  |  Height:  |  Size: 1.5 MiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 MiB

After

Width:  |  Height:  |  Size: 1.3 MiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 MiB

After

Width:  |  Height:  |  Size: 991 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 MiB

After

Width:  |  Height:  |  Size: 789 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 MiB

After

Width:  |  Height:  |  Size: 978 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 MiB

After

Width:  |  Height:  |  Size: 1.3 MiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 MiB

After

Width:  |  Height:  |  Size: 642 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 MiB

After

Width:  |  Height:  |  Size: 1.1 MiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 244 KiB

After

Width:  |  Height:  |  Size: 95 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

After

Width:  |  Height:  |  Size: 417 KiB

Before After
Before After

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 808 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 143 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 486 KiB

View file

@ -0,0 +1,251 @@
import 'dart:io';
import 'dart:ui' as ui;
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:fladder/util/adaptive_layout/adaptive_layout.dart';
import 'package:fladder/util/adaptive_layout/adaptive_layout_model.dart';
import 'package:fladder/util/poster_defaults.dart';
class _DeviceProfile {
final String name;
final Size size;
final double scale;
final bool isDesktop;
final InputDevice inputDevice;
final ViewSize viewSize;
final LayoutMode layoutMode;
final TargetPlatform platform;
const _DeviceProfile({
required this.name,
required this.size,
this.scale = 1.0,
this.isDesktop = false,
this.inputDevice = InputDevice.dPad,
required this.viewSize,
required this.layoutMode,
required this.platform,
});
}
class ScreenshotRunner extends StatefulWidget {
final Widget child;
final String outputPath;
const ScreenshotRunner({
super.key,
required this.child,
required this.outputPath,
});
@override
State<ScreenshotRunner> createState() => _ScreenshotRunnerState();
}
class _ScreenshotRunnerState extends State<ScreenshotRunner> {
final GlobalKey _repaintKey = GlobalKey();
int _currentIndex = 0;
bool _isCapturing = false;
bool _showNameInput = false;
final TextEditingController _nameController = TextEditingController();
Future<void> _capture(_DeviceProfile device, String screenshotName) async {
final boundary = _repaintKey.currentContext!.findRenderObject() as RenderRepaintBoundary?;
if (boundary == null) {
debugPrint('No boundary found for ${device.name}');
return;
}
final rawImage = await boundary.toImage(pixelRatio: 1.0);
final resized = await resizeUiImage(
rawImage,
(device.size.width * device.scale).toInt(),
(device.size.height * device.scale).toInt(),
);
final byteData = await resized.toByteData(format: ui.ImageByteFormat.png);
final bytes = byteData!.buffer.asUint8List();
final dir = Directory('${widget.outputPath}/${device.name}');
if (!dir.existsSync()) dir.createSync(recursive: true);
final file = File('${dir.path}/$screenshotName.png');
await file.writeAsBytes(bytes);
debugPrint('Saved: ${file.path}');
}
Future<void> _runScreenshots(String value) async {
if (_isCapturing) return;
setState(() => _isCapturing = true);
for (int i = 0; i < _defaultDeviceProfiles.length; i++) {
setState(() => _currentIndex = i);
await Future.delayed(const Duration(milliseconds: 300));
await _capture(_defaultDeviceProfiles[i], value);
}
setState(() => _isCapturing = false);
}
Future<ui.Image> resizeUiImage(ui.Image image, int targetWidth, int targetHeight) async {
final recorder = ui.PictureRecorder();
final canvas = Canvas(recorder);
final paint = Paint();
final src = Rect.fromLTWH(0, 0, image.width.toDouble(), image.height.toDouble());
final dst = Rect.fromLTWH(0, 0, targetWidth.toDouble(), targetHeight.toDouble());
canvas.drawImageRect(image, src, dst, paint);
final picture = recorder.endRecording();
return await picture.toImage(targetWidth, targetHeight);
}
@override
Widget build(BuildContext context) {
final device = _defaultDeviceProfiles[_currentIndex];
final scale = device.scale;
final mediaQuery = MediaQuery.of(context);
final screenSize = device.size;
final scaledMedia = _isCapturing
? mediaQuery.copyWith(
size: screenSize,
padding: EdgeInsets.zero,
viewInsets: EdgeInsets.zero,
viewPadding: EdgeInsets.zero,
devicePixelRatio: mediaQuery.devicePixelRatio * scale,
)
: MediaQuery.of(context);
return Scaffold(
body: Stack(
children: [
Center(
child: RepaintBoundary(
key: _repaintKey,
child: FittedBox(
alignment: Alignment.center,
child: SizedBox(
width: scaledMedia.size.width,
height: scaledMedia.size.height,
child: MediaQuery(
data: scaledMedia,
child: Builder(
builder: (context) {
return AdaptiveLayout(
data: _isCapturing
? AdaptiveLayoutModel(
viewSize: device.viewSize,
layoutMode: device.layoutMode,
inputDevice: device.inputDevice,
platform: device.platform,
isDesktop: device.isDesktop,
posterDefaults: const PosterDefaults(size: 350, ratio: 0.55),
controller: {},
sideBarWidth: 0,
)
: AdaptiveLayout.of(context),
child: widget.child,
);
},
),
),
),
),
),
),
if (_showNameInput)
Center(
child: Overlay(
initialEntries: [
OverlayEntry(
canSizeOverlay: true,
builder: (context) => Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.black,
borderRadius: BorderRadius.circular(12),
),
width: 300,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Text("Enter screenshot name:"),
const SizedBox(height: 8),
TextField(
controller: _nameController,
autofocus: true,
onSubmitted: (value) {
setState(() => _showNameInput = false);
_runScreenshots(value.trim());
_nameController.clear();
},
),
const SizedBox(height: 12),
ElevatedButton(
onPressed: () {
setState(() => _showNameInput = false);
_runScreenshots(_nameController.text.trim());
_nameController.clear();
},
child: const Text("Capture"),
),
],
),
),
)
],
),
),
],
),
floatingActionButton: FloatingActionButton.extended(
onPressed: () => setState(() {
_showNameInput = true;
}),
label: Text(_isCapturing ? 'Capturing...' : 'Take Screenshots'),
icon: const Icon(Icons.camera),
),
);
}
}
const _defaultDeviceProfiles = [
_DeviceProfile(
name: 'Mobile',
size: Size(412, 915),
scale: 3.0,
inputDevice: InputDevice.touch,
viewSize: ViewSize.phone,
layoutMode: LayoutMode.single,
platform: TargetPlatform.android,
isDesktop: false,
),
_DeviceProfile(
name: 'Tablet',
size: Size(1194, 834),
scale: 2.0,
inputDevice: InputDevice.touch,
viewSize: ViewSize.tablet,
layoutMode: LayoutMode.single,
platform: TargetPlatform.android,
isDesktop: false,
),
_DeviceProfile(
name: 'Television',
size: Size(1920, 1080),
scale: 1.0,
inputDevice: InputDevice.dPad,
viewSize: ViewSize.television,
layoutMode: LayoutMode.dual,
platform: TargetPlatform.android,
isDesktop: false,
),
];

View file

@ -72,12 +72,12 @@ class _SideNavigationBarState extends ConsumerState<SideNavigationBar> {
return Stack( return Stack(
children: [ children: [
AdaptiveLayoutBuilder( AdaptiveLayout(
adaptiveLayout: AdaptiveLayout.of(context).copyWith( data: AdaptiveLayout.of(context).copyWith(
// -0.1 offset to fix single visible pixel line // -0.1 offset to fix single visible pixel line
sideBarWidth: (fullyExpanded ? expandedWidth : collapsedWidth) - 0.1, sideBarWidth: (fullyExpanded ? expandedWidth : collapsedWidth) - 0.1,
), ),
child: (context) => widget.child, child: widget.child,
), ),
FocusTraversalGroup( FocusTraversalGroup(
policy: _RailTraversalPolicy(), policy: _RailTraversalPolicy(),