Skip to content

feat(settings): add configurable base URL#180

Open
heyoferu wants to merge 1 commit intodstark5:mainfrom
heyoferu:main
Open

feat(settings): add configurable base URL#180
heyoferu wants to merge 1 commit intodstark5:mainfrom
heyoferu:main

Conversation

@heyoferu
Copy link

Due to recently issues with "the" site, this pr introduces support for configuring the base URL at runtime, allowing users to change the source URL from the settings page.

Copilot AI review requested due to automatic review settings February 20, 2026 06:37
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds runtime configurability for the Anna’s Archive base URL, allowing users to change the source domain from the Settings page and having that preference persist across app restarts.

Changes:

  • Introduces a baseUrlProvider and wires it into search + book-info providers.
  • Updates AnnasArchieve to accept a configurable baseUrl instead of a static constant.
  • Persists the base URL in the preferences table, loads it on startup, and adds a Settings UI dialog to edit it.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
lib/ui/settings_page.dart Adds a “Base URL” settings entry and dialog for editing/saving the URL.
lib/state/state.dart Adds baseUrlProvider and uses it to construct AnnasArchieve for search/detail providers.
lib/services/database.dart Seeds a default annasArchiveBaseUrl preference on DB open.
lib/services/annas_archieve.dart Converts base URL from static const to instance field with constructor injection.
lib/main.dart Loads persisted base URL at startup and overrides baseUrlProvider accordingly.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

String currentBaseUrl = ref.read(baseUrlProvider);
controller.text = currentBaseUrl;

return showDialog(
Copy link

Copilot AI Feb 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_showBaseUrlDialog is declared async with return type Future<void>, but it returns showDialog(...) (a Future<T?>). This is a type mismatch and will fail to compile. await showDialog(...) and then return, or change the function signature to return the dialog's Future type (e.g. Future<T?>).

Suggested change
return showDialog(
await showDialog(

Copilot uses AI. Check for mistakes.
Comment on lines +61 to +129
return showDialog(
context: context,
builder: (context) {
return AlertDialog(
title: const Text('Configure Base URL'),
content: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
TextField(
controller: controller,
decoration: const InputDecoration(
hintText: 'https://annas-archive.li',
labelText: 'Base URL',
),
),
],
),
),
actions: [
TextButton(
onPressed: () {
Navigator.of(context).pop();
},
child: Text('Cancel',
style: TextStyle(color: Theme.of(context).colorScheme.error)),
),
ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red,
foregroundColor: Colors.white,
),
onPressed: () async {
String newUrl = controller.text.trim();
// Remove trailing slash if present
if (newUrl.endsWith('/')) {
newUrl = newUrl.substring(0, newUrl.length - 1);
}
// Validate URL format
if (newUrl.isNotEmpty &&
(newUrl.startsWith('http://') ||
newUrl.startsWith('https://'))) {
ref.read(baseUrlProvider.notifier).state = newUrl;
await dataBase.savePreference('annasArchiveBaseUrl', newUrl);
Navigator.of(context).pop();
// Show confirmation
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Base URL updated to: $newUrl'),
duration: const Duration(seconds: 2),
),
);
} else {
// Show error
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text(
'Invalid URL. Must start with http:// or https://'),
duration: Duration(seconds: 2),
),
);
}
},
child: const Text('Save'),
),
],
);
},
);
Copy link

Copilot AI Feb 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The TextEditingController created for the dialog is never disposed. Since this function creates the controller, it should dispose it (e.g., try/finally around an await showDialog(...)) to avoid leaking resources.

