Add feed aggregation and embedding generation features
This commit is contained in:
@@ -1,18 +1,65 @@
|
||||
|
||||
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 StatelessWidget {
|
||||
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) {
|
||||
// TODO: implement build
|
||||
|
||||
if (_isLoading) {
|
||||
return Scaffold(
|
||||
child: Center(
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return Scaffold(
|
||||
headers: [
|
||||
AppBar(
|
||||
@@ -24,58 +71,377 @@ class SettingsPage extends StatelessWidget {
|
||||
currentPage: "settings",
|
||||
)
|
||||
],
|
||||
child: Column(
|
||||
children: [
|
||||
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: Card(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
|
||||
Text(
|
||||
"API Key"
|
||||
).extraBold,
|
||||
|
||||
const SizedBox(height: 16),
|
||||
|
||||
TextField(
|
||||
|
||||
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),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const SizedBox(height: 16),
|
||||
class AddFeedDialog extends StatelessWidget {
|
||||
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: Card(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
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: [
|
||||
|
||||
Text(
|
||||
"Feeds"
|
||||
).extraBold,
|
||||
|
||||
const SizedBox(height: 16),
|
||||
|
||||
TextField(
|
||||
|
||||
Button.outline(
|
||||
child: Text(
|
||||
"Cancel"
|
||||
),
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
Gap(8),
|
||||
|
||||
],
|
||||
).withMargin(
|
||||
all: 10
|
||||
SubmitButton(
|
||||
child: Text(
|
||||
existingFeed != null ? "Update Feed" : "Add Feed"
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user