Add API Server (Go/PocketBase), Web App (Go BFF + Vue), Fly App (Flutter/DJI MSDK), Adobe Plugin, and Docker/Docker AIO deployment configs. Design assets and build artifacts are gitignored. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
146 lines
4.9 KiB
Dart
146 lines
4.9 KiB
Dart
import 'package:flutter/material.dart';
|
|
|
|
import '../theme.dart';
|
|
|
|
/// Portrait media grid — mirrors the ui_kit/fly "Album" mockup. Media is
|
|
/// placeholder content (the app has no on-device gallery source yet); the
|
|
/// layout, filters and badges match the design.
|
|
class AlbumPage extends StatefulWidget {
|
|
const AlbumPage({super.key});
|
|
|
|
@override
|
|
State<AlbumPage> createState() => _AlbumPageState();
|
|
}
|
|
|
|
class _AlbumPageState extends State<AlbumPage> {
|
|
static const List<String> _filters = <String>['All', 'Photos', 'Videos', 'Pano'];
|
|
int _active = 0;
|
|
|
|
// (isVideo, duration) — placeholder set from the mockup.
|
|
static const List<(bool, String?)> _media = <(bool, String?)>[
|
|
(true, '0:24'), (false, null), (false, null),
|
|
(true, '1:12'), (false, null), (false, null),
|
|
(false, null), (true, '0:08'), (false, null),
|
|
(false, null), (true, '0:31'), (false, null),
|
|
];
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final PVScheme s = PVScheme.of(context);
|
|
return Scaffold(
|
|
backgroundColor: s.bgApp,
|
|
body: SafeArea(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: <Widget>[
|
|
_header(s),
|
|
_filterBar(s),
|
|
const SizedBox(height: 14),
|
|
Expanded(child: _grid(s)),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _header(PVScheme s) {
|
|
return Padding(
|
|
padding: const EdgeInsets.fromLTRB(12, 6, 20, 12),
|
|
child: Row(
|
|
children: <Widget>[
|
|
IconButton(
|
|
onPressed: () => Navigator.of(context).maybePop(),
|
|
icon: Icon(Icons.chevron_left, size: 26, color: s.textSecondary),
|
|
),
|
|
Text(
|
|
'Album',
|
|
style: TextStyle(fontFamily: PV.fontSans, fontSize: 20, fontWeight: FontWeight.w700, letterSpacing: -0.4, color: s.textPrimary),
|
|
),
|
|
const Spacer(),
|
|
Text('${_media.length} items', style: TextStyle(fontFamily: PV.fontMono, fontSize: 12, color: s.textTertiary)),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _filterBar(PVScheme s) {
|
|
return SizedBox(
|
|
height: 28,
|
|
child: ListView.separated(
|
|
scrollDirection: Axis.horizontal,
|
|
padding: const EdgeInsets.symmetric(horizontal: 20),
|
|
itemCount: _filters.length,
|
|
separatorBuilder: (_, __) => const SizedBox(width: 8),
|
|
itemBuilder: (BuildContext context, int i) {
|
|
final bool active = i == _active;
|
|
return GestureDetector(
|
|
onTap: () => setState(() => _active = i),
|
|
child: Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 12),
|
|
alignment: Alignment.center,
|
|
decoration: BoxDecoration(
|
|
color: active ? s.accent : s.surfaceInset,
|
|
borderRadius: BorderRadius.circular(999),
|
|
),
|
|
child: Text(
|
|
_filters[i],
|
|
style: TextStyle(
|
|
fontFamily: PV.fontSans,
|
|
fontSize: 12.5,
|
|
fontWeight: FontWeight.w600,
|
|
color: active ? Colors.white : s.textSecondary,
|
|
),
|
|
),
|
|
),
|
|
);
|
|
},
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _grid(PVScheme s) {
|
|
return GridView.builder(
|
|
padding: const EdgeInsets.fromLTRB(20, 0, 20, 20),
|
|
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
|
crossAxisCount: 3,
|
|
crossAxisSpacing: 6,
|
|
mainAxisSpacing: 6,
|
|
),
|
|
itemCount: _media.length,
|
|
itemBuilder: (BuildContext context, int i) {
|
|
final (bool isVideo, String? dur) = _media[i];
|
|
return ClipRRect(
|
|
borderRadius: BorderRadius.circular(10),
|
|
child: Container(
|
|
decoration: BoxDecoration(
|
|
gradient: LinearGradient(
|
|
begin: Alignment.topLeft,
|
|
end: Alignment.bottomRight,
|
|
colors: <Color>[s.surface2, s.surfaceInset],
|
|
transform: GradientRotation((140 + i * 14) * 3.1415926 / 180),
|
|
),
|
|
border: Border.all(color: s.border),
|
|
borderRadius: BorderRadius.circular(10),
|
|
),
|
|
child: Stack(
|
|
children: <Widget>[
|
|
Center(child: Icon(isVideo ? Icons.videocam_outlined : Icons.image_outlined, size: 20, color: s.textTertiary)),
|
|
if (dur != null)
|
|
Positioned(
|
|
right: 6,
|
|
bottom: 5,
|
|
child: Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 1),
|
|
decoration: BoxDecoration(color: const Color(0xB30B1730), borderRadius: BorderRadius.circular(5)),
|
|
child: Text(dur, style: const TextStyle(fontFamily: PV.fontMono, fontSize: 9.5, color: Colors.white)),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
}
|