Suggested change
return showDialog(
context: context,
builder: (context) {
return AlertDialog(
title: const Text('Configure Base URL'),
content: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
TextField(
controller: controller,
decoration: const InputDecoration(
hintText: 'https://annas-archive.li',
labelText: 'Base URL',
),
),
],
),
),
actions: [
TextButton(
onPressed: () {
Navigator.of(context).pop();
},
child: Text('Cancel',
style: TextStyle(color: Theme.of(context).colorScheme.error)),
),
ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red,
foregroundColor: Colors.white,
),
onPressed: () async {
String newUrl = controller.text.trim();
// Remove trailing slash if present
if (newUrl.endsWith('/')) {
newUrl = newUrl.substring(0, newUrl.length - 1);
}
// Validate URL format
if (newUrl.isNotEmpty &&
(newUrl.startsWith('http://') ||
newUrl.startsWith('https://'))) {
ref.read(baseUrlProvider.notifier).state = newUrl;
await dataBase.savePreference('annasArchiveBaseUrl', newUrl);
Navigator.of(context).pop();
// Show confirmation
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Base URL updated to: $newUrl'),
duration: const Duration(seconds: 2),
),
);
} else {
// Show error
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text(
'Invalid URL. Must start with http:// or https://'),
duration: Duration(seconds: 2),
),
);
}
},
child: const Text('Save'),
),
],
);
},
);
try {
await showDialog(
context: context,
builder: (context) {
return AlertDialog(
title: const Text('Configure Base URL'),
content: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
TextField(
controller: controller,
decoration: const InputDecoration(
hintText: 'https://annas-archive.li',
labelText: 'Base URL',
),
),
],
),
),
actions: [
TextButton(
onPressed: () {
Navigator.of(context).pop();
},
child: Text('Cancel',
style:
TextStyle(color: Theme.of(context).colorScheme.error)),
),
ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red,
foregroundColor: Colors.white,
),
onPressed: () async {
String newUrl = controller.text.trim();
// Remove trailing slash if present
if (newUrl.endsWith('/')) {
newUrl = newUrl.substring(0, newUrl.length - 1);
}
// Validate URL format
if (newUrl.isNotEmpty &&
(newUrl.startsWith('http://') ||
newUrl.startsWith('https://'))) {
ref.read(baseUrlProvider.notifier).state = newUrl;
await dataBase.savePreference(
'annasArchiveBaseUrl', newUrl);
Navigator.of(context).pop();
// Show confirmation
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Base URL updated to: $newUrl'),
duration: const Duration(seconds: 2),
),
);
} else {
// Show error
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text(
'Invalid URL. Must start with http:// or https://'),
duration: Duration(seconds: 2),
),
);
}
},
child: const Text('Save'),
),
],
);
},
);
} finally {
controller.dispose();
}

Copilot uses AI. Check for mistakes.
Comment on lines +103 to +112
ref.read(baseUrlProvider.notifier).state = newUrl;
await dataBase.savePreference('annasArchiveBaseUrl', newUrl);
Navigator.of(context).pop();
// Show confirmation
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Base URL updated to: $newUrl'),
duration: const Duration(seconds: 2),
),
);
Copy link

Copilot AI Feb 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Navigator.of(context).pop() is called and then a SnackBar is shown using the dialog's context. After popping, that context may no longer be valid / may not have the intended ScaffoldMessenger. Consider capturing a parent BuildContext (from the page) for ScaffoldMessenger, or show the snackbar before popping, or use ScaffoldMessenger.of(rootContext).

Copilot uses AI. Check for mistakes.
Comment on lines +94 to +103
String newUrl = controller.text.trim();
// Remove trailing slash if present
if (newUrl.endsWith('/')) {
newUrl = newUrl.substring(0, newUrl.length - 1);
}
// Validate URL format
if (newUrl.isNotEmpty &&
(newUrl.startsWith('http://') ||
newUrl.startsWith('https://'))) {
ref.read(baseUrlProvider.notifier).state = newUrl;
Copy link

Copilot AI Feb 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

URL validation only checks the scheme prefix and strips a trailing slash; it still allows invalid/malformed URLs (e.g. https:// or https://host/path) which will break URL construction later ($baseUrl/search...). Consider validating via Uri.tryParse, ensuring a non-empty host, and rejecting non-empty path/query/fragment so the stored value is an origin/base host.

Copilot uses AI. Check for mistakes.
Comment on lines 80 to +83
final cookieProvider = StateProvider<String>((ref) => "");
final userAgentProvider = StateProvider<String>((ref) => "");
final baseUrlProvider =
StateProvider<String>((ref) => "https://annas-archive.li");
Copy link

Copilot AI Feb 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new configurable baseUrlProvider is only wired into search/book-info requests. Download mirror handling still hard-codes https://annas-archive.se in lib/services/download_file.dart (and even drops those URLs in _reorderMirrors), which will make downloads behave incorrectly if the user configures the base URL to that domain. Consider refactoring mirror ordering/filtering to not hard-code a specific domain and/or to use the configured base URL.

Copilot uses AI. Check for mistakes.
Comment on lines +100 to +102
if (newUrl.isNotEmpty &&
(newUrl.startsWith('http://') ||
newUrl.startsWith('https://'))) {
Copy link

Copilot AI Feb 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The base URL dialog explicitly allows values starting with http://, which will cause all subsequent requests via Dio to use unencrypted HTTP when such a URL is saved. An attacker on the same network can eavesdrop on or tamper with users’ searches and downloaded content whenever a non-TLS mirror is configured. Restrict this setting to https:// URLs (or gate http:// usage behind an explicit developer/debug flag) so that production traffic is always protected by TLS.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants