added cargo files

This commit is contained in:
2026-03-03 10:57:43 -05:00
parent 478a90e01b
commit 169df46bc2
813 changed files with 227273 additions and 9 deletions

View File

@@ -0,0 +1,128 @@
// Copyright 2020 Ben Hills and the project contributors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:pinepods_mobile/bloc/settings/settings_bloc.dart';
import 'package:pinepods_mobile/l10n/L.dart';
/// A widget that allows users to reorder the bottom navigation bar items
class BottomBarOrderWidget extends StatefulWidget {
const BottomBarOrderWidget({super.key});
@override
State<BottomBarOrderWidget> createState() => _BottomBarOrderWidgetState();
}
class _BottomBarOrderWidgetState extends State<BottomBarOrderWidget> {
late List<String> _currentOrder;
@override
void initState() {
super.initState();
final settingsBloc = Provider.of<SettingsBloc>(context, listen: false);
_currentOrder = List.from(settingsBloc.currentSettings.bottomBarOrder);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Reorganize Bottom Bar'),
actions: [
TextButton(
onPressed: () async {
final settingsBloc = Provider.of<SettingsBloc>(context, listen: false);
settingsBloc.setBottomBarOrder(_currentOrder);
// Show a brief confirmation
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Bottom bar order saved!'),
duration: Duration(seconds: 1),
),
);
// Small delay to let the user see the changes take effect
await Future.delayed(const Duration(milliseconds: 500));
if (mounted) {
Navigator.pop(context);
}
},
child: Text(
'Save',
style: TextStyle(color: Theme.of(context).colorScheme.primary),
),
),
],
),
body: Column(
children: [
Padding(
padding: const EdgeInsets.all(16.0),
child: Text(
'Drag and drop to reorder the bottom navigation items. The first items will be easier to access.',
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Theme.of(context).textTheme.bodySmall?.color,
),
),
),
Expanded(
child: ReorderableListView(
onReorder: (oldIndex, newIndex) {
setState(() {
if (newIndex > oldIndex) {
newIndex -= 1;
}
final item = _currentOrder.removeAt(oldIndex);
_currentOrder.insert(newIndex, item);
});
},
children: _currentOrder.map((item) {
return ListTile(
key: Key(item),
leading: Icon(_getIconForItem(item)),
title: Text(item),
trailing: const Icon(Icons.drag_handle),
);
}).toList(),
),
),
Padding(
padding: const EdgeInsets.all(16.0),
child: Row(
children: [
Expanded(
child: ElevatedButton(
onPressed: () {
setState(() {
_currentOrder = ['Home', 'Feed', 'Saved', 'Podcasts', 'Downloads', 'History', 'Playlists', 'Search'];
});
},
child: const Text('Reset to Default'),
),
),
],
),
),
],
),
);
}
IconData _getIconForItem(String item) {
switch (item) {
case 'Home': return Icons.home;
case 'Feed': return Icons.rss_feed;
case 'Saved': return Icons.bookmark;
case 'Podcasts': return Icons.podcasts;
case 'Downloads': return Icons.download;
case 'History': return Icons.history;
case 'Playlists': return Icons.playlist_play;
case 'Search': return Icons.search;
default: return Icons.home;
}
}
}

View File

