Remove the old PocketBase backup from the Fly App

backup_pocketbase_app/ held the pre-API-Server login and PocketBase service
from June; nothing uses it, and its imports made `flutter analyze` report 20
errors. It stays in git history.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
tajniak81andClaude Opus 5.5 committed 2026-10-22 14:43:51 +02:00
1 parent 8f259339fc
commit 66c3dbcd71
4 files changed
-894

No files matched your search

@@ -1,146 +0,0 @@
import 'package:flutter/material.dart';
import 'pb_service.dart';
class LoginPage extends StatefulWidget {
const LoginPage({super.key, required this.onSignedIn});
final VoidCallback onSignedIn;
@override
State<LoginPage> createState() => _LoginPageState();
}
class _LoginPageState extends State<LoginPage> {
late final TextEditingController _server =
TextEditingController(text: pb.defaultBaseUrl);
final TextEditingController _email =
TextEditingController(text: 'pilot@dji.local');
final TextEditingController _password = TextEditingController();
final TextEditingController _device =
TextEditingController(text: 'phone');
bool _busy = false;
String? _error;
Future<void> _signIn() async {
setState(() {
_busy = true;
_error = null;
});
try {
await pb.signIn(
baseUrl: _server.text,
email: _email.text,
password: _password.text,
deviceId: _device.text,
);
widget.onSignedIn();
} catch (e) {
setState(() => _error = _friendly(e));
} finally {
if (mounted) setState(() => _busy = false);
}
}
String _friendly(Object e) {
final String s = e.toString();
if (s.contains('Failed host lookup') || s.contains('SocketException')) {
return 'Cannot reach the server. Check the address and that PocketBase is running.';
}
if (s.contains('Failed to authenticate')) return 'Invalid email or password.';
return s;
}
@override
void dispose() {
_server.dispose();
_email.dispose();
_password.dispose();
_device.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: SingleChildScrollView(
padding: const EdgeInsets.all(24),
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 380),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
Icon(Icons.flight_takeoff,
size: 56, color: Theme.of(context).colorScheme.primary),
const SizedBox(height: 12),
Text('DJI MSDK Sample',
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.headlineSmall),
Text('Sign in to the PocketBase backend',
textAlign: TextAlign.center,
style: TextStyle(color: Colors.grey.shade600)),
const SizedBox(height: 24),
TextField(
controller: _server,
keyboardType: TextInputType.url,
decoration: const InputDecoration(
labelText: 'Server', hintText: '10.2.1.101:8090',
border: OutlineInputBorder(), prefixIcon: Icon(Icons.dns),
),
),
const SizedBox(height: 12),
TextField(
controller: _email,
keyboardType: TextInputType.emailAddress,
autofillHints: const <String>[AutofillHints.username],
decoration: const InputDecoration(
labelText: 'Email', border: OutlineInputBorder(),
prefixIcon: Icon(Icons.person),
),
),
const SizedBox(height: 12),
TextField(
controller: _password,
obscureText: true,
autofillHints: const <String>[AutofillHints.password],
onSubmitted: (_) => _busy ? null : _signIn(),
decoration: const InputDecoration(
labelText: 'Password', border: OutlineInputBorder(),
prefixIcon: Icon(Icons.lock),
),
),
const SizedBox(height: 12),
TextField(
controller: _device,
decoration: const InputDecoration(
labelText: 'Device id', border: OutlineInputBorder(),
prefixIcon: Icon(Icons.smartphone),
),
),
if (_error != null) ...<Widget>[
const SizedBox(height: 12),
Text(_error!, style: const TextStyle(color: Colors.red)),
],
const SizedBox(height: 20),
FilledButton(
onPressed: _busy ? null : _signIn,
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 12),
child: _busy
? const SizedBox(
height: 20, width: 20,
child: CircularProgressIndicator(strokeWidth: 2))
: const Text('Sign in'),
),
),
],
),
),
),
),
);
}
}
@@ -1,424 +0,0 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'dji_service.dart';
import 'login_page.dart';
import 'pb_service.dart';
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await pb.init();
runApp(const DjiSampleApp());
}
class DjiSampleApp extends StatelessWidget {
const DjiSampleApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'DJI MSDK Sample',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: const Color(0xFF1565C0)),
useMaterial3: true,
),
home: const AuthGate(),
);
}
}
/// Shows the login screen until the user is authenticated with PocketBase.
class AuthGate extends StatefulWidget {
const AuthGate({super.key});
@override
State<AuthGate> createState() => _AuthGateState();
}
class _AuthGateState extends State<AuthGate> {
StreamSubscription<BackendStatus>? _sub;
@override
void initState() {
super.initState();
_sub = pb.status.listen((_) {
if (mounted) setState(() {});
});
}
@override
void dispose() {
_sub?.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
if (pb.isAuthed) {
return const HomePage();
}
return LoginPage(onSignedIn: () => setState(() {}));
}
}
enum RegistrationState { idle, registering, success, failed }
class HomePage extends StatefulWidget {
const HomePage({super.key});
@override
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
final DjiService _dji = DjiService();
StreamSubscription<Map<String, dynamic>>? _sub;
StreamSubscription<BackendStatus>? _backendSub;
BackendStatus _backend = BackendStatus.online;
String _sdkVersion = '…';
RegistrationState _registration = RegistrationState.idle;
String? _registrationError;
bool _connected = false;
String? _model;
int? _satellites;
bool? _isFlying;
String? _flightMode;
double? _altitude;
double? _latitude;
double? _longitude;
int? _batteryPercent;
@override
void initState() {
super.initState();
_backend = pb.currentStatus;
// Route server interactions through PocketBase.
pb.onCommand = _handleServerCommand;
pb.stateProvider = _currentState;
_backendSub = pb.status.listen((BackendStatus s) {
if (mounted) setState(() => _backend = s);
});
_init();
}
Future<void> _init() async {
_sub = _dji.events().listen(_onEvent, onError: (Object e) {
_snack('Event channel error: $e');
});
try {
final String version = await _dji.getSdkVersion();
if (mounted) setState(() => _sdkVersion = version);
} catch (_) {
if (mounted) setState(() => _sdkVersion = 'unavailable');
}
}
void _onEvent(Map<String, dynamic> event) {
if (!mounted) return;
// Persist every event to PocketBase (telemetry is throttled inside).
pb.onEvent(event);
switch (event['type'] as String?) {
case 'registration':
setState(() {
switch (event['state'] as String?) {
case 'registering':
_registration = RegistrationState.registering;
_registrationError = null;
break;
case 'success':
_registration = RegistrationState.success;
break;
case 'failed':
_registration = RegistrationState.failed;
_registrationError = event['error'] as String?;
break;
}
});
break;
case 'connection':
setState(() {
_connected = event['connected'] as bool? ?? false;
_model = event['model'] as String?;
if (!_connected) _clearTelemetry();
});
break;
case 'telemetry':
setState(() {
_satellites = event['satelliteCount'] as int?;
_isFlying = event['isFlying'] as bool?;
_flightMode = event['flightMode'] as String?;
_altitude = (event['altitude'] as num?)?.toDouble();
_latitude = (event['latitude'] as num?)?.toDouble();
_longitude = (event['longitude'] as num?)?.toDouble();
});
break;
case 'battery':
setState(() => _batteryPercent = event['percent'] as int?);
break;
}
}
void _clearTelemetry() {
_satellites = null;
_isFlying = null;
_flightMode = null;
_altitude = null;
_latitude = null;
_longitude = null;
_batteryPercent = null;
}
/// Current app state shared with PocketBase for the device presence record.
Map<String, dynamic> _currentState() => <String, dynamic>{
'connected': _connected,
'model': _model ?? '',
'registration': _registrationWire(),
};
String _registrationWire() {
switch (_registration) {
case RegistrationState.success:
return 'success';
case RegistrationState.registering:
return 'registering';
case RegistrationState.failed:
return 'failed';
case RegistrationState.idle:
return 'not registered';
}
}
Future<void> _register() async {
try {
await _dji.registerApp();
} catch (e) {
_snack('registerApp failed: $e');
}
}
Future<void> _connect() async {
try {
final bool started = await _dji.startConnection();
_snack(started ? 'Scanning for product…' : 'Could not start connection');
} catch (e) {
_snack('startConnection failed: $e');
}
}
void _handleServerCommand(String command, Map<String, dynamic> payload) {
switch (command) {
case 'registerApp':
_register();
break;
case 'startConnection':
_connect();
break;
case 'stopConnection':
_dji.stopConnection();
break;
default:
_snack('Unknown server command: $command');
return;
}
_snack('Server command: $command');
}
Future<void> _logout() async {
await pb.signOut();
if (mounted) {
Navigator.of(context).pushAndRemoveUntil(
MaterialPageRoute<void>(
builder: (_) => const AuthGate(),
),
(Route<dynamic> route) => false,
);
}
}
void _snack(String msg) {
if (!mounted) return;
ScaffoldMessenger.of(context)
..clearSnackBars()
..showSnackBar(SnackBar(content: Text(msg)));
}
@override
void dispose() {
_sub?.cancel();
_backendSub?.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('DJI MSDK Sample'),
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
actions: <Widget>[
IconButton(
tooltip: 'Log out',
onPressed: _logout,
icon: const Icon(Icons.logout),
),
],
),
body: ListView(
padding: const EdgeInsets.all(16),
children: <Widget>[
_backendCard(),
const SizedBox(height: 12),
_registrationCard(),
const SizedBox(height: 12),
_connectionCard(),
const SizedBox(height: 12),
_telemetryCard(),
],
),
);
}
Widget _backendCard() {
final (Color color, String label) = switch (_backend) {
BackendStatus.online => (Colors.green, 'Streaming to PocketBase'),
BackendStatus.signingIn => (Colors.orange, 'Connecting…'),
BackendStatus.error => (Colors.red, 'Backend error'),
BackendStatus.signedOut => (Colors.grey, 'Signed out'),
};
return Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Row(
children: <Widget>[
Icon(Icons.cloud_done, color: color),
const SizedBox(width: 10),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(label, style: Theme.of(context).textTheme.titleMedium),
const SizedBox(height: 2),
Text('${pb.userEmail} · device "${pb.deviceId}"',
style: const TextStyle(color: Colors.black54, fontSize: 12)),
],
),
),
TextButton.icon(
onPressed: _logout,
icon: const Icon(Icons.logout),
label: const Text('Log out'),
),
],
),
),
);
}
Widget _registrationCard() {
final (Color color, IconData icon, String label) = switch (_registration) {
RegistrationState.idle => (Colors.grey, Icons.help_outline, 'Not registered'),
RegistrationState.registering => (Colors.orange, Icons.sync, 'Registering…'),
RegistrationState.success => (Colors.green, Icons.verified, 'Registered'),
RegistrationState.failed => (Colors.red, Icons.error_outline, 'Registration failed'),
};
return Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Row(
children: <Widget>[
Icon(icon, color: color),
const SizedBox(width: 8),
Text(label, style: Theme.of(context).textTheme.titleMedium),
],
),
const SizedBox(height: 4),
Text('SDK version: $_sdkVersion'),
if (_registrationError != null) ...<Widget>[
const SizedBox(height: 4),
Text(_registrationError!, style: const TextStyle(color: Colors.red)),
],
const SizedBox(height: 12),
FilledButton.icon(
onPressed: _registration == RegistrationState.registering ? null : _register,
icon: const Icon(Icons.app_registration),
label: const Text('Register app'),
),
],
),
),
);
}
Widget _connectionCard() {
return Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Row(
children: <Widget>[
Icon(_connected ? Icons.link : Icons.link_off,
color: _connected ? Colors.green : Colors.grey),
const SizedBox(width: 8),
Text(_connected ? 'Product connected' : 'No product',
style: Theme.of(context).textTheme.titleMedium),
],
),
const SizedBox(height: 4),
Text('Model: ${_model ?? '—'}'),
const SizedBox(height: 12),
OutlinedButton.icon(
onPressed: _registration == RegistrationState.success ? _connect : null,
icon: const Icon(Icons.usb),
label: const Text('Connect to product'),
),
],
),
),
);
}
Widget _telemetryCard() {
return Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text('Telemetry', style: Theme.of(context).textTheme.titleMedium),
const Divider(),
_row('Battery', _batteryPercent == null ? '—' : '$_batteryPercent%'),
_row('GPS satellites', _satellites?.toString() ?? '—'),
_row('Flight mode', _flightMode ?? '—'),
_row('Flying', _isFlying == null ? '—' : (_isFlying! ? 'yes' : 'no')),
_row('Altitude', _altitude == null ? '—' : '${_altitude!.toStringAsFixed(1)} m'),
_row('Latitude', _latitude?.toStringAsFixed(6) ?? '—'),
_row('Longitude', _longitude?.toStringAsFixed(6) ?? '—'),
],
),
),
);
}
Widget _row(String label, String value) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 4),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
Text(label, style: const TextStyle(color: Colors.black54)),
Text(value, style: const TextStyle(fontWeight: FontWeight.w600)),
],
),
);
}
}
@@ -1,230 +0,0 @@
import 'dart:async';
import 'package:pocketbase/pocketbase.dart';
import 'package:shared_preferences/shared_preferences.dart';
enum BackendStatus { signedOut, signingIn, online, error }
/// PocketBase-backed service: authentication, telemetry persistence, device
/// presence, and inbound command subscription. A single global [pb] instance is
/// shared across the app.
class PbService {
PocketBase? _pb;
SharedPreferences? _prefs;
String _deviceId = 'phone';
Timer? _heartbeat;
String? _devicesRecordId;
UnsubscribeFunc? _cmdUnsub;
DateTime _lastTelemetry = DateTime.fromMillisecondsSinceEpoch(0);
/// Called when a command record targeting this device is created.
void Function(String command, Map<String, dynamic> payload)? onCommand;
/// Returns the app's current state for the device presence record.
/// Expected keys: connected (bool), model (String), registration (String).
Map<String, dynamic> Function()? stateProvider;
final StreamController<BackendStatus> _statusController =
StreamController<BackendStatus>.broadcast();
Stream<BackendStatus> get status => _statusController.stream;
BackendStatus _status = BackendStatus.signedOut;
BackendStatus get currentStatus => _status;
bool get isAuthed => _pb?.authStore.isValid ?? false;
String get userEmail => _pb?.authStore.record?.getStringValue('email') ?? '';
String get deviceId => _deviceId;
String get baseUrl => _pb?.baseURL ?? '';
void _setStatus(BackendStatus s) {
_status = s;
if (!_statusController.isClosed) _statusController.add(s);
}
/// Restores a persisted session (if any) on app start.
Future<void> init() async {
_prefs = await SharedPreferences.getInstance();
_deviceId = _prefs!.getString('pb_device') ?? 'phone';
final String? url = _prefs!.getString('pb_url');
final String? auth = _prefs!.getString('pb_auth');
if (url != null && auth != null && auth.isNotEmpty) {
_pb = _build(url, auth);
if (_pb!.authStore.isValid) {
_setStatus(BackendStatus.online);
await _afterAuth();
}
}
}
PocketBase _build(String url, String? initialAuth) {
final AsyncAuthStore store = AsyncAuthStore(
save: (String data) async => _prefs?.setString('pb_auth', data),
clear: () async => _prefs?.remove('pb_auth'),
initial: initialAuth,
);
return PocketBase(url, authStore: store);
}
String get defaultBaseUrl => _prefs?.getString('pb_url') ?? 'http://10.2.1.101:8090';
Future<void> signIn({
required String baseUrl,
required String email,
required String password,
String deviceId = 'phone',
}) async {
_deviceId = deviceId.trim().isEmpty ? 'phone' : deviceId.trim();
final String url = _normalize(baseUrl);
_setStatus(BackendStatus.signingIn);
try {
final PocketBase pb = _build(url, null);
await pb.collection('users').authWithPassword(email.trim(), password);
_pb = pb;
await _prefs?.setString('pb_url', url);
await _prefs?.setString('pb_device', _deviceId);
_setStatus(BackendStatus.online);
await _afterAuth();
} catch (e) {
_setStatus(BackendStatus.error);
rethrow;
}
}
Future<void> signOut() async {
_heartbeat?.cancel();
_heartbeat = null;
try {
await _upsertDevice(online: false);
} catch (_) {}
try {
await _cmdUnsub?.call();
} catch (_) {}
_cmdUnsub = null;
_devicesRecordId = null;
_pb?.authStore.clear();
_setStatus(BackendStatus.signedOut);
}
Future<void> _afterAuth() async {
await _upsertDevice(online: true);
_startHeartbeat();
await _subscribeCommands();
}
String _normalize(String url) {
String u = url.trim();
if (!u.startsWith('http://') && !u.startsWith('https://')) u = 'http://$u';
return u;
}
// ── Telemetry ingestion ────────────────────────────────────────────────────
/// Feed an event from `DjiService.events()`.
void onEvent(Map<String, dynamic> event) {
final PocketBase? pb = _pb;
if (pb == null || !pb.authStore.isValid) return;
final String? type = event['type'] as String?;
if (type == 'telemetry') {
final DateTime now = DateTime.now();
if (now.difference(_lastTelemetry) < const Duration(milliseconds: 500)) return;
_lastTelemetry = now;
}
_appendEvent(type ?? 'event', event);
// Reflect registration/connection promptly in the presence record.
if (type == 'registration' || type == 'connection') {
_upsertDevice(online: true);
}
}
Future<void> _appendEvent(String kind, Map<String, dynamic> event) async {
final PocketBase? pb = _pb;
if (pb == null) return;
try {
await pb.collection('telemetry').create(body: <String, dynamic>{
'device': _deviceId,
'kind': kind,
'payload': event,
'owner': pb.authStore.record?.id,
});
} catch (_) {
// best-effort; drop on transient failure
}
}
// ── Device presence ─────────────────────────────────────────────────────────
void _startHeartbeat() {
_heartbeat?.cancel();
_heartbeat = Timer.periodic(const Duration(seconds: 6), (_) => _upsertDevice(online: true));
}
Future<void> _upsertDevice({required bool online}) async {
final PocketBase? pb = _pb;
if (pb == null) return;
final Map<String, dynamic> st = stateProvider?.call() ?? <String, dynamic>{};
final Map<String, dynamic> body = <String, dynamic>{
'device': _deviceId,
'online': online,
'connected': st['connected'] ?? false,
'model': st['model'] ?? '',
'registration': st['registration'] ?? '',
'lastSeen': DateTime.now().millisecondsSinceEpoch ~/ 1000,
'owner': pb.authStore.record?.id,
};
try {
if (_devicesRecordId == null) {
try {
final RecordModel existing =
await pb.collection('devices').getFirstListItem('device="$_deviceId"');
_devicesRecordId = existing.id;
} catch (_) {
// none yet
}
}
if (_devicesRecordId == null) {
final RecordModel rec = await pb.collection('devices').create(body: body);
_devicesRecordId = rec.id;
} else {
await pb.collection('devices').update(_devicesRecordId!, body: body);
}
} catch (_) {
_devicesRecordId = null; // record may have been removed; recreate next tick
}
}
// ── Commands ────────────────────────────────────────────────────────────────
Future<void> _subscribeCommands() async {
final PocketBase? pb = _pb;
if (pb == null) return;
try {
await _cmdUnsub?.call();
} catch (_) {}
_cmdUnsub = await pb.collection('commands').subscribe(
'*',
(RecordSubscriptionEvent e) {
if (e.action != 'create') return;
final RecordModel? rec = e.record;
if (rec == null) return;
if (rec.getStringValue('device') != _deviceId) return;
final String cmd = rec.getStringValue('command');
final Map<String, dynamic> payload =
(rec.get<Map<String, dynamic>?>('payload') ?? <String, dynamic>{});
if (cmd.isNotEmpty) onCommand?.call(cmd, payload);
},
filter: 'device="$_deviceId"',
);
}
void dispose() {
_heartbeat?.cancel();
_statusController.close();
}
}
/// App-wide PocketBase service instance.
final PbService pb = PbService();
@@ -1,94 +0,0 @@
name: pilotvault_fly
description: "DJI Mobile SDK V4 sample app built with Flutter"
# The following line prevents the package from being accidentally published to
# pub.dev using `flutter pub publish`. This is preferred for private packages.
publish_to: 'none' # Remove this line if you wish to publish to pub.dev
# The following defines the version and build number for your application.
# A version number is three numbers separated by dots, like 1.2.43
# followed by an optional build number separated by a +.
# Both the version and the builder number may be overridden in flutter
# build by specifying --build-name and --build-number, respectively.
# In Android, build-name is used as versionName while build-number used as versionCode.
# Read more about Android versioning at https://developer.android.com/studio/publish/versioning
# In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion.
# Read more about iOS versioning at
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
# In Windows, build-name is used as the major, minor, and patch parts
# of the product and file versions while build-number is used as the build suffix.
version: 1.0.0+1
environment:
sdk: ^3.6.2
# Dependencies specify other packages that your package needs in order to work.
# To automatically upgrade your package dependencies to the latest versions
# consider running `flutter pub upgrade --major-versions`. Alternatively,
# dependencies can be manually updated by changing the version numbers below to
# the latest version available on pub.dev. To see which dependencies have newer
# versions available, run `flutter pub outdated`.
dependencies:
flutter:
sdk: flutter
# The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^1.0.8
# PocketBase backend client (auth, records, realtime).
pocketbase: ^0.22.0
# Persists the auth token across app launches.
shared_preferences: ^2.3.2
dev_dependencies:
flutter_test:
sdk: flutter
# The "flutter_lints" package below contains a set of recommended lints to
# encourage good coding practices. The lint set provided by the package is
# activated in the `analysis_options.yaml` file located at the root of your
# package. See that file for information about deactivating specific lint
# rules and activating additional ones.
flutter_lints: ^5.0.0
# For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec
# The following section is specific to Flutter packages.
flutter:
# The following line ensures that the Material Icons font is
# included with your application, so that you can use the icons in
# the material Icons class.
uses-material-design: true
# To add assets to your application, add an assets section, like this:
# assets:
# - images/a_dot_burr.jpeg
# - images/a_dot_ham.jpeg
# An image asset can refer to one or more resolution-specific "variants", see
# https://flutter.dev/to/resolution-aware-images
# For details regarding adding assets from package dependencies, see
# https://flutter.dev/to/asset-from-package
# To add custom fonts to your application, add a fonts section here,
# in this "flutter" section. Each entry in this list should have a
# "family" key with the font family name, and a "fonts" key with a
# list giving the asset and other descriptors for the font. For
# example:
# fonts:
# - family: Schyler
# fonts:
# - asset: fonts/Schyler-Regular.ttf
# - asset: fonts/Schyler-Italic.ttf
# style: italic
# - family: Trajan Pro
# fonts:
# - asset: fonts/TrajanPro.ttf
# - asset: fonts/TrajanPro_Bold.ttf
# weight: 700
#
# For details regarding fonts from package dependencies,
# see https://flutter.dev/to/font-from-package