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
188 lines
5.0 KiB
Dart
188 lines
5.0 KiB
Dart
import 'dart:math' as math;
|
|
import 'dart:ui' as ui;
|
|
import 'package:flutter/material.dart';
|
|
|
|
// 3D Pyramid logo — two faces + base outline + spine
|
|
class PyramidLogo extends StatelessWidget {
|
|
final double size;
|
|
final Color color;
|
|
const PyramidLogo({super.key, this.size = 36, this.color = Colors.white});
|
|
|
|
@override
|
|
Widget build(BuildContext context) =>
|
|
CustomPaint(size: Size(size, size), painter: _PyramidLogoPainter(color: color));
|
|
}
|
|
|
|
class _PyramidLogoPainter extends CustomPainter {
|
|
final Color color;
|
|
const _PyramidLogoPainter({required this.color});
|
|
|
|
@override
|
|
void paint(Canvas canvas, Size size) {
|
|
final w = size.width;
|
|
final h = size.height;
|
|
final sx = w / 48;
|
|
final sy = h / 48;
|
|
Offset p(double x, double y) => Offset(x * sx, y * sy);
|
|
|
|
final apex = p(24, 6);
|
|
final baseL = p(6, 40);
|
|
final baseR = p(42, 40);
|
|
final baseM = p(24, 34);
|
|
|
|
// Left face — full opacity → 55%
|
|
final leftPath = Path()..moveTo(apex.dx, apex.dy)..lineTo(baseL.dx, baseL.dy)..lineTo(baseM.dx, baseM.dy)..close();
|
|
canvas.drawPath(leftPath, Paint()..shader = ui.Gradient.linear(
|
|
Offset(0, apex.dy), Offset(0, baseL.dy),
|
|
[color.withAlpha(255), color.withAlpha(140)],
|
|
));
|
|
|
|
// Right face — 85% → 25%
|
|
final rightPath = Path()..moveTo(apex.dx, apex.dy)..lineTo(baseR.dx, baseR.dy)..lineTo(baseM.dx, baseM.dy)..close();
|
|
canvas.drawPath(rightPath, Paint()..shader = ui.Gradient.linear(
|
|
Offset(apex.dx, apex.dy), Offset(baseR.dx, baseR.dy),
|
|
[color.withAlpha(217), color.withAlpha(64)],
|
|
));
|
|
|
|
// Base outline
|
|
canvas.drawPath(
|
|
Path()..moveTo(baseL.dx, baseL.dy)..lineTo(baseM.dx, baseM.dy)..lineTo(baseR.dx, baseR.dy),
|
|
Paint()..color = color.withAlpha(102)..style = PaintingStyle.stroke
|
|
..strokeWidth = math.max(sx, 0.7)..strokeJoin = StrokeJoin.round,
|
|
);
|
|
|
|
// Spine
|
|
canvas.drawLine(apex, baseM, Paint()..color = color.withAlpha(230)
|
|
..strokeWidth = math.max(sx * 0.8, 0.6));
|
|
}
|
|
|
|
@override
|
|
bool shouldRepaint(_PyramidLogoPainter old) => old.color != color;
|
|
}
|
|
|
|
// Pyramid mark — stylised stacked triangles
|
|
class PyramidMark extends StatelessWidget {
|
|
final double size;
|
|
final Color? color;
|
|
|
|
const PyramidMark({super.key, this.size = 30, this.color});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final c = color ?? Colors.white;
|
|
return SizedBox(
|
|
width: size,
|
|
height: size,
|
|
child: CustomPaint(painter: _PyramidPainter(c)),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _PyramidPainter extends CustomPainter {
|
|
final Color color;
|
|
_PyramidPainter(this.color);
|
|
|
|
@override
|
|
void paint(Canvas canvas, Size size) {
|
|
final paint = Paint()..color = color..style = PaintingStyle.fill;
|
|
final w = size.width;
|
|
final h = size.height;
|
|
|
|
// bottom big triangle
|
|
final big = Path()
|
|
..moveTo(w * 0.5, h * 0.1)
|
|
..lineTo(w * 0.92, h * 0.88)
|
|
..lineTo(w * 0.08, h * 0.88)
|
|
..close();
|
|
canvas.drawPath(big, paint);
|
|
|
|
// inner cutout (dark) to create layered look
|
|
final cut = Paint()
|
|
..color = Colors.black.withAlpha(80)
|
|
..style = PaintingStyle.fill;
|
|
final inner = Path()
|
|
..moveTo(w * 0.5, h * 0.28)
|
|
..lineTo(w * 0.74, h * 0.72)
|
|
..lineTo(w * 0.26, h * 0.72)
|
|
..close();
|
|
canvas.drawPath(inner, cut);
|
|
}
|
|
|
|
@override
|
|
bool shouldRepaint(_PyramidPainter old) => old.color != color;
|
|
}
|
|
|
|
// Pyramid space glyph (letter + triangle overlay)
|
|
class PyramidGlyph extends StatelessWidget {
|
|
final double size;
|
|
final Color color;
|
|
final String label;
|
|
|
|
const PyramidGlyph({
|
|
super.key,
|
|
this.size = 30,
|
|
required this.color,
|
|
required this.label,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return SizedBox(
|
|
width: size,
|
|
height: size,
|
|
child: Stack(
|
|
alignment: Alignment.center,
|
|
children: [
|
|
CustomPaint(
|
|
size: Size(size, size),
|
|
painter: _PyramidPainter(color),
|
|
),
|
|
Text(
|
|
label.isNotEmpty ? label[0].toUpperCase() : '?',
|
|
style: TextStyle(
|
|
color: Colors.white,
|
|
fontSize: size * 0.38,
|
|
fontWeight: FontWeight.bold,
|
|
height: 1,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
// Presence indicator dot
|
|
class PresenceDot extends StatelessWidget {
|
|
final String status; // online, away, busy, offline
|
|
final double size;
|
|
final Color borderColor;
|
|
|
|
const PresenceDot({
|
|
super.key,
|
|
required this.status,
|
|
this.size = 10,
|
|
this.borderColor = const Color(0xFF111114),
|
|
});
|
|
|
|
Color get _color => switch (status) {
|
|
'online' => const Color(0xFF4ADE80),
|
|
'away' => const Color(0xFFFACC15),
|
|
'busy' => const Color(0xFFF87171),
|
|
_ => const Color(0xFF6F6F7D),
|
|
};
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Container(
|
|
width: size,
|
|
height: size,
|
|
decoration: BoxDecoration(
|
|
color: _color,
|
|
shape: BoxShape.circle,
|
|
border: Border.all(color: borderColor, width: 2),
|
|
),
|
|
);
|
|
}
|
|
}
|