initial commit: LeafViewer, LeafZoomable, LeafContextMenu, LeafPopover, LeafImage, misc widgets + tests
This commit is contained in:
commit
9bb0ff9187
156 files changed
+9887
No files matched your search
@@ -0,0 +1,171 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:leaf_widgets/leaf_widgets.dart';
|
||||
|
||||
import 'viewer_demo.dart';
|
||||
|
||||
void main() {
|
||||
runApp(const LeafWidgetsGalleryApp());
|
||||
}
|
||||
|
||||
class LeafWidgetsGalleryApp extends StatelessWidget {
|
||||
const LeafWidgetsGalleryApp({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MaterialApp(
|
||||
title: 'leaf_widgets gallery',
|
||||
debugShowCheckedModeBanner: false,
|
||||
theme: ThemeData(
|
||||
colorScheme: ColorScheme.fromSeed(seedColor: Colors.green, brightness: Brightness.dark),
|
||||
),
|
||||
home: const _GalleryHome(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _GalleryHome extends StatefulWidget {
|
||||
const _GalleryHome();
|
||||
|
||||
@override
|
||||
State<_GalleryHome> createState() => _GalleryHomeState();
|
||||
}
|
||||
|
||||
class _GalleryHomeState extends State<_GalleryHome> {
|
||||
bool _favorited = false;
|
||||
bool _swipeEmulation = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final cs = Theme.of(context).colorScheme;
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('leaf_widgets gallery')),
|
||||
body: ListView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
children: [
|
||||
_Section(title: 'AnimatedStar', child: const Row(children: [AnimatedStar(), SizedBox(width: 24), AnimatedStar(size: 36, color: Colors.cyanAccent)])),
|
||||
_Section(
|
||||
title: 'AnimatedFavoriteButton',
|
||||
child: Row(
|
||||
children: [
|
||||
AnimatedFavoriteButton(isFavorited: _favorited, onChanged: (v) async {
|
||||
await Future<void>.delayed(const Duration(milliseconds: 600));
|
||||
if (!mounted) return false;
|
||||
setState(() => _favorited = v);
|
||||
return true;
|
||||
}),
|
||||
const SizedBox(width: 24),
|
||||
const AnimatedFavoriteButton(),
|
||||
],
|
||||
),
|
||||
),
|
||||
_Section(
|
||||
title: 'LeafPopover (tap trigger)',
|
||||
child: LeafPopover(
|
||||
popoverAnchor: Alignment.topCenter,
|
||||
childAnchor: Alignment.bottomCenter,
|
||||
spacing: 8,
|
||||
popoverBuilder: (context, controller) => Card(
|
||||
margin: EdgeInsets.zero,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Text('Popover content'),
|
||||
TextButton(onPressed: controller.hide, child: const Text('Close')),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
child: const Chip(label: Text('Open popover')),
|
||||
),
|
||||
),
|
||||
_Section(
|
||||
title: 'LeafContextMenu (right-click / long-press)',
|
||||
child: LeafContextMenu(
|
||||
entries: const [
|
||||
ContextMenuAction(label: 'Copy', icon: Icons.copy, shortcut: 'Ctrl+C'),
|
||||
ContextMenuAction(label: 'Open', icon: Icons.open_in_new),
|
||||
ContextMenuDivider(),
|
||||
ContextMenuAction(label: 'Delete', icon: Icons.delete, destructive: true),
|
||||
],
|
||||
onDismissed: () => debugPrint('menu dismissed'),
|
||||
child: Container(
|
||||
height: 100,
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(color: cs.surfaceContainerHigh, borderRadius: BorderRadius.circular(12)),
|
||||
child: const Text('Right-click me'),
|
||||
),
|
||||
),
|
||||
),
|
||||
_Section(
|
||||
title: 'LeafImage (network + reveal)',
|
||||
child: LeafImage(
|
||||
image: const NetworkImage('https://picsum.photos/seed/leaf/800/500'),
|
||||
height: 220,
|
||||
width: double.infinity,
|
||||
fit: BoxFit.cover,
|
||||
imageBuilder: (context, child) => ClipRRect(borderRadius: BorderRadius.circular(12), child: child),
|
||||
revealAnimation: const GradientRevealAnimation(),
|
||||
loadingBuilder: (context, progress) => Container(
|
||||
decoration: BoxDecoration(color: cs.surfaceContainerHigh, borderRadius: BorderRadius.circular(12)),
|
||||
child: progress > 0
|
||||
? Center(child: LinearProgressIndicator(value: progress))
|
||||
: const Center(child: CircularProgressIndicator()),
|
||||
),
|
||||
errorBuilder: (context, error, retry) => Center(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [const Icon(Icons.error_outline), TextButton(onPressed: retry, child: const Text('Retry'))],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
_Section(
|
||||
title: 'LeafViewer (fullscreen feel test)',
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
FilledButton.icon(
|
||||
icon: const Icon(Icons.fullscreen),
|
||||
label: const Text('Open 3-image viewer'),
|
||||
onPressed: () => openLeafViewerDemo(context, enableSwipeGestures: _swipeEmulation),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
SwitchListTile(
|
||||
dense: true,
|
||||
contentPadding: EdgeInsets.zero,
|
||||
title: const Text('Swipe gestures (phone emulation)'),
|
||||
value: _swipeEmulation,
|
||||
onChanged: (v) => setState(() => _swipeEmulation = v),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _Section extends StatelessWidget {
|
||||
final String title;
|
||||
final Widget child;
|
||||
|
||||
const _Section({required this.title, required this.child});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 24),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(title, style: Theme.of(context).textTheme.titleSmall),
|
||||
const SizedBox(height: 12),
|
||||
child,
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
import 'dart:io';
|
||||
import 'dart:ui' as ui;
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:leaf_widgets/leaf_widgets.dart';
|
||||
|
||||
class _DemoImage {
|
||||
const _DemoImage(this.url, this.size);
|
||||
final String url;
|
||||
final Size size;
|
||||
}
|
||||
|
||||
const _demoImages = [
|
||||
_DemoImage(
|
||||
'https://file.pawchive.pw/data/82/91/829126892beebd0a7ff5b5a398e63a760b53162b15323ce900d944d126aa69ec.png?f=829126892beebd0a7ff5b5a398e63a760b53162b15323ce900d944d126aa69ec.png',
|
||||
Size(1457, 2048),
|
||||
),
|
||||
_DemoImage(
|
||||
'https://file.pawchive.pw/data/09/f4/09f4e606de27786cf633282b8d3e194b62a0b6859368f8ac38d28300972f763c.jpeg?f=09f4e606de27786cf633282b8d3e194b62a0b6859368f8ac38d28300972f763c.jpeg',
|
||||
Size(8500, 6000),
|
||||
),
|
||||
_DemoImage(
|
||||
'https://file.pawchive.pw/data/79/e9/79e996f02a2700dbc38c590057d027b7566608be8e87e49950cec165d6f8b525.png?f=79e996f02a2700dbc38c590057d027b7566608be8e87e49950cec165d6f8b525.png',
|
||||
Size(3267, 1634),
|
||||
),
|
||||
];
|
||||
|
||||
Size _containSize(Size viewport, Size image) {
|
||||
final scale = viewport.width / image.width < viewport.height / image.height
|
||||
? viewport.width / image.width
|
||||
: viewport.height / image.height;
|
||||
return Size(image.width * scale, image.height * scale);
|
||||
}
|
||||
|
||||
void openLeafViewerDemo(BuildContext context, {bool enableSwipeGestures = false}) {
|
||||
Navigator.of(context).push(
|
||||
PageRouteBuilder(
|
||||
// Transparent route so the scrim fade reveals the gallery behind it,
|
||||
// like the real app's fullscreen viewer.
|
||||
opaque: false,
|
||||
barrierColor: Colors.transparent,
|
||||
barrierDismissible: true,
|
||||
transitionDuration: const Duration(milliseconds: 200),
|
||||
reverseTransitionDuration: const Duration(milliseconds: 200),
|
||||
transitionsBuilder: (context, animation, secondaryAnimation, child) =>
|
||||
FadeTransition(opacity: CurvedAnimation(parent: animation, curve: Curves.easeOut), child: child),
|
||||
pageBuilder: (context, animation, secondaryAnimation) => Scaffold(
|
||||
backgroundColor: Colors.transparent,
|
||||
body: Stack(
|
||||
children: [
|
||||
LeafViewer(
|
||||
pageCount: _demoImages.length,
|
||||
enableSwipeGestures: enableSwipeGestures,
|
||||
backgroundBuilder: (context, progress) =>
|
||||
Container(color: Colors.black.withValues(alpha: 0.87 * (1 - progress))),
|
||||
pageBuilder: (context, index) {
|
||||
final demo = _demoImages[index];
|
||||
return LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
return LeafZoomable(
|
||||
childSize: _containSize(constraints.biggest, demo.size),
|
||||
originalSize: demo.size,
|
||||
child: Image(
|
||||
image: _HeaderImageProvider(demo.url),
|
||||
fit: BoxFit.contain,
|
||||
filterQuality: FilterQuality.high,
|
||||
errorBuilder: (context, error, stackTrace) => const ColoredBox(
|
||||
color: Color(0xFF1A1A1A),
|
||||
child: Center(child: Icon(Icons.broken_image, color: Colors.white38)),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
Positioned(
|
||||
top: 8,
|
||||
right: 8,
|
||||
child: IconButton(
|
||||
icon: const Icon(Icons.close),
|
||||
color: Colors.white70,
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// ImageProvider for `file.pawchive.pw` (requires Referer + browser UA).
|
||||
class _HeaderImageProvider extends ImageProvider<_HeaderImageProvider> {
|
||||
const _HeaderImageProvider(this.url);
|
||||
|
||||
final String url;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) => other is _HeaderImageProvider && other.url == url;
|
||||
|
||||
@override
|
||||
int get hashCode => url.hashCode;
|
||||
|
||||
@override
|
||||
Future<_HeaderImageProvider> obtainKey(ImageConfiguration configuration) => SynchronousFuture(this);
|
||||
|
||||
@override
|
||||
ImageStreamCompleter loadImage(_HeaderImageProvider key, ImageDecoderCallback decode) {
|
||||
return MultiFrameImageStreamCompleter(codec: _loadAsync(decode), scale: 1.0);
|
||||
}
|
||||
|
||||
Future<ui.Codec> _loadAsync(ImageDecoderCallback decode) async {
|
||||
final client = HttpClient();
|
||||
try {
|
||||
final request = await client.getUrl(Uri.parse(url));
|
||||
request.headers.set('Referer', 'https://pawchive.pw/');
|
||||
request.headers.set('User-Agent',
|
||||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36');
|
||||
final response = await request.close();
|
||||
if (response.statusCode != 200) {
|
||||
throw HttpException('GET $url -> ${response.statusCode}', uri: Uri.parse(url));
|
||||
}
|
||||
final bytes = await consolidateHttpClientResponseBytes(response);
|
||||
return await decode(await ui.ImmutableBuffer.fromUint8List(bytes));
|
||||
} finally {
|
||||
client.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user