449 lines
13 KiB
Dart
449 lines
13 KiB
Dart
|
|
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<SettingsPage> createState() => _SettingsPageState();
|
|
}
|
|
|
|
class _SettingsPageState extends State<SettingsPage> {
|
|
|
|
bool _isLoading = true;
|
|
|
|
String apiKey = "";
|
|
List<Feed> 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"
|
|
),
|
|
),
|
|
],
|
|
)
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
} |