25ed765a03
6 Wochen uncommittete Arbeit (Voice-Channels, LiveKit-Manager, Settings-Modal u.v.m.) als ein WIP-Commit gesichert, damit nichts verloren geht und der Pi den aktuellen Stand klonen kann. Thematische Aufarbeitung: siehe ROADMAP M0. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CPrAGBxBT6GfPXzeWQ4AXb
545 lines
20 KiB
Dart
545 lines
20 KiB
Dart
// pyramid_video_player.dart
|
|
// Pyramid · Video Player
|
|
//
|
|
// Drei Varianten:
|
|
// VideoPlayerVariant.defaultHover → Chrome erscheint nur bei Hover/Tap (für Vollbild-Embeds)
|
|
// VideoPlayerVariant.minimal → Chrome immer sichtbar (für kleine Inline-Embeds)
|
|
// VideoPlayerVariant.framed → Editorial: Stage in Card, Controls außerhalb (für Galerie-Karten)
|
|
//
|
|
// Hinweis: Dies ist ein UI-Mockup. Für echte Wiedergabe diesen Widget mit
|
|
// `video_player`-Package verbinden (controller.value.position etc. an `position` mappen).
|
|
//
|
|
// Verwendung:
|
|
// PyramidVideoPlayer(
|
|
// variant: VideoPlayerVariant.defaultHover,
|
|
// title: 'test clip',
|
|
// subtitle: 'pyramid · 1080p',
|
|
// duration: Duration(minutes: 4, seconds: 12),
|
|
// position: Duration(minutes: 1, seconds: 28),
|
|
// buffered: 0.65,
|
|
// playing: true,
|
|
// onTogglePlay: () { ... },
|
|
// onSeek: (Duration to) { ... },
|
|
// onSpeedChange: (double speed) { ... },
|
|
// onQualityChange: (String quality) { ... },
|
|
// );
|
|
|
|
import 'package:flutter/material.dart';
|
|
import 'pyramid_theme.dart';
|
|
|
|
enum VideoPlayerVariant { defaultHover, minimal, framed }
|
|
|
|
class PyramidVideoPlayer extends StatefulWidget {
|
|
final VideoPlayerVariant variant;
|
|
final String title;
|
|
final String subtitle;
|
|
final Duration duration;
|
|
final Duration position;
|
|
final double buffered; // 0..1
|
|
final bool playing;
|
|
final double volume; // 0..1
|
|
final VoidCallback? onTogglePlay;
|
|
final ValueChanged<Duration>? onSeek;
|
|
final ValueChanged<double>? onVolumeChange;
|
|
final ValueChanged<double>? onSpeedChange;
|
|
final ValueChanged<String>? onQualityChange;
|
|
final VoidCallback? onSkipBack;
|
|
final VoidCallback? onSkipForward;
|
|
final VoidCallback? onFullscreen;
|
|
final VoidCallback? onCaptions;
|
|
final VoidCallback? onMore;
|
|
|
|
const PyramidVideoPlayer({
|
|
super.key,
|
|
this.variant = VideoPlayerVariant.defaultHover,
|
|
required this.title,
|
|
this.subtitle = '',
|
|
required this.duration,
|
|
required this.position,
|
|
this.buffered = 0.0,
|
|
this.playing = false,
|
|
this.volume = 0.7,
|
|
this.onTogglePlay,
|
|
this.onSeek,
|
|
this.onVolumeChange,
|
|
this.onSpeedChange,
|
|
this.onQualityChange,
|
|
this.onSkipBack,
|
|
this.onSkipForward,
|
|
this.onFullscreen,
|
|
this.onCaptions,
|
|
this.onMore,
|
|
});
|
|
|
|
@override
|
|
State<PyramidVideoPlayer> createState() => _PyramidVideoPlayerState();
|
|
}
|
|
|
|
class _PyramidVideoPlayerState extends State<PyramidVideoPlayer> {
|
|
bool _hovered = false;
|
|
String _quality = 'Auto';
|
|
double _speed = 1.0;
|
|
bool _settingsOpen = false;
|
|
|
|
static const _qualities = ['Auto', '1080p', '720p', '480p', '360p'];
|
|
static const _speeds = [0.5, 1.0, 1.25, 1.5, 2.0];
|
|
|
|
double get progress => widget.duration.inMilliseconds == 0
|
|
? 0
|
|
: widget.position.inMilliseconds / widget.duration.inMilliseconds;
|
|
|
|
bool get _showChrome {
|
|
if (widget.variant == VideoPlayerVariant.minimal) return true;
|
|
if (widget.variant == VideoPlayerVariant.framed) return true;
|
|
return _hovered || !widget.playing;
|
|
}
|
|
|
|
String _fmt(Duration d) {
|
|
final m = d.inMinutes;
|
|
final s = (d.inSeconds % 60).toString().padLeft(2, '0');
|
|
return '$m:$s';
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
if (widget.variant == VideoPlayerVariant.framed) return _buildFramed();
|
|
return _buildOverlay();
|
|
}
|
|
|
|
// ─── Default / Minimal ─────────────────────────────────────────────
|
|
Widget _buildOverlay() {
|
|
final aspect = widget.variant == VideoPlayerVariant.minimal ? 16 / 9 : 16 / 10;
|
|
return MouseRegion(
|
|
onEnter: (_) => setState(() => _hovered = true),
|
|
onExit: (_) => setState(() { _hovered = false; _settingsOpen = false; }),
|
|
child: GestureDetector(
|
|
onTap: widget.onTogglePlay,
|
|
child: ClipRRect(
|
|
borderRadius: BorderRadius.circular(PyramidTheme.rLg),
|
|
child: AspectRatio(
|
|
aspectRatio: aspect,
|
|
child: Stack(fit: StackFit.expand, children: [
|
|
_stage(),
|
|
_grain(),
|
|
if (!widget.playing) Center(child: _CenterPlay(onTap: widget.onTogglePlay)),
|
|
AnimatedOpacity(
|
|
duration: const Duration(milliseconds: 200),
|
|
opacity: _showChrome ? 1 : 0,
|
|
child: _topBar(onDark: true),
|
|
),
|
|
AnimatedOpacity(
|
|
duration: const Duration(milliseconds: 200),
|
|
opacity: _showChrome ? 1 : 0,
|
|
child: Align(alignment: Alignment.bottomCenter, child: _bottomBar(onDark: true)),
|
|
),
|
|
]),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
// ─── Framed ────────────────────────────────────────────────────────
|
|
Widget _buildFramed() {
|
|
return Container(
|
|
padding: const EdgeInsets.all(10),
|
|
decoration: BoxDecoration(
|
|
color: PyramidTheme.bg1,
|
|
border: Border.all(color: PyramidTheme.border),
|
|
borderRadius: BorderRadius.circular(PyramidTheme.rLg),
|
|
),
|
|
child: Column(mainAxisSize: MainAxisSize.min, children: [
|
|
ClipRRect(
|
|
borderRadius: BorderRadius.circular(PyramidTheme.rLg - 4),
|
|
child: AspectRatio(
|
|
aspectRatio: 16 / 9,
|
|
child: MouseRegion(
|
|
onEnter: (_) => setState(() => _hovered = true),
|
|
onExit: (_) => setState(() => _hovered = false),
|
|
child: GestureDetector(
|
|
onTap: widget.onTogglePlay,
|
|
child: Stack(fit: StackFit.expand, children: [
|
|
_stage(),
|
|
_grain(),
|
|
if (!widget.playing) Center(child: _CenterPlay(onTap: widget.onTogglePlay)),
|
|
_topBar(onDark: true),
|
|
]),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(height: 12),
|
|
_bottomBar(onDark: false),
|
|
]),
|
|
);
|
|
}
|
|
|
|
// ─── Stage (placeholder bg) ────────────────────────────────────────
|
|
Widget _stage() {
|
|
return Container(
|
|
decoration: BoxDecoration(
|
|
gradient: LinearGradient(
|
|
begin: Alignment.topLeft, end: Alignment.bottomRight,
|
|
colors: [const Color(0xFF1A1A22), const Color(0xFF0C0C10)],
|
|
),
|
|
),
|
|
alignment: Alignment.center,
|
|
child: Opacity(
|
|
opacity: 0.35,
|
|
child: CustomPaint(size: const Size(72, 60), painter: _BigTriPainter()),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _grain() => IgnorePointer(child: Opacity(
|
|
opacity: 0.05,
|
|
child: Container(decoration: const BoxDecoration(color: Colors.white12)),
|
|
));
|
|
|
|
// ─── Top bar (title + captions + more) ─────────────────────────────
|
|
Widget _topBar({required bool onDark}) {
|
|
final fg = onDark ? Colors.white : PyramidTheme.fg;
|
|
return Container(
|
|
padding: const EdgeInsets.fromLTRB(16, 14, 16, 14),
|
|
decoration: BoxDecoration(
|
|
gradient: onDark
|
|
? LinearGradient(begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [Colors.black.withOpacity(0.5), Colors.transparent])
|
|
: null,
|
|
),
|
|
child: Row(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
|
Expanded(child: Column(crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [
|
|
Text(widget.title, style: TextStyle(color: fg, fontSize: 13, fontFamily: PyramidTheme.fontSans, fontWeight: FontWeight.w500)),
|
|
if (widget.subtitle.isNotEmpty)
|
|
Text(widget.subtitle.toUpperCase(), style: TextStyle(color: fg.withOpacity(0.65), fontSize: 10, fontFamily: PyramidTheme.fontMono, letterSpacing: 1.2)),
|
|
])),
|
|
_OverlayBtn(icon: Icons.closed_caption_outlined, onTap: widget.onCaptions, onDark: onDark),
|
|
_OverlayBtn(icon: Icons.more_horiz, onTap: widget.onMore, onDark: onDark),
|
|
]),
|
|
);
|
|
}
|
|
|
|
// ─── Bottom bar (scrubber + controls) ──────────────────────────────
|
|
Widget _bottomBar({required bool onDark}) {
|
|
final iconColor = onDark ? Colors.white.withOpacity(0.85) : PyramidTheme.fgMuted;
|
|
final timeColor = onDark ? Colors.white.withOpacity(0.78) : PyramidTheme.fgDim;
|
|
|
|
return Container(
|
|
padding: const EdgeInsets.fromLTRB(16, 14, 16, 14),
|
|
decoration: BoxDecoration(
|
|
gradient: onDark
|
|
? LinearGradient(begin: Alignment.bottomCenter, end: Alignment.topCenter, colors: [Colors.black.withOpacity(0.75), Colors.transparent])
|
|
: null,
|
|
),
|
|
child: Column(mainAxisSize: MainAxisSize.min, children: [
|
|
_scrubber(onDark: onDark),
|
|
const SizedBox(height: 10),
|
|
Row(children: [
|
|
// Left side
|
|
_PlayMini(playing: widget.playing, onTap: widget.onTogglePlay, onDark: onDark),
|
|
const SizedBox(width: 4),
|
|
_OverlayBtn(icon: Icons.replay_10, onTap: widget.onSkipBack, onDark: onDark),
|
|
_OverlayBtn(icon: Icons.forward_10, onTap: widget.onSkipForward, onDark: onDark),
|
|
_OverlayBtn(icon: Icons.volume_up_outlined, onTap: null, onDark: onDark),
|
|
const SizedBox(width: 8),
|
|
Text('${_fmt(widget.position)} / ${_fmt(widget.duration)}',
|
|
style: TextStyle(color: timeColor, fontSize: 11, fontFamily: PyramidTheme.fontMono, letterSpacing: 0.4)),
|
|
const Spacer(),
|
|
// Right side
|
|
_QualityBadge(label: _quality == 'Auto' ? '1080p' : _quality, onDark: onDark),
|
|
const SizedBox(width: 4),
|
|
_SettingsButton(
|
|
open: _settingsOpen,
|
|
onTap: () => setState(() => _settingsOpen = !_settingsOpen),
|
|
speed: '${_speed == _speed.toInt() ? _speed.toInt() : _speed}x',
|
|
quality: _quality,
|
|
onDark: onDark,
|
|
onSpeed: (s) {
|
|
setState(() { _speed = s; _settingsOpen = false; });
|
|
widget.onSpeedChange?.call(s);
|
|
},
|
|
onQuality: (q) {
|
|
setState(() { _quality = q; _settingsOpen = false; });
|
|
widget.onQualityChange?.call(q);
|
|
},
|
|
speeds: _speeds, qualities: _qualities,
|
|
),
|
|
_OverlayBtn(icon: Icons.fullscreen, onTap: widget.onFullscreen, onDark: onDark),
|
|
]),
|
|
]),
|
|
);
|
|
}
|
|
|
|
Widget _scrubber({required bool onDark}) {
|
|
final trackBg = onDark ? Colors.white.withOpacity(0.18) : PyramidTheme.bg3;
|
|
final bufferedBg = onDark ? Colors.white.withOpacity(0.28) : PyramidTheme.borderStrong;
|
|
|
|
return LayoutBuilder(builder: (_, c) => GestureDetector(
|
|
onTapDown: (d) => widget.onSeek?.call(Duration(
|
|
milliseconds: (widget.duration.inMilliseconds * (d.localPosition.dx / c.maxWidth).clamp(0.0, 1.0)).round(),
|
|
)),
|
|
onPanUpdate: (d) => widget.onSeek?.call(Duration(
|
|
milliseconds: (widget.duration.inMilliseconds * (d.localPosition.dx / c.maxWidth).clamp(0.0, 1.0)).round(),
|
|
)),
|
|
child: SizedBox(
|
|
height: 14,
|
|
child: Stack(alignment: Alignment.centerLeft, children: [
|
|
Container(height: 3, decoration: BoxDecoration(color: trackBg, borderRadius: BorderRadius.circular(2))),
|
|
FractionallySizedBox(widthFactor: widget.buffered, child: Container(height: 3, decoration: BoxDecoration(color: bufferedBg, borderRadius: BorderRadius.circular(2)))),
|
|
FractionallySizedBox(widthFactor: progress, child: Container(height: 3, decoration: BoxDecoration(color: PyramidTheme.accent, borderRadius: BorderRadius.circular(2)))),
|
|
Positioned(
|
|
left: c.maxWidth * progress - 5,
|
|
child: Container(width: 10, height: 10, decoration: BoxDecoration(color: PyramidTheme.accent, shape: BoxShape.circle, boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.35), blurRadius: 0, spreadRadius: 3)])),
|
|
),
|
|
]),
|
|
),
|
|
));
|
|
}
|
|
}
|
|
|
|
// ───────────────────────── Overlay sub-widgets ─────────────────────────
|
|
|
|
class _CenterPlay extends StatelessWidget {
|
|
final VoidCallback? onTap;
|
|
const _CenterPlay({this.onTap});
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return GestureDetector(
|
|
onTap: onTap,
|
|
child: Container(
|
|
width: 64, height: 64,
|
|
decoration: BoxDecoration(
|
|
color: Colors.white.withOpacity(0.08),
|
|
shape: BoxShape.circle,
|
|
border: Border.all(color: Colors.white.withOpacity(0.15)),
|
|
),
|
|
child: const Icon(Icons.play_arrow, color: Colors.white, size: 28),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _OverlayBtn extends StatelessWidget {
|
|
final IconData icon;
|
|
final VoidCallback? onTap;
|
|
final bool onDark;
|
|
const _OverlayBtn({required this.icon, this.onTap, required this.onDark});
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return InkWell(
|
|
onTap: onTap,
|
|
borderRadius: BorderRadius.circular(PyramidTheme.rSm),
|
|
child: Container(
|
|
width: 32, height: 32,
|
|
alignment: Alignment.center,
|
|
child: Icon(icon, size: 17, color: onDark ? Colors.white.withOpacity(0.85) : PyramidTheme.fgMuted),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _PlayMini extends StatelessWidget {
|
|
final bool playing;
|
|
final VoidCallback? onTap;
|
|
final bool onDark;
|
|
const _PlayMini({required this.playing, this.onTap, required this.onDark});
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return GestureDetector(
|
|
onTap: onTap,
|
|
child: Container(
|
|
width: 36, height: 36,
|
|
decoration: BoxDecoration(
|
|
color: onDark ? Colors.white : PyramidTheme.accent,
|
|
shape: BoxShape.circle,
|
|
),
|
|
child: Icon(playing ? Icons.pause : Icons.play_arrow,
|
|
color: onDark ? const Color(0xFF0A0A0C) : PyramidTheme.accentFg, size: 16),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _QualityBadge extends StatelessWidget {
|
|
final String label;
|
|
final bool onDark;
|
|
const _QualityBadge({required this.label, required this.onDark});
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 3),
|
|
decoration: BoxDecoration(
|
|
color: onDark ? Colors.white.withOpacity(0.08) : PyramidTheme.bg2,
|
|
borderRadius: BorderRadius.circular(PyramidTheme.rSm),
|
|
),
|
|
child: Text(label.toUpperCase(),
|
|
style: TextStyle(
|
|
fontFamily: PyramidTheme.fontMono, fontSize: 10, fontWeight: FontWeight.w500, letterSpacing: 0.6,
|
|
color: onDark ? Colors.white.withOpacity(0.78) : PyramidTheme.fgMuted,
|
|
)),
|
|
);
|
|
}
|
|
}
|
|
|
|
// ───────────────────────── Settings menu ─────────────────────────
|
|
|
|
class _SettingsButton extends StatelessWidget {
|
|
final bool open;
|
|
final VoidCallback onTap;
|
|
final String speed;
|
|
final String quality;
|
|
final ValueChanged<double> onSpeed;
|
|
final ValueChanged<String> onQuality;
|
|
final List<double> speeds;
|
|
final List<String> qualities;
|
|
final bool onDark;
|
|
|
|
const _SettingsButton({
|
|
required this.open, required this.onTap,
|
|
required this.speed, required this.quality,
|
|
required this.onSpeed, required this.onQuality,
|
|
required this.speeds, required this.qualities,
|
|
required this.onDark,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return PopupMenuButton<void>(
|
|
tooltip: 'Einstellungen',
|
|
offset: const Offset(0, -12),
|
|
position: PopupMenuPosition.over,
|
|
color: onDark ? const Color(0xFF14141A).withOpacity(0.96) : PyramidTheme.bg2,
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(PyramidTheme.rBase),
|
|
side: BorderSide(color: onDark ? Colors.white.withOpacity(0.1) : PyramidTheme.border),
|
|
),
|
|
itemBuilder: (ctx) => [
|
|
PopupMenuItem<void>(
|
|
enabled: false,
|
|
padding: EdgeInsets.zero,
|
|
child: _SettingsMenu(
|
|
speed: speed, quality: quality,
|
|
onSpeed: onSpeed, onQuality: onQuality,
|
|
speeds: speeds, qualities: qualities,
|
|
onDark: onDark,
|
|
),
|
|
),
|
|
],
|
|
child: Container(
|
|
width: 32, height: 32,
|
|
alignment: Alignment.center,
|
|
child: Icon(Icons.settings_outlined, size: 17, color: onDark ? Colors.white.withOpacity(0.85) : PyramidTheme.fgMuted),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _SettingsMenu extends StatefulWidget {
|
|
final String speed;
|
|
final String quality;
|
|
final ValueChanged<double> onSpeed;
|
|
final ValueChanged<String> onQuality;
|
|
final List<double> speeds;
|
|
final List<String> qualities;
|
|
final bool onDark;
|
|
|
|
const _SettingsMenu({
|
|
required this.speed, required this.quality,
|
|
required this.onSpeed, required this.onQuality,
|
|
required this.speeds, required this.qualities,
|
|
required this.onDark,
|
|
});
|
|
@override
|
|
State<_SettingsMenu> createState() => _SettingsMenuState();
|
|
}
|
|
|
|
class _SettingsMenuState extends State<_SettingsMenu> {
|
|
String? section; // null | 'speed' | 'quality'
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final fg = widget.onDark ? Colors.white.withOpacity(0.9) : PyramidTheme.fg;
|
|
final dim = widget.onDark ? Colors.white.withOpacity(0.6) : PyramidTheme.fgMuted;
|
|
|
|
Widget row(String label, String value, VoidCallback onTap) => InkWell(
|
|
onTap: onTap,
|
|
child: Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
|
|
child: Row(mainAxisSize: MainAxisSize.min, children: [
|
|
Text(label, style: TextStyle(color: fg, fontSize: 12, fontFamily: PyramidTheme.fontSans)),
|
|
const SizedBox(width: 24),
|
|
Text(value, style: TextStyle(color: dim, fontSize: 11, fontFamily: PyramidTheme.fontMono)),
|
|
const SizedBox(width: 4),
|
|
Icon(Icons.chevron_right, size: 14, color: dim),
|
|
]),
|
|
),
|
|
);
|
|
|
|
Widget item(String label, bool active, VoidCallback onTap) => InkWell(
|
|
onTap: onTap,
|
|
child: Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
|
child: Row(mainAxisSize: MainAxisSize.min, children: [
|
|
SizedBox(width: 18, child: active ? Icon(Icons.check, size: 14, color: PyramidTheme.accent) : null),
|
|
Text(label, style: TextStyle(
|
|
color: active ? PyramidTheme.accent : fg,
|
|
fontSize: 12,
|
|
fontFamily: section == 'speed' ? PyramidTheme.fontMono : PyramidTheme.fontSans,
|
|
)),
|
|
]),
|
|
),
|
|
);
|
|
|
|
if (section == null) {
|
|
return SizedBox(
|
|
width: 200,
|
|
child: Column(mainAxisSize: MainAxisSize.min, children: [
|
|
row('Geschwindigkeit', widget.speed, () => setState(() => section = 'speed')),
|
|
row('Qualität', widget.quality, () => setState(() => section = 'quality')),
|
|
]),
|
|
);
|
|
}
|
|
|
|
final isSpeed = section == 'speed';
|
|
return SizedBox(
|
|
width: 180,
|
|
child: Column(mainAxisSize: MainAxisSize.min, children: [
|
|
InkWell(
|
|
onTap: () => setState(() => section = null),
|
|
child: Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8),
|
|
child: Row(mainAxisSize: MainAxisSize.min, children: [
|
|
Icon(Icons.arrow_back, size: 12, color: dim),
|
|
const SizedBox(width: 8),
|
|
Text(isSpeed ? 'GESCHWINDIGKEIT' : 'QUALITÄT',
|
|
style: TextStyle(color: dim, fontSize: 10, fontFamily: PyramidTheme.fontMono, letterSpacing: 0.8)),
|
|
]),
|
|
),
|
|
),
|
|
Container(height: 1, color: widget.onDark ? Colors.white.withOpacity(0.08) : PyramidTheme.border),
|
|
if (isSpeed)
|
|
...widget.speeds.map((s) {
|
|
final lbl = '${s == s.toInt() ? s.toInt() : s}x';
|
|
return item(lbl, lbl == widget.speed, () => widget.onSpeed(s));
|
|
})
|
|
else
|
|
...widget.qualities.map((q) => item(q, q == widget.quality, () => widget.onQuality(q))),
|
|
]),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _BigTriPainter extends CustomPainter {
|
|
@override
|
|
void paint(Canvas canvas, Size s) {
|
|
final p = Paint()..color = PyramidTheme.accent;
|
|
final path = Path()
|
|
..moveTo(s.width / 2, 0)
|
|
..lineTo(s.width, s.height)
|
|
..lineTo(0, s.height)
|
|
..close();
|
|
canvas.drawPath(path, p);
|
|
}
|
|
@override
|
|
bool shouldRepaint(_) => false;
|
|
}
|