import 'package:capstone_project/providers/settings.dart'; import 'package:capstone_project/widgets/navbar.dart'; import 'package:file_picker/file_picker.dart'; import 'package:go_router/go_router.dart'; import 'package:path_provider/path_provider.dart'; import 'package:shadcn_flutter/shadcn_flutter.dart'; class SettingsPage extends StatefulWidget { static GoRoute route = GoRoute( path: "/settings", builder: (context, state) => SettingsPage() ); @override State createState() => _SettingsPageState(); } class _SettingsPageState extends State { bool _isLoading = true; String apiKey = ""; List feeds = []; String appStorageLocation = ""; @override void initState() { // TODO: implement initState super.initState(); loadPage(); } void loadPage() async { SettingsProvider settings = SettingsProvider.of(context); setState(() { _isLoading = true; }); apiKey = settings.openAIApiKey; feeds = settings.feeds; appStorageLocation = settings.applicationStorageLocation; setState(() { _isLoading = false; }); } @override Widget build(BuildContext context) { if (_isLoading) { return Scaffold( child: Center( child: CircularProgressIndicator(), ), ); } return Scaffold( headers: [ AppBar( title: Text("Settings"), ) ], footers: [ ProjNavBar( currentPage: "settings", ) ], child: Center( child: SizedBox( width: 600, child: SingleChildScrollView( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( "Open AI" ).h4, Gap(12), Text( "API Key" ).small.normal, Gap(8), TextField( placeholder: Text( "Enter your OpenAI API key" ), initialValue: apiKey.substring(0, 8) + "xxxxxx (Redacted for security)" , onChanged: (value) { SettingsProvider settings = SettingsProvider.of(context); settings.setOpenAIApiKey(value); }, ), Gap(8), if (apiKey.isEmpty) DestructiveBadge( child: Text( "API key is required to use AI features." ), ), Gap(16), Divider(), Gap(16), Row( children: [ Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( "News Feeds" ).h4, Gap(4), Text( "Manage your RSS news feeds." ).muted.small, ], ), ), Button.outline( child: Text( "Add Feed" ), onPressed: () { showDialog( context: context, barrierDismissible: false, builder: (context) => AddFeedDialog() ).then((value) { loadPage(); }); }, ) ], ), Gap(24), for (Feed feed in feeds) ...[ Row( children: [ Expanded( child: Text( feed.title.isNotEmpty ? feed.title : feed.url, style: TextStyle( decoration: feed.enabled ? TextDecoration.none : TextDecoration.lineThrough ), ), ), Builder( builder: (context2) { return GhostButton( density: ButtonDensity.dense, child: Icon( LucideIcons.ellipsis, ), onPressed: () { showDropdown( context: context2, builder: (_) { return DropdownMenu( children: [ MenuButton( child: Text( "Edit" ), trailing: Icon( LucideIcons.filePen ), onPressed: (_) { // Navigator.of(context).pop(); showDialog( context: context, barrierDismissible: false, builder: (context) => AddFeedDialog( existingFeed: feed, ) ).then((value) { loadPage(); }); }, ) ], ); } ); }, ); } ), Gap(4), Switch( value: feed.enabled, onChanged: (value) { SettingsProvider settings = SettingsProvider.of(context); settings.updateFeed( feed.id, enabled: value ); loadPage(); }, ), ], ), SelectableText( feed.url, style: TextStyle( decoration: feed.enabled ? TextDecoration.none : TextDecoration.lineThrough ), ).muted.small, Gap(12), ], Gap(12), Divider(), Gap(16), Text( "Application Data" ).h4, Gap(12), Text( "Application Storage Location" ).small.normal, Gap(8), Row( children: [ Expanded( child: TextField( placeholder: Text( "Select a diretory using Browse" ), initialValue: appStorageLocation, enabled: false, ), ), Gap(8), Button.outline( child: Text( "Browse" ), onPressed: () { FilePicker.platform.getDirectoryPath().then((selectedPath) { if (selectedPath != null) { SettingsProvider settings = SettingsProvider.of(context); settings.setApplicationStorageLocation(selectedPath); loadPage(); } }); }, ), Gap(8), IconButton.destructive( icon: Icon( LucideIcons.refreshCcw ), onPressed: () async { SettingsProvider settings = SettingsProvider.of(context); settings.setApplicationStorageLocation((await getApplicationDocumentsDirectory()).path); loadPage(); }, ) ], ), ], ).withMargin( all: 10 ), ), ), ).withPadding(all: 24), ); } } class AddFeedDialog extends StatelessWidget { Feed? existingFeed; AddFeedDialog({super.key, this.existingFeed}); InputKey feedTitleKey = InputKey("feed_title"); InputKey feedUrlKey = InputKey("feed_url"); @override Widget build(BuildContext context) { return AlertDialog( title: Text( existingFeed == null ? "Add Feed" : "Edit Feed" ), content: SizedBox( width: 400, child: Form( onSubmit: (context, values) { String title = values[feedTitleKey] ?? ""; String url = values[feedUrlKey] ?? ""; SettingsProvider settings = SettingsProvider.of(context); if (existingFeed != null) { settings.updateFeed( existingFeed!.id, title: title, url: url ); } else { settings.addFeed(Feed( title: title, url: url, enabled: true )); } Navigator.of(context).pop(); }, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( "Add a custom RSS feed to your news sources." ).muted, Gap(16), FormField( key: feedTitleKey, label: Text( "Title" ), validator: ConditionalValidator((value) { if (value is String) { return value.trim().isNotEmpty; } return false; }, message: "Title cannot be empty"), child: TextField( initialValue: existingFeed?.title, ), ), Gap(12), FormField( key: feedUrlKey, label: Text( "Resource URL" ), validator: ConditionalValidator((value) { if (value is String) { return value.trim().isNotEmpty; } return false; }, message: "URL cannot be empty"), child: TextField( initialValue: existingFeed?.url, ) ), Gap(24), Text( "Only use valid RSS feed URLs. Preferably only use sites you trust, and specifically business and financial news sources." ), Gap(24), Row( mainAxisAlignment: MainAxisAlignment.end, children: [ Button.outline( child: Text( "Cancel" ), onPressed: () { Navigator.of(context).pop(); }, ), Gap(8), SubmitButton( child: Text( existingFeed != null ? "Update Feed" : "Add Feed" ), ), ], ) ], ), ), ), ); } }