@@ -0,0 +1,183 @@
// Copyright 2020 Ben Hills and the project contributors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:pinepods_mobile/bloc/settings/settings_bloc.dart';
import 'package:pinepods_mobile/entities/app_settings.dart';
import 'package:pinepods_mobile/l10n/L.dart';
import 'package:flutter/material.dart';
import 'package:flutter_dialogs/flutter_dialogs.dart';
import 'package:provider/provider.dart';
class EpisodeRefreshWidget extends StatefulWidget {
const EpisodeRefreshWidget({super.key});
@override
State<EpisodeRefreshWidget> createState() => _EpisodeRefreshWidgetState();
}
class _EpisodeRefreshWidgetState extends State<EpisodeRefreshWidget> {
@override
Widget build(BuildContext context) {
var settingsBloc = Provider.of<SettingsBloc>(context);
return StreamBuilder<AppSettings>(
stream: settingsBloc.settings,
initialData: AppSettings.sensibleDefaults(),
builder: (context, snapshot) {
return Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
ListTile(
title: Text(L.of(context)!.settings_auto_update_episodes),
subtitle: updateSubtitle(snapshot.data!),
onTap: () {
showPlatformDialog<void>(
context: context,
useRootNavigator: false,
builder: (BuildContext context) {
return AlertDialog(
title: Text(
L.of(context)!.settings_auto_update_episodes_heading,
style: Theme.of(context).textTheme.titleMedium,
textAlign: TextAlign.center,
),
scrollable: true,
content: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return Column(children: <Widget>[
RadioListTile<int>(
title: Text(L.of(context)!.settings_auto_update_episodes_never),
dense: true,
contentPadding: const EdgeInsets.symmetric(horizontal: 0.0),
value: -1,
groupValue: snapshot.data!.autoUpdateEpisodePeriod,
onChanged: (int? value) {
setState(() {
settingsBloc.autoUpdatePeriod(value ?? -1);
Navigator.pop(context);
});
},
),
RadioListTile<int>(
title: Text(L.of(context)!.settings_auto_update_episodes_always),
dense: true,
contentPadding: const EdgeInsets.symmetric(horizontal: 0.0),
value: 0,
groupValue: snapshot.data!.autoUpdateEpisodePeriod,
onChanged: (int? value) {
setState(() {
settingsBloc.autoUpdatePeriod(value ?? 0);
Navigator.pop(context);
});
},
),
RadioListTile<int>(
title: Text(L.of(context)!.settings_auto_update_episodes_30min),
dense: true,
contentPadding: const EdgeInsets.symmetric(horizontal: 0.0),
value: 30,
groupValue: snapshot.data!.autoUpdateEpisodePeriod,
onChanged: (int? value) {
setState(() {
settingsBloc.autoUpdatePeriod(value ?? 30);
Navigator.pop(context);
});
},
),
RadioListTile<int>(
title: Text(L.of(context)!.settings_auto_update_episodes_1hour),
dense: true,
contentPadding: const EdgeInsets.symmetric(horizontal: 0.0),
value: 60,
groupValue: snapshot.data!.autoUpdateEpisodePeriod,
onChanged: (int? value) {
setState(() {
settingsBloc.autoUpdatePeriod(value ?? 60);
Navigator.pop(context);
});
},
),
RadioListTile<int>(
title: Text(L.of(context)!.settings_auto_update_episodes_3hour),
dense: true,
contentPadding: const EdgeInsets.symmetric(horizontal: 0.0),
value: 180,
groupValue: snapshot.data!.autoUpdateEpisodePeriod,
onChanged: (int? value) {
setState(() {
settingsBloc.autoUpdatePeriod(value ?? 180);
Navigator.pop(context);
});
},
),
RadioListTile<int>(
title: Text(L.of(context)!.settings_auto_update_episodes_6hour),
dense: true,
contentPadding: const EdgeInsets.symmetric(horizontal: 0.0),
value: 360,
groupValue: snapshot.data!.autoUpdateEpisodePeriod,
onChanged: (int? value) {
setState(() {
settingsBloc.autoUpdatePeriod(value ?? 360);
Navigator.pop(context);
});
},
),
RadioListTile<int>(
title: Text(L.of(context)!.settings_auto_update_episodes_12hour),
dense: true,
contentPadding: const EdgeInsets.symmetric(horizontal: 0.0),
value: 720,
groupValue: snapshot.data!.autoUpdateEpisodePeriod,
onChanged: (int? value) {
setState(() {
settingsBloc.autoUpdatePeriod(value ?? 720);
Navigator.pop(context);
});
},
),
]);
},
));
},
);
},
),
],
);
});
}
Text updateSubtitle(AppSettings settings) {
switch (settings.autoUpdateEpisodePeriod) {
case -1:
return Text(L.of(context)!.settings_auto_update_episodes_never);
case 0:
return Text(L.of(context)!.settings_auto_update_episodes_always);
case 10:
return Text(L.of(context)!.settings_auto_update_episodes_10min);
case 30:
return Text(L.of(context)!.settings_auto_update_episodes_30min);
case 60:
return Text(L.of(context)!.settings_auto_update_episodes_1hour);
case 180:
return Text(L.of(context)!.settings_auto_update_episodes_3hour);
case 360:
return Text(L.of(context)!.settings_auto_update_episodes_6hour);
case 720:
return Text(L.of(context)!.settings_auto_update_episodes_12hour);
}
return const Text('Never');
}
}

