Rebuild the Flutter/DJI-MSDK-V4 Fly App to the v2 UI kit and wire the full SDK surface behind it. Native (Kotlin): split DjiSdkBridge into a method/event router delegating to per-subsystem SubBridge helpers sharing a BridgeCtx — FlightController (takeoff/land/RTH + rich telemetry), Camera (mode/record/photo/exposure), Gimbal, Mission (Waypoint + ActiveTrack; QuickShots via ActiveTrack QUICK_SHOT), Media (MediaManager list/thumbnail/download), and optional DJI account login. Manifest gains scoped media permissions. Flutter: ten screens under lib/ui/ (Flight HUD, capture modes, camera settings, settings menu, map+waypoints, home, album, academy, profile, routes/flight logs), driven by an expanded FlightModel. New PVIcon renders the kit's SVG paths via flutter_svg; map uses flutter_map + latlong2. Pin transitive androidx.core/browser down to SDK-35-compatible versions so the newer plugins don't force AGP 8.9.1 onto the DJI toolchain. Verified with `flutter build apk --debug` (compiles Dart + all Kotlin); runtime behaviour is untested here — it needs a physical DJI-connected device. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
105 lines
5.1 KiB
Dart
105 lines
5.1 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:url_launcher/url_launcher.dart';
|
|
|
|
import '../theme.dart';
|
|
import 'pv_icons.dart';
|
|
|
|
/// Academy — mirrors the v2 "Academy" mockup. A featured card plus a list of
|
|
/// tutorial / manual / tips entries that open reference material in the browser.
|
|
class AcademyPage extends StatelessWidget {
|
|
const AcademyPage({super.key});
|
|
|
|
static const List<(String, String, String, String)> _cards = <(String, String, String, String)>[
|
|
('academy', 'Flight tutorials', '6 lessons', 'https://www.dji.com/dk/dji-fly'),
|
|
('shield', 'Product manuals', 'Air 3 · RC 2', 'https://www.pix-pro.com/blog/dji-fly-guide-part1'),
|
|
('sun', 'Flight tips', 'Wind & weather', 'https://www.skyzr.com/en/dji/dji-fly-app/the-ultimate-dji-fly-app-guide/'),
|
|
];
|
|
|
|
Future<void> _open(BuildContext context, String url) async {
|
|
final Uri uri = Uri.parse(url);
|
|
if (!await launchUrl(uri, mode: LaunchMode.externalApplication)) {
|
|
if (context.mounted) {
|
|
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Could not open $url')));
|
|
}
|
|
}
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final PVScheme s = PVScheme.of(context);
|
|
return Scaffold(
|
|
backgroundColor: s.bgApp,
|
|
body: SafeArea(
|
|
child: ListView(
|
|
children: <Widget>[
|
|
Padding(
|
|
padding: const EdgeInsets.fromLTRB(12, 6, 20, 14),
|
|
child: Row(children: <Widget>[
|
|
IconButton(onPressed: () => Navigator.of(context).maybePop(), icon: PVIcon('chevronLeft', size: 24, color: s.textSecondary)),
|
|
Text('Academy', style: TextStyle(fontFamily: PV.fontSans, fontSize: 20, fontWeight: FontWeight.w700, letterSpacing: -0.4, color: s.textPrimary)),
|
|
]),
|
|
),
|
|
GestureDetector(
|
|
onTap: () => _open(context, 'https://www.dji.com/dk/dji-fly'),
|
|
child: Container(
|
|
height: 132,
|
|
margin: const EdgeInsets.fromLTRB(20, 0, 20, 16),
|
|
decoration: BoxDecoration(
|
|
borderRadius: BorderRadius.circular(16),
|
|
border: Border.all(color: s.border),
|
|
gradient: const LinearGradient(begin: Alignment.topLeft, end: Alignment.bottomRight, colors: <Color>[Color(0xFF26406E), Color(0xFF0F1E3D)]),
|
|
),
|
|
child: Stack(children: <Widget>[
|
|
const Positioned(
|
|
left: 16, bottom: 16, right: 64,
|
|
child: Column(crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: <Widget>[
|
|
Text('GETTING STARTED', style: TextStyle(fontFamily: PV.fontMono, fontSize: 10, letterSpacing: 1.4, color: Color(0xB3EAF0FA))),
|
|
SizedBox(height: 3),
|
|
Text('Your first flight in 5 minutes', style: TextStyle(fontFamily: PV.fontSans, fontSize: 17, fontWeight: FontWeight.w700, color: Color(0xFFEAF0FA))),
|
|
]),
|
|
),
|
|
Positioned(
|
|
top: 16, right: 16,
|
|
child: Container(
|
|
width: 40, height: 40,
|
|
decoration: const BoxDecoration(color: Color(0x29FFFFFF), shape: BoxShape.circle),
|
|
alignment: Alignment.center,
|
|
child: const PVIcon('play', size: 18, color: Colors.white, fill: true),
|
|
),
|
|
),
|
|
]),
|
|
),
|
|
),
|
|
for (final (String ic, String t, String sub, String url) in _cards)
|
|
Padding(
|
|
padding: const EdgeInsets.fromLTRB(20, 0, 20, 10),
|
|
child: GestureDetector(
|
|
onTap: () => _open(context, url),
|
|
child: Container(
|
|
height: 62,
|
|
padding: const EdgeInsets.symmetric(horizontal: 14),
|
|
decoration: BoxDecoration(color: s.surface, borderRadius: BorderRadius.circular(14), border: Border.all(color: s.border), boxShadow: s.shadowXs),
|
|
child: Row(children: <Widget>[
|
|
Container(
|
|
width: 38, height: 38,
|
|
decoration: BoxDecoration(color: s.accentSoft, borderRadius: BorderRadius.circular(10)),
|
|
alignment: Alignment.center,
|
|
child: PVIcon(ic, size: 19, color: s.accentSoftFg),
|
|
),
|
|
const SizedBox(width: 12),
|
|
Expanded(child: Column(crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[
|
|
Text(t, style: TextStyle(fontFamily: PV.fontSans, fontSize: 14.5, fontWeight: FontWeight.w600, color: s.textPrimary)),
|
|
Text(sub, style: TextStyle(fontFamily: PV.fontMono, fontSize: 11, color: s.textSecondary)),
|
|
])),
|
|
PVIcon('chevronRight', size: 18, color: s.textTertiary),
|
|
]),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|