new settings design

master
gitea 2 months ago
parent 2db3339071
commit ffd1ac5ddc

@ -28,6 +28,7 @@ class RelayManagementScreen extends StatefulWidget {
class _RelayManagementScreenState extends State<RelayManagementScreen> {
final TextEditingController _urlController = TextEditingController();
final FocusNode _urlFocusNode = FocusNode();
bool _useNip05RelaysAutomatically = false;
bool _isDarkMode = false;
bool _isLoadingSetting = true;
@ -37,8 +38,6 @@ class _RelayManagementScreenState extends State<RelayManagementScreen> {
MultiMediaService? _multiMediaService;
List<MediaServerConfig> _mediaServers = [];
MediaServerConfig? _defaultMediaServer;
bool _mediaServersExpanded = false;
bool _settingsExpanded = true; // Settings expanded by default
// Store original values to detect changes
List<MediaServerConfig> _originalMediaServers = [];
@ -51,6 +50,16 @@ class _RelayManagementScreenState extends State<RelayManagementScreen> {
super.initState();
_initializeMultiMediaService();
_loadSetting();
_urlFocusNode.addListener(_onUrlFocusChanged);
}
void _onUrlFocusChanged() {
if (_urlFocusNode.hasFocus && _urlController.text.isEmpty) {
_urlController.text = 'wss://';
_urlController.selection = TextSelection.fromPosition(
TextPosition(offset: _urlController.text.length),
);
}
}
Future<void> _initializeMultiMediaService() async {
@ -71,7 +80,6 @@ class _RelayManagementScreenState extends State<RelayManagementScreen> {
_defaultMediaServer = _multiMediaService!.getDefaultServer();
_originalMediaServers = List.from(_mediaServers);
_originalDefaultServerId = _defaultMediaServer?.id;
_mediaServersExpanded = _mediaServers.isNotEmpty; // Auto-expand if servers exist
});
}
}
@ -133,6 +141,8 @@ class _RelayManagementScreenState extends State<RelayManagementScreen> {
@override
void dispose() {
_urlController.dispose();
_urlFocusNode.removeListener(_onUrlFocusChanged);
_urlFocusNode.dispose();
super.dispose();
}
@ -399,7 +409,7 @@ class _RelayManagementScreenState extends State<RelayManagementScreen> {
(s) => s.isDefault,
orElse: () => updatedServers.isNotEmpty ? updatedServers.first : result,
);
_mediaServersExpanded = true; // Expand to show the newly added server
// Server added successfully
});
Logger.info('Media server added. Total servers: ${_mediaServers.length}, Default: ${_defaultMediaServer?.baseUrl}');
@ -495,274 +505,142 @@ class _RelayManagementScreenState extends State<RelayManagementScreen> {
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Scaffold(
appBar: AppBar(
title: const Text('Advanced Settings'),
actions: _hasUnsavedChanges
? [
TextButton.icon(
onPressed: _saveAllSettings,
icon: const Icon(Icons.save, size: 18),
label: const Text('Save'),
style: TextButton.styleFrom(
foregroundColor: Colors.green,
),
),
]
: null,
),
body: ListenableBuilder(
listenable: widget.controller,
builder: (context, child) {
return SingleChildScrollView(
child: Column(
children: [
// Settings section - Now using ExpansionTile for uniformity
ExpansionTile(
title: const Text('Settings'),
subtitle: _hasUnsavedChanges
? const Text('You have unsaved changes', style: TextStyle(color: Colors.orange))
: Text('${_useNip05RelaysAutomatically ? "NIP-05" : "Manual"} relays • ${_isDarkMode ? "Dark" : "Light"} mode'),
initiallyExpanded: _settingsExpanded,
trailing: _hasUnsavedChanges
? ElevatedButton.icon(
onPressed: _saveAllSettings,
icon: const Icon(Icons.save, size: 18),
label: const Text('Save'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.green,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
),
)
: null,
onExpansionChanged: (expanded) {
setState(() {
_settingsExpanded = expanded;
});
},
children: [
Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (_isLoadingSetting)
const Row(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
SizedBox(
width: 16,
height: 16,
child: CircularProgressIndicator(strokeWidth: 2),
),
SizedBox(width: 12),
Text('Loading settings...'),
],
// Preferences Section
_buildSectionCard(
context: context,
title: 'Preferences',
icon: Icons.settings_outlined,
child: _isLoadingSetting
? const Padding(
padding: EdgeInsets.symmetric(vertical: 8),
child: Center(child: CircularProgressIndicator(strokeWidth: 2)),
)
else ...[
SwitchListTile(
title: const Text('Use NIP-05 relays automatically'),
subtitle: const Text(
'Automatically replace relays with NIP-05 preferred relays upon login',
style: TextStyle(fontSize: 12),
),
value: _useNip05RelaysAutomatically,
onChanged: (value) {
_saveSetting('use_nip05_relays_automatically', value);
},
contentPadding: EdgeInsets.zero,
),
const SizedBox(height: 8),
SwitchListTile(
title: const Text('Dark Mode'),
subtitle: const Text(
'Enable dark theme for the app',
style: TextStyle(fontSize: 12),
),
: Column(
children: [
_buildSettingTile(
context: context,
title: 'Dark Mode',
subtitle: 'Enable dark theme for the app',
value: _isDarkMode,
onChanged: (value) {
_saveSetting('dark_mode', value);
},
contentPadding: EdgeInsets.zero,
icon: Icons.dark_mode_outlined,
),
const SizedBox(height: 16),
// Media Server Settings - Multiple Servers
],
),
),
const SizedBox(height: 8),
// Media Server Section
Builder(
builder: (context) {
final immichEnabled = dotenv.env['IMMICH_ENABLE']?.toLowerCase() != 'false';
final defaultBlossomServer = dotenv.env['BLOSSOM_SERVER'] ?? 'https://media.based21.com';
return ExpansionTile(
key: ValueKey('media-servers-${_mediaServers.length}-${_mediaServersExpanded}'),
title: const Text('Media Servers'),
subtitle: Text(_mediaServers.isEmpty
? 'No servers configured'
: '${_mediaServers.length} server(s)${_defaultMediaServer != null ? " • Default: ${_defaultMediaServer!.name ?? _defaultMediaServer!.type}" : ""}'),
initiallyExpanded: _mediaServersExpanded,
onExpansionChanged: (expanded) {
setState(() {
_mediaServersExpanded = expanded;
});
},
return _buildSectionCard(
context: context,
title: 'Media Server',
icon: Icons.storage_outlined,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// Server list
if (_mediaServers.isEmpty)
Padding(
padding: const EdgeInsets.all(16.0),
child: Text(
'No media servers configured. Add one to get started.',
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Colors.grey,
),
),
if (_defaultMediaServer != null)
_buildMediaServerTile(
context: context,
server: _defaultMediaServer!,
isDefault: true,
onEdit: () => _editMediaServer(_defaultMediaServer!),
onRemove: () => _removeMediaServer(_defaultMediaServer!),
)
else
...List.generate(_mediaServers.length, (index) {
final server = _mediaServers[index];
return ListTile(
key: ValueKey(server.id),
leading: Icon(
server.isDefault ? Icons.star : Icons.star_border,
color: server.isDefault ? Colors.amber : Colors.grey,
),
title: Text(server.name ?? '${server.type.toUpperCase()} Server'),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(server.baseUrl),
if (server.isDefault)
Padding(
padding: const EdgeInsets.only(top: 4.0),
child: Chip(
label: const Text('Default'),
labelStyle: const TextStyle(fontSize: 10),
padding: EdgeInsets.zero,
),
),
],
),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
icon: const Icon(Icons.edit),
onPressed: () => _editMediaServer(server),
padding: const EdgeInsets.symmetric(vertical: 4),
child: Text(
'No media server configured',
style: theme.textTheme.bodySmall?.copyWith(
color: Colors.grey,
fontSize: 12,
),
IconButton(
icon: const Icon(Icons.delete),
onPressed: () => _removeMediaServer(server),
),
],
),
onTap: () => _setDefaultServer(server),
);
}),
// Add server button
Padding(
padding: const EdgeInsets.all(16.0),
child: ElevatedButton.icon(
const SizedBox(height: 8),
OutlinedButton.icon(
onPressed: () => _addMediaServer(immichEnabled, defaultBlossomServer),
icon: const Icon(Icons.add),
label: const Text('Add Media Server'),
icon: const Icon(Icons.add, size: 16),
label: const Text('Add More', style: TextStyle(fontSize: 13)),
style: OutlinedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 12),
minimumSize: const Size(0, 36),
),
),
],
),
);
},
),
],
],
),
),
],
),
// Error message
if (widget.controller.error != null)
Container(
width: double.infinity,
padding: const EdgeInsets.all(16),
color: Colors.red.shade100,
child: Row(
children: [
Icon(Icons.error, color: Colors.red.shade700),
const SizedBox(width: 8),
Expanded(
child: Text(
widget.controller.error!,
style: TextStyle(color: Colors.red.shade700),
),
),
IconButton(
icon: const Icon(Icons.close),
onPressed: widget.controller.clearError,
color: Colors.red.shade700,
),
],
),
),
const SizedBox(height: 8),
// Relay Management Section (Expandable)
ExpansionTile(
title: const Text('Nostr Relays'),
subtitle: Text('${widget.controller.relays.length} relay(s) configured'),
initiallyExpanded: false,
children: [
Padding(
padding: const EdgeInsets.all(16),
// Relays Section
_buildSectionCard(
context: context,
title: 'Relays',
icon: Icons.cloud_outlined,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// Test All and Toggle All buttons
Row(
children: [
Expanded(
child: ElevatedButton.icon(
onPressed: widget.controller.isCheckingHealth
? null
: widget.controller.checkRelayHealth,
icon: widget.controller.isCheckingHealth
? const SizedBox(
width: 16,
height: 16,
child: CircularProgressIndicator(strokeWidth: 2),
)
: const Icon(Icons.network_check),
label: const Text('Test All'),
),
),
const SizedBox(width: 8),
Expanded(
child: ElevatedButton.icon(
onPressed: widget.controller.relays.isEmpty
? null
: () async {
final hasEnabled = widget.controller.relays.any((r) => r.isEnabled);
if (hasEnabled) {
await widget.controller.turnAllOff();
} else {
await widget.controller.turnAllOn();
}
},
icon: const Icon(Icons.power_settings_new),
label: Text(
widget.controller.relays.isNotEmpty &&
widget.controller.relays.any((r) => r.isEnabled)
? 'Turn All Off'
: 'Turn All On',
),
),
),
],
),
const SizedBox(height: 16),
// Add relay input
Row(
children: [
Expanded(
child: TextField(
TextField(
controller: _urlController,
focusNode: _urlFocusNode,
style: const TextStyle(fontSize: 14),
decoration: InputDecoration(
labelText: 'Relay URL',
hintText: 'wss://relay.example.com',
border: const OutlineInputBorder(),
prefixIcon: const Icon(Icons.link, size: 18),
contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12),
isDense: true,
),
keyboardType: TextInputType.url,
),
),
const SizedBox(width: 8),
const SizedBox(height: 8),
ElevatedButton.icon(
onPressed: () async {
final url = _urlController.text.trim();
String url = _urlController.text.trim();
// Remove wss:// prefix if user added it manually, we'll add it properly
if (url.startsWith('wss://')) {
url = url.substring(6);
}
if (url.isNotEmpty) {
final success = await widget.controller.addRelay(url);
final fullUrl = url.startsWith('wss://') ? url : 'wss://$url';
final success = await widget.controller.addRelay(fullUrl);
if (mounted) {
if (success) {
_urlController.clear();
@ -787,48 +665,49 @@ class _RelayManagementScreenState extends State<RelayManagementScreen> {
}
}
},
icon: const Icon(Icons.add),
label: const Text('Add'),
icon: const Icon(Icons.add, size: 16),
label: const Text('Add Relay', style: TextStyle(fontSize: 13)),
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 12),
minimumSize: const Size(0, 36),
),
],
),
const SizedBox(height: 16),
const SizedBox(height: 12),
// Relay list
SizedBox(
height: 300,
child: widget.controller.relays.isEmpty
? Center(
if (widget.controller.relays.isEmpty)
Padding(
padding: const EdgeInsets.symmetric(vertical: 16),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.cloud_off,
size: 48,
size: 36,
color: Colors.grey.shade400,
),
const SizedBox(height: 16),
const SizedBox(height: 8),
Text(
'No relays configured',
style: Theme.of(context).textTheme.titleMedium?.copyWith(
style: theme.textTheme.bodyMedium?.copyWith(
color: Colors.grey,
fontSize: 13,
),
),
const SizedBox(height: 8),
const SizedBox(height: 4),
Text(
'Add a relay to get started',
style: Theme.of(context).textTheme.bodySmall?.copyWith(
style: theme.textTheme.bodySmall?.copyWith(
color: Colors.grey,
fontSize: 11,
),
),
],
),
)
: ListView.builder(
shrinkWrap: true,
itemCount: widget.controller.relays.length,
itemBuilder: (context, index) {
final relay = widget.controller.relays[index];
return _RelayListItem(
else
...widget.controller.relays.map((relay) {
return Padding(
padding: const EdgeInsets.only(bottom: 6),
child: _RelayListItem(
relay: relay,
onToggle: () async {
await widget.controller.toggleRelay(relay.url);
@ -842,15 +721,47 @@ class _RelayManagementScreenState extends State<RelayManagementScreen> {
),
);
},
),
);
},
}).toList(),
],
),
),
],
// Error message
if (widget.controller.error != null) ...[
const SizedBox(height: 8),
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.red.shade50,
borderRadius: BorderRadius.circular(10),
border: Border.all(color: Colors.red.shade200),
),
child: Row(
children: [
Icon(Icons.error_outline, size: 16, color: Colors.red.shade700),
const SizedBox(width: 8),
Expanded(
child: Text(
widget.controller.error!,
style: TextStyle(
color: Colors.red.shade700,
fontSize: 12,
),
),
),
IconButton(
icon: const Icon(Icons.close, size: 16),
onPressed: widget.controller.clearError,
color: Colors.red.shade700,
padding: EdgeInsets.zero,
constraints: const BoxConstraints(minWidth: 24, minHeight: 24),
),
],
),
),
],
],
),
);
@ -858,6 +769,207 @@ class _RelayManagementScreenState extends State<RelayManagementScreen> {
),
);
}
Widget _buildSectionCard({
required BuildContext context,
required String title,
required IconData icon,
required Widget child,
}) {
final theme = Theme.of(context);
final isDark = theme.brightness == Brightness.dark;
return Card(
elevation: 0,
margin: EdgeInsets.zero,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
side: BorderSide(
color: isDark ? Colors.grey.shade800 : Colors.grey.shade200,
width: 1,
),
),
child: Padding(
padding: const EdgeInsets.all(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Container(
padding: const EdgeInsets.all(6),
decoration: BoxDecoration(
color: theme.primaryColor.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(6),
),
child: Icon(icon, color: theme.primaryColor, size: 16),
),
const SizedBox(width: 8),
Text(
title,
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w600,
fontSize: 15,
),
),
],
),
const SizedBox(height: 12),
child,
],
),
),
);
}
Widget _buildSettingTile({
required BuildContext context,
required String title,
required String subtitle,
required bool value,
required ValueChanged<bool> onChanged,
required IconData icon,
}) {
final theme = Theme.of(context);
return Row(
children: [
Icon(icon, size: 16, color: theme.primaryColor),
const SizedBox(width: 10),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: theme.textTheme.bodyMedium?.copyWith(
fontWeight: FontWeight.w500,
fontSize: 14,
),
),
const SizedBox(height: 2),
Text(
subtitle,
style: theme.textTheme.bodySmall?.copyWith(
color: Colors.grey,
fontSize: 11,
),
),
],
),
),
Transform.scale(
scale: 0.85,
child: Switch(
value: value,
onChanged: onChanged,
),
),
],
);
}
Widget _buildMediaServerTile({
required BuildContext context,
required MediaServerConfig server,
required bool isDefault,
required VoidCallback onEdit,
required VoidCallback onRemove,
}) {
final theme = Theme.of(context);
return Container(
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
color: theme.cardColor,
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: isDefault ? theme.primaryColor.withValues(alpha: 0.3) : Colors.grey.shade300,
width: isDefault ? 1.5 : 1,
),
),
child: Row(
children: [
Container(
padding: const EdgeInsets.all(5),
decoration: BoxDecoration(
color: theme.primaryColor.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(6),
),
child: Icon(
Icons.storage,
color: theme.primaryColor,
size: 16,
),
),
const SizedBox(width: 8),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Flexible(
child: Text(
server.name ?? '${server.type.toUpperCase()} Server',
style: theme.textTheme.bodyMedium?.copyWith(
fontWeight: FontWeight.w600,
fontSize: 13,
),
overflow: TextOverflow.ellipsis,
),
),
if (isDefault) ...[
const SizedBox(width: 6),
Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 1),
decoration: BoxDecoration(
color: theme.primaryColor.withValues(alpha: 0.2),
borderRadius: BorderRadius.circular(3),
),
child: Text(
'Default',
style: TextStyle(
fontSize: 9,
fontWeight: FontWeight.w600,
color: theme.primaryColor,
),
),
),
],
],
),
const SizedBox(height: 2),
Text(
server.baseUrl,
style: theme.textTheme.bodySmall?.copyWith(
color: Colors.grey,
fontSize: 11,
),
overflow: TextOverflow.ellipsis,
),
],
),
),
IconButton(
icon: const Icon(Icons.edit, size: 16),
onPressed: onEdit,
tooltip: 'Edit',
padding: EdgeInsets.zero,
constraints: const BoxConstraints(minWidth: 28, minHeight: 28),
),
IconButton(
icon: const Icon(Icons.delete_outline, size: 16),
onPressed: onRemove,
tooltip: 'Remove',
color: Colors.red,
padding: EdgeInsets.zero,
constraints: const BoxConstraints(minWidth: 28, minHeight: 28),
),
],
),
);
}
}
/// Widget for displaying a single relay in the list.
@ -879,10 +991,21 @@ class _RelayListItem extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Card(
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
final theme = Theme.of(context);
final isConnected = relay.isConnected && relay.isEnabled;
return Container(
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
color: theme.cardColor,
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: isConnected
? Colors.green.withValues(alpha: 0.3)
: Colors.grey.shade300,
width: isConnected ? 1.5 : 1,
),
),
child: Row(
children: [
// Status indicator
@ -891,12 +1014,19 @@ class _RelayListItem extends StatelessWidget {
height: 10,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: relay.isConnected && relay.isEnabled
? Colors.green
: Colors.grey,
color: isConnected ? Colors.green : Colors.grey,
boxShadow: isConnected
? [
BoxShadow(
color: Colors.green.withValues(alpha: 0.5),
blurRadius: 3,
spreadRadius: 1,
),
]
: null,
),
),
const SizedBox(width: 8),
const SizedBox(width: 10),
// URL and status text
Expanded(
child: Column(
@ -905,69 +1035,59 @@ class _RelayListItem extends StatelessWidget {
children: [
Text(
relay.url,
style: const TextStyle(
fontSize: 13,
style: theme.textTheme.bodyMedium?.copyWith(
fontWeight: FontWeight.w500,
fontSize: 13,
),
overflow: TextOverflow.ellipsis,
maxLines: 1,
),
const SizedBox(height: 2),
Row(
children: [
Icon(
isConnected ? Icons.check_circle : Icons.cancel,
size: 12,
color: isConnected ? Colors.green : Colors.grey,
),
const SizedBox(width: 3),
Text(
relay.isConnected && relay.isEnabled
? 'Connected'
: 'Disabled',
style: TextStyle(
isConnected ? 'Connected' : 'Disabled',
style: theme.textTheme.bodySmall?.copyWith(
color: isConnected ? Colors.green : Colors.grey,
fontWeight: FontWeight.w500,
fontSize: 11,
color: relay.isConnected && relay.isEnabled
? Colors.green
: Colors.grey,
),
),
],
),
],
),
),
const SizedBox(width: 8),
// Toggle switch
Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(
relay.isEnabled ? 'ON' : 'OFF',
style: TextStyle(
fontSize: 11,
fontWeight: FontWeight.w500,
color: relay.isEnabled
? Colors.green
: Colors.grey[600],
),
),
const SizedBox(width: 4),
Transform.scale(
scale: 0.8,
scale: 0.85,
child: Switch(
value: relay.isEnabled,
onChanged: (_) => onToggle(),
),
),
],
),
const SizedBox(width: 4),
// Remove button
IconButton(
icon: const Icon(Icons.delete, size: 18),
icon: const Icon(Icons.delete_outline, size: 16),
color: Colors.red,
tooltip: 'Remove',
onPressed: onRemove,
padding: EdgeInsets.zero,
constraints: const BoxConstraints(
minWidth: 32,
minHeight: 32,
minWidth: 28,
minHeight: 28,
),
),
],
),
),
);
}
}

Loading…
Cancel
Save

Powered by TurnKey Linux.