View File

@@ -0,0 +1,311 @@
// lib/ui/settings/pinepods_login.dart
import 'package:flutter/material.dart';
import 'package:pinepods_mobile/bloc/settings/settings_bloc.dart';
import 'package:pinepods_mobile/l10n/L.dart';
import 'package:pinepods_mobile/services/pinepods/pinepods_service.dart';
import 'package:pinepods_mobile/services/pinepods/login_service.dart';
import 'package:pinepods_mobile/ui/widgets/restart_widget.dart';
import 'package:pinepods_mobile/ui/settings/settings_section_label.dart';
import 'package:provider/provider.dart';
import 'package:http/http.dart' as http;
import 'dart:convert';
class PinepodsLoginWidget extends StatefulWidget {
const PinepodsLoginWidget({Key? key}) : super(key: key);
@override
State<PinepodsLoginWidget> createState() => _PinepodsLoginWidgetState();
}
class _PinepodsLoginWidgetState extends State<PinepodsLoginWidget> {
final _serverController = TextEditingController();
final _usernameController = TextEditingController();
final _passwordController = TextEditingController();
final _mfaController = TextEditingController();
bool _isLoading = false;
bool _showMfaField = false;
String _errorMessage = '';
bool _isLoggedIn = false;
String? _connectedServer;
String? _tempServerUrl;
String? _tempUsername;
int? _tempUserId;
String? _tempMfaSessionToken;
@override
void initState() {
super.initState();
// Initialize UI based on saved settings
_loadSavedSettings();
}
void _loadSavedSettings() {
var settingsBloc = Provider.of<SettingsBloc>(context, listen: false);
var settings = settingsBloc.currentSettings;
// Check if we have PinePods settings
setState(() {
_isLoggedIn = false;
_connectedServer = null;
// We'll add these properties to AppSettings in the next step
if (settings.pinepodsServer != null &&
settings.pinepodsServer!.isNotEmpty &&
settings.pinepodsApiKey != null &&
settings.pinepodsApiKey!.isNotEmpty) {
_isLoggedIn = true;
_connectedServer = settings.pinepodsServer;
}
});
}
Future<void> _connectToPinepods() async {
if (!_showMfaField && (_serverController.text.isEmpty ||
_usernameController.text.isEmpty ||
_passwordController.text.isEmpty)) {
setState(() {
_errorMessage = 'Please fill in all fields';
});
return;
}
if (_showMfaField && _mfaController.text.isEmpty) {
setState(() {
_errorMessage = 'Please enter your MFA code';
});
return;
}
setState(() {
_isLoading = true;
_errorMessage = '';
});
try {
if (_showMfaField && _tempMfaSessionToken != null) {
// Complete MFA login flow
final mfaCode = _mfaController.text.trim();
final result = await PinepodsLoginService.completeMfaLogin(
serverUrl: _tempServerUrl!,
username: _tempUsername!,
mfaSessionToken: _tempMfaSessionToken!,
mfaCode: mfaCode,
);
if (result.isSuccess) {
// Save the connection details including user ID
var settingsBloc = Provider.of<SettingsBloc>(context, listen: false);
settingsBloc.setPinepodsServer(result.serverUrl!);
settingsBloc.setPinepodsApiKey(result.apiKey!);
settingsBloc.setPinepodsUserId(result.userId!);
setState(() {
_isLoggedIn = true;
_connectedServer = _tempServerUrl;
_showMfaField = false;
_tempServerUrl = null;
_tempUsername = null;
_tempUserId = null;
_tempMfaSessionToken = null;
_isLoading = false;
});
} else {
setState(() {
_errorMessage = result.errorMessage ?? 'MFA verification failed';
_isLoading = false;
});
}
} else {
// Initial login flow
final serverUrl = _serverController.text.trim();
final username = _usernameController.text.trim();
final password = _passwordController.text;
final result = await PinepodsLoginService.login(
serverUrl,
username,
password,
);
if (result.isSuccess) {
// Save the connection details including user ID
var settingsBloc = Provider.of<SettingsBloc>(context, listen: false);
settingsBloc.setPinepodsServer(result.serverUrl!);
settingsBloc.setPinepodsApiKey(result.apiKey!);
settingsBloc.setPinepodsUserId(result.userId!);
setState(() {
_isLoggedIn = true;
_connectedServer = serverUrl;
_isLoading = false;
});
} else if (result.requiresMfa) {
// Store MFA session info and show MFA field
setState(() {
_tempServerUrl = result.serverUrl;
_tempUsername = result.username;
_tempUserId = result.userId;
_tempMfaSessionToken = result.mfaSessionToken;
_showMfaField = true;
_isLoading = false;
_errorMessage = 'Please enter your MFA code';
});
} else {
setState(() {
_errorMessage = result.errorMessage ?? 'Login failed';
_isLoading = false;
});
}
}
} catch (e) {
setState(() {
_errorMessage = 'Error: ${e.toString()}';
_isLoading = false;
});
}
}
void _resetMfa() {
setState(() {
_showMfaField = false;
_tempServerUrl = null;
_tempUsername = null;
_tempUserId = null;
_tempMfaSessionToken = null;
_mfaController.clear();
_errorMessage = '';
});
}
void _logOut() async {
var settingsBloc = Provider.of<SettingsBloc>(context, listen: false);
// Clear all PinePods user data
settingsBloc.setPinepodsServer(null);
settingsBloc.setPinepodsApiKey(null);
settingsBloc.setPinepodsUserId(null);
settingsBloc.setPinepodsUsername(null);
settingsBloc.setPinepodsEmail(null);
setState(() {
_isLoggedIn = false;
_connectedServer = null;
});
// Wait for the settings to be processed and then restart the app
WidgetsBinding.instance.addPostFrameCallback((_) async {
await Future.delayed(const Duration(milliseconds: 100));
if (mounted) {
// Restart the entire app to reset all state
RestartWidget.restartApp(context);
}
});
}
@override
Widget build(BuildContext context) {
// Add a divider label for the PinePods section
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SettingsDividerLabel(label: 'PinePods Server'),
const Divider(),
if (_isLoggedIn) ...[
// Show connected status
ListTile(
title: const Text('PinePods Connection'),
subtitle: Text(_connectedServer ?? ''),
trailing: TextButton(
onPressed: _logOut,
child: const Text('Log Out'),
),
),
] else ...[
// Show login form
Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
TextField(
controller: _serverController,
decoration: const InputDecoration(
labelText: 'Server URL',
hintText: 'https://your-pinepods-server.com',
),
),
const SizedBox(height: 16),
TextField(
controller: _usernameController,
decoration: const InputDecoration(
labelText: 'Username',
),
),
const SizedBox(height: 16),
TextField(
controller: _passwordController,
decoration: const InputDecoration(
labelText: 'Password',
),
obscureText: true,
enabled: !_showMfaField,
),
// MFA Field (shown when MFA is required)
if (_showMfaField) ...[
const SizedBox(height: 16),
TextField(
controller: _mfaController,
decoration: InputDecoration(
labelText: 'MFA Code',
hintText: 'Enter 6-digit code',
suffixIcon: IconButton(
icon: const Icon(Icons.close),
onPressed: _resetMfa,
tooltip: 'Cancel MFA',
),
),
keyboardType: TextInputType.number,
maxLength: 6,
),
],
if (_errorMessage.isNotEmpty) ...[
const SizedBox(height: 16),
Text(
_errorMessage,
style: TextStyle(color: Theme.of(context).colorScheme.error),
),
],
const SizedBox(height: 16),
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: _isLoading ? null : _connectToPinepods,
child: _isLoading
? const SizedBox(
height: 20,
width: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
),
)
: Text(_showMfaField ? 'Verify MFA Code' : 'Connect'),
),
),
],
),
),
],
],
);
}
@override
void dispose() {
_serverController.dispose();
_usernameController.dispose();
_passwordController.dispose();
_mfaController.dispose();
super.dispose();
}
}

View File

@@ -0,0 +1,118 @@
// Copyright 2020 Ben Hills and the project contributors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:pinepods_mobile/bloc/settings/settings_bloc.dart';
import 'package:pinepods_mobile/entities/app_settings.dart';
import 'package:pinepods_mobile/l10n/L.dart';
import 'package:pinepods_mobile/ui/widgets/action_text.dart';
import 'package:flutter/material.dart';
import 'package:flutter_dialogs/flutter_dialogs.dart';
import 'package:provider/provider.dart';
class SearchProviderWidget extends StatefulWidget {
final ValueChanged<String?>? onChanged;
const SearchProviderWidget({
super.key,
this.onChanged,
});
@override
State<SearchProviderWidget> createState() => _SearchProviderWidgetState();
}
class _SearchProviderWidgetState extends State<SearchProviderWidget> {
@override
Widget build(BuildContext context) {
var settingsBloc = Provider.of<SettingsBloc>(context);
return StreamBuilder<AppSettings>(
stream: settingsBloc.settings,
initialData: AppSettings.sensibleDefaults(),
builder: (context, snapshot) {
return snapshot.data!.searchProviders.length > 1
? Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: [
ListTile(
title: Text(L.of(context)!.search_provider_label),
subtitle: Text(snapshot.data!.searchProvider == 'itunes' ? 'iTunes' : 'PodcastIndex'),
onTap: () {
showPlatformDialog<void>(
context: context,
useRootNavigator: false,
builder: (BuildContext context) {
return AlertDialog(
title: Semantics(
header: true,
child: Text(L.of(context)!.search_provider_label,
style: Theme.of(context).textTheme.titleMedium, textAlign: TextAlign.center),
),
content: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return Column(mainAxisSize: MainAxisSize.min, children: <Widget>[
RadioListTile<String>(
title: const Text('iTunes'),
value: 'itunes',
dense: true,
contentPadding: const EdgeInsets.symmetric(horizontal: 0.0),
groupValue: snapshot.data!.searchProvider,
onChanged: (String? value) {
setState(() {
settingsBloc.setSearchProvider(value ?? 'itunes');
if (widget.onChanged != null) {
widget.onChanged!(value);
}
Navigator.pop(context);
});
},
),
RadioListTile<String>(
title: const Text('PodcastIndex'),
value: 'podcastindex',
dense: true,
contentPadding: const EdgeInsets.symmetric(horizontal: 0.0),
groupValue: snapshot.data!.searchProvider,
onChanged: (String? value) {
setState(() {
settingsBloc.setSearchProvider(value ?? 'podcastindex');
if (widget.onChanged != null) {
widget.onChanged!(value);
}
Navigator.pop(context);
});
},
),
SimpleDialogOption(
padding: const EdgeInsets.symmetric(vertical: 16.0, horizontal: 24.0),
// child: Text(L.of(context)!.close_button_label),
child: Align(
alignment: Alignment.centerRight,
child: TextButton(
child: ActionText(L.of(context)!.close_button_label),
onPressed: () {
Navigator.pop(context, '');
},
),
),
),
]);
},
));
},
);
},
),
],
)
: Container();
});
}
}

