added cargo files
This commit is contained in:
128
PinePods-0.8.2/mobile/lib/ui/settings/bottom_bar_order.dart
Normal file
128
PinePods-0.8.2/mobile/lib/ui/settings/bottom_bar_order.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
183
PinePods-0.8.2/mobile/lib/ui/settings/episode_refresh.dart
Normal file
183
PinePods-0.8.2/mobile/lib/ui/settings/episode_refresh.dart
Normal 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');
|
||||
}
|
||||
}
|
||||
311
PinePods-0.8.2/mobile/lib/ui/settings/pinepods_login.dart
Normal file
311
PinePods-0.8.2/mobile/lib/ui/settings/pinepods_login.dart
Normal 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();
|
||||
}
|
||||
}
|
||||
118
PinePods-0.8.2/mobile/lib/ui/settings/search_provider.dart
Normal file
118
PinePods-0.8.2/mobile/lib/ui/settings/search_provider.dart
Normal 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();
|
||||
});
|
||||
}
|
||||
}
|
||||
339
PinePods-0.8.2/mobile/lib/ui/settings/settings.dart
Normal file
339
PinePods-0.8.2/mobile/lib/ui/settings/settings.dart
Normal 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,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user