chore: Added easy screenshot taker and new screenshots
|
Before Width: | Height: | Size: 2.5 MiB After Width: | Height: | Size: 639 KiB |
|
Before Width: | Height: | Size: 1.9 MiB After Width: | Height: | Size: 579 KiB |
|
Before Width: | Height: | Size: 1.2 MiB After Width: | Height: | Size: 462 KiB |
|
Before Width: | Height: | Size: 1.5 MiB After Width: | Height: | Size: 412 KiB |
|
Before Width: | Height: | Size: 1.6 MiB After Width: | Height: | Size: 534 KiB |
|
Before Width: | Height: | Size: 1.8 MiB After Width: | Height: | Size: 524 KiB |
|
Before Width: | Height: | Size: 1.4 MiB After Width: | Height: | Size: 166 KiB |
|
Before Width: | Height: | Size: 1.8 MiB After Width: | Height: | Size: 515 KiB |
|
Before Width: | Height: | Size: 98 KiB After Width: | Height: | Size: 93 KiB |
|
Before Width: | Height: | Size: 930 KiB After Width: | Height: | Size: 251 KiB |
|
Before Width: | Height: | Size: 4.3 MiB After Width: | Height: | Size: 1.5 MiB |
|
Before Width: | Height: | Size: 3.3 MiB After Width: | Height: | Size: 1.3 MiB |
|
Before Width: | Height: | Size: 2.1 MiB After Width: | Height: | Size: 991 KiB |
|
Before Width: | Height: | Size: 2.2 MiB After Width: | Height: | Size: 789 KiB |
|
Before Width: | Height: | Size: 3.3 MiB After Width: | Height: | Size: 978 KiB |
|
Before Width: | Height: | Size: 2.8 MiB After Width: | Height: | Size: 1.3 MiB |
|
Before Width: | Height: | Size: 1.6 MiB After Width: | Height: | Size: 642 KiB |
|
Before Width: | Height: | Size: 3.6 MiB After Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 244 KiB After Width: | Height: | Size: 95 KiB |
|
Before Width: | Height: | Size: 1.1 MiB After Width: | Height: | Size: 417 KiB |
BIN
assets/marketing/screenshots/Television/Dashboard.png
Normal file
|
After Width: | Height: | Size: 1.8 MiB |
BIN
assets/marketing/screenshots/Television/Details.png
Normal file
|
After Width: | Height: | Size: 2 MiB |
BIN
assets/marketing/screenshots/Television/Favourites.png
Normal file
|
After Width: | Height: | Size: 808 KiB |
BIN
assets/marketing/screenshots/Television/Library.png
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
BIN
assets/marketing/screenshots/Television/Library_Search.png
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
BIN
assets/marketing/screenshots/Television/Player.png
Normal file
|
After Width: | Height: | Size: 5.4 MiB |
BIN
assets/marketing/screenshots/Television/Settings.png
Normal file
|
After Width: | Height: | Size: 143 KiB |
BIN
assets/marketing/screenshots/Television/Sync.png
Normal file
|
After Width: | Height: | Size: 486 KiB |
251
lib/util/screenshot_runner.dart
Normal 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,
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
@ -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(),
|
||||||
|
|
|
||||||