View File

@@ -0,0 +1,339 @@
// Copyright 2020 Ben Hills and the project contributors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:pinepods_mobile/bloc/podcast/podcast_bloc.dart';
import 'package:pinepods_mobile/bloc/settings/settings_bloc.dart';
import 'package:pinepods_mobile/core/utils.dart';
import 'package:pinepods_mobile/entities/app_settings.dart';
import 'package:pinepods_mobile/l10n/L.dart';
import 'package:pinepods_mobile/ui/settings/episode_refresh.dart';
import 'package:pinepods_mobile/ui/settings/search_provider.dart';
import 'package:pinepods_mobile/ui/settings/settings_section_label.dart';
import 'package:pinepods_mobile/ui/settings/bottom_bar_order.dart';
import 'package:pinepods_mobile/ui/widgets/action_text.dart';
import 'package:pinepods_mobile/ui/settings/pinepods_login.dart';
import 'package:pinepods_mobile/ui/debug/debug_logs_page.dart';
import 'package:pinepods_mobile/ui/themes.dart';
import 'package:file_picker/file_picker.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_dialogs/flutter_dialogs.dart';
import 'package:provider/provider.dart';
/// This is the settings page and allows the user to select various
/// options for the app.
///
/// This is a self contained page and so, unlike the other forms, talks directly
/// to a settings service rather than a BLoC. Whilst this deviates slightly from
/// the overall architecture, adding a BLoC to simply be consistent with the rest
/// of the application would add unnecessary complexity.
///
/// This page is built with both Android & iOS in mind. However, the
/// rest of the application is not prepared for iOS design; this
/// is in preparation for the iOS version.
class Settings extends StatefulWidget {
const Settings({
super.key,
});
@override
State<Settings> createState() => _SettingsState();
}
class _SettingsState extends State<Settings> {
bool sdcard = false;
Widget _buildList(BuildContext context) {
var settingsBloc = Provider.of<SettingsBloc>(context);
var podcastBloc = Provider.of<PodcastBloc>(context);
return StreamBuilder<AppSettings>(
stream: settingsBloc.settings,
initialData: settingsBloc.currentSettings,
builder: (context, snapshot) {
return ListView(
children: [
SettingsDividerLabel(label: L.of(context)!.settings_personalisation_divider_label),
const Divider(),
MergeSemantics(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
L.of(context)!.settings_theme_switch_label,
style: Theme.of(context).textTheme.titleMedium,
),
const SizedBox(height: 8),
Text(
ThemeRegistry.getTheme(snapshot.data!.theme).description,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.7),
),
),
const SizedBox(height: 12),
Row(
children: [
const Icon(Icons.palette, size: 20),
const SizedBox(width: 12),
Expanded(
child: DropdownButton<String>(
value: snapshot.data!.theme,
isExpanded: true,
underline: Container(),
items: ThemeRegistry.themeList.map((theme) {
return DropdownMenuItem<String>(
value: theme.key,
child: Row(
children: [
Container(
width: 16,
height: 16,
decoration: BoxDecoration(
color: theme.isDark ? Colors.grey[800] : Colors.grey[200],
border: Border.all(
color: theme.themeData.colorScheme.primary,
width: 2,
),
borderRadius: BorderRadius.circular(8),
),
),
const SizedBox(width: 8),
Expanded(
child: Text(
theme.name,
overflow: TextOverflow.ellipsis,
),
),
],
),
);
}).toList(),
onChanged: (String? newTheme) {
if (newTheme != null) {
settingsBloc.setTheme(newTheme);
}
},
),
),
],
),
],
),
),
),
sdcard
? MergeSemantics(
child: ListTile(
title: Text(L.of(context)!.settings_download_sd_card_label),
trailing: Switch.adaptive(
value: snapshot.data!.storeDownloadsSDCard,
onChanged: (value) => sdcard
? setState(() {
if (value) {
_showStorageDialog(enableExternalStorage: true);
} else {
_showStorageDialog(enableExternalStorage: false);
}
settingsBloc.storeDownloadonSDCard(value);
})
: null,
),
),
)
: const SizedBox(
height: 0,
width: 0,
),
SettingsDividerLabel(label: 'Navigation'),
const Divider(),
ListTile(
title: const Text('Reorganize Bottom Bar'),
subtitle: const Text('Customize the order of bottom navigation items'),
leading: const Icon(Icons.reorder),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const BottomBarOrderWidget(),
),
);
},
),
SettingsDividerLabel(label: L.of(context)!.settings_playback_divider_label),
const Divider(),
MergeSemantics(
child: ListTile(
title: Text(L.of(context)!.settings_auto_open_now_playing),
trailing: Switch.adaptive(
value: snapshot.data!.autoOpenNowPlaying,
onChanged: (value) => setState(() => settingsBloc.setAutoOpenNowPlaying(value)),
),
),
),
const SearchProviderWidget(),
SettingsDividerLabel(label: 'Debug'),
const Divider(),
ListTile(
title: const Text('App Logs'),
subtitle: const Text('View debug logs and device information'),
leading: const Icon(Icons.bug_report),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const DebugLogsPage(),
),
);
},
),
const PinepodsLoginWidget(),
const _WebAppInfoWidget(),
],
);
});
}
Widget _buildAndroid(BuildContext context) {
return AnnotatedRegion<SystemUiOverlayStyle>(
value: Theme.of(context).appBarTheme.systemOverlayStyle!,
child: Scaffold(
appBar: AppBar(
elevation: 0.0,
title: Text(
L.of(context)!.settings_label,
),
),
body: _buildList(context),
),
);
}
Widget _buildIos(BuildContext context) {
return CupertinoPageScaffold(
navigationBar: CupertinoNavigationBar(
padding: const EdgeInsetsDirectional.all(0.0),
leading: CupertinoButton(
child: const Icon(Icons.arrow_back_ios),
onPressed: () {
Navigator.pop(context);
}),
middle: Text(
L.of(context)!.settings_label,
style: TextStyle(color: Theme.of(context).colorScheme.primary),
),
backgroundColor: Theme.of(context).colorScheme.surface,
),
child: Material(child: _buildList(context)),
);
}
void _showStorageDialog({required bool enableExternalStorage}) {
showPlatformDialog<void>(
context: context,
useRootNavigator: false,
builder: (_) => BasicDialogAlert(
title: Text(L.of(context)!.settings_download_switch_label),
content: Text(
enableExternalStorage
? L.of(context)!.settings_download_switch_card
: L.of(context)!.settings_download_switch_internal,
),
actions: <Widget>[
BasicDialogAction(
title: Text(
L.of(context)!.ok_button_label,
),
onPressed: () {
Navigator.pop(context);
},
),
],
),
);
}
@override
Widget build(context) {
switch (defaultTargetPlatform) {
case TargetPlatform.android:
return _buildAndroid(context);
case TargetPlatform.iOS:
return _buildIos(context);
default:
assert(false, 'Unexpected platform $defaultTargetPlatform');
return _buildAndroid(context);
}
}
@override
void initState() {
super.initState();
hasExternalStorage().then((value) {
setState(() {
sdcard = value;
});
});
}
}
class _WebAppInfoWidget extends StatelessWidget {
const _WebAppInfoWidget();
@override
Widget build(BuildContext context) {
return Consumer<SettingsBloc>(
builder: (context, settingsBloc, child) {
final settings = settingsBloc.currentSettings;
final serverUrl = settings.pinepodsServer;
// Only show if user is connected to a server
if (serverUrl == null || serverUrl.isEmpty) {
return const SizedBox.shrink();
}
return Padding(
padding: const EdgeInsets.all(16.0),
child: Card(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(
Icons.web,
color: Theme.of(context).primaryColor,
size: 20,
),
const SizedBox(width: 8),
Text(
'Web App Settings',
style: TextStyle(
fontWeight: FontWeight.bold,
color: Theme.of(context).primaryColor,
),
),
],
),
const SizedBox(height: 8),
Text(
'Many more server side and user settings available from the PinePods web app. Please head to $serverUrl to adjust much more',
style: Theme.of(context).textTheme.bodySmall,
),
],
),
),
),
);
},
);
}
}

View File

@@ -0,0 +1,33 @@
// Copyright 2020 Ben Hills and the project contributors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/material.dart';
class SettingsDividerLabel extends StatelessWidget {
final String label;
final EdgeInsetsGeometry padding;
const SettingsDividerLabel({
super.key,
required this.label,
this.padding = const EdgeInsets.fromLTRB(16.0, 24.0, 0.0, 0.0),
});
@override
Widget build(BuildContext context) {
return Padding(
padding: padding,
child: Semantics(
header: true,
child: Text(
label,
style: Theme.of(context).textTheme.titleSmall!.copyWith(
fontSize: 12.0,
color: Theme.of(context).primaryColor,
),
),
),
);
}
}