diff --git a/lib/main.dart b/lib/main.dart index e875a97..913a9a1 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -28,7 +28,8 @@ import 'package:openlib/state/state.dart' openPdfWithExternalAppProvider, openEpubWithExternalAppProvider, userAgentProvider, - cookieProvider; + cookieProvider, + baseUrlProvider; void main() async { WidgetsFlutterBinding.ensureInitialized(); @@ -54,6 +55,12 @@ void main() async { String browserUserAgent = await dataBase.getBrowserOptions('userAgent'); String browserCookie = await dataBase.getBrowserOptions('cookie'); + String annasArchiveBaseUrl; + try { + annasArchiveBaseUrl = await dataBase.getPreference('annasArchiveBaseUrl'); + } catch (e) { + annasArchiveBaseUrl = 'https://annas-archive.li'; + } if (Platform.isAndroid) { SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle( @@ -73,6 +80,7 @@ void main() async { .overrideWith((ref) => openEpubwithExternalapp), userAgentProvider.overrideWith((ref) => browserUserAgent), cookieProvider.overrideWith((ref) => browserCookie), + baseUrlProvider.overrideWith((ref) => annasArchiveBaseUrl), ], child: const MyApp(), ), @@ -144,8 +152,7 @@ class _MainScreenState extends ConsumerState { child: AppBar( backgroundColor: Theme.of(context).colorScheme.surface, title: const Text("Openlib"), - titleTextStyle: - Theme.of(context).textTheme.displayLarge, + titleTextStyle: Theme.of(context).textTheme.displayLarge, ), ), Expanded( @@ -156,8 +163,7 @@ class _MainScreenState extends ConsumerState { ), bottomNavigationBar: SafeArea( child: GNav( - backgroundColor: - isDarkMode ? Colors.black : Colors.grey.shade200, + backgroundColor: isDarkMode ? Colors.black : Colors.grey.shade200, haptic: true, tabBorderRadius: 50, tabActiveBorder: Border.all( @@ -170,10 +176,8 @@ class _MainScreenState extends ConsumerState { color: Colors.white, activeColor: Colors.white, iconSize: 19, - tabBackgroundColor: - Theme.of(context).colorScheme.secondary, - padding: - const EdgeInsets.symmetric(horizontal: 13, vertical: 6.5), + tabBackgroundColor: Theme.of(context).colorScheme.secondary, + padding: const EdgeInsets.symmetric(horizontal: 13, vertical: 6.5), tabs: const [ GButton(icon: Icons.trending_up, text: 'Home'), GButton(icon: Icons.search, text: 'Search'), diff --git a/lib/services/annas_archieve.dart b/lib/services/annas_archieve.dart index 3ea2fc2..19af047 100644 --- a/lib/services/annas_archieve.dart +++ b/lib/services/annas_archieve.dart @@ -53,7 +53,9 @@ class BookInfoData extends BookData { // ==================================================================== class AnnasArchieve { - static const String baseUrl = "https://annas-archive.se"; + String baseUrl; + + AnnasArchieve({this.baseUrl = "https://annas-archive.li"}); final Dio dio = Dio(); @@ -91,7 +93,7 @@ class AnnasArchieve { } return value; } - + // -------------------------------------------------------------------- // _parser FUNCTION (Search Results - Fixed nth-of-type issue) // -------------------------------------------------------------------- @@ -108,7 +110,8 @@ class AnnasArchieve { container.querySelector('a.line-clamp-\\[3\\].js-vim-focus'); final thumbnailElement = container.querySelector('a[href^="/md5/"] img'); - if (mainLinkElement == null || mainLinkElement.attributes['href'] == null) { + if (mainLinkElement == null || + mainLinkElement.attributes['href'] == null) { continue; } @@ -120,27 +123,31 @@ class AnnasArchieve { // Fix: Use sequential traversal instead of :nth-of-type dom.Element? authorLinkElement = mainLinkElement.nextElementSibling; dom.Element? publisherLinkElement = authorLinkElement?.nextElementSibling; - - if (authorLinkElement?.attributes['href']?.startsWith('/search?q=') != true) { - authorLinkElement = null; + + if (authorLinkElement?.attributes['href']?.startsWith('/search?q=') != + true) { + authorLinkElement = null; } - if (publisherLinkElement?.attributes['href']?.startsWith('/search?q=') != true) { - publisherLinkElement = null; + if (publisherLinkElement?.attributes['href']?.startsWith('/search?q=') != + true) { + publisherLinkElement = null; } final String? authorRaw = authorLinkElement?.text.trim(); final String? author = (authorRaw != null && authorRaw.contains('icon-')) ? authorRaw.split(' ').skip(1).join(' ').trim() : authorRaw; - + final String? publisher = publisherLinkElement?.text.trim(); - + final infoElement = container.querySelector('div.text-gray-800'); // No need for _safeParse here if we only treat info as a string - final String? info = infoElement?.text.trim(); - + final String? info = infoElement?.text.trim(); + final bool hasMatchingFileType = fileType.isEmpty - ? (info?.contains(RegExp(r'(PDF|EPUB|CBR|CBZ)', caseSensitive: false)) == true) + ? (info?.contains( + RegExp(r'(PDF|EPUB|CBR|CBZ)', caseSensitive: false)) == + true) : info?.toLowerCase().contains(fileType.toLowerCase()) == true; if (hasMatchingFileType) { @@ -165,44 +172,49 @@ class AnnasArchieve { // -------------------------------------------------------------------- Future _bookInfoParser(resData, url) async { var document = parse(resData.toString()); - final main = document.querySelector('div.main-inner'); + final main = document.querySelector('div.main-inner'); if (main == null) return null; // --- Mirror Link Extraction --- String? mirror; - final slowDownloadLinks = main.querySelectorAll('ul.list-inside a[href*="/slow_download/"]'); - if (slowDownloadLinks.isNotEmpty && slowDownloadLinks.first.attributes['href'] != null) { - mirror = baseUrl + slowDownloadLinks.first.attributes['href']!; + final slowDownloadLinks = + main.querySelectorAll('ul.list-inside a[href*="/slow_download/"]'); + if (slowDownloadLinks.isNotEmpty && + slowDownloadLinks.first.attributes['href'] != null) { + mirror = baseUrl + slowDownloadLinks.first.attributes['href']!; } // -------------------------------- - // --- Core Info Extraction --- - + // Title - final titleElement = main.querySelector('div.font-semibold.text-2xl'); - + final titleElement = main.querySelector('div.font-semibold.text-2xl'); + // Author - final authorLinkElement = main.querySelector('a[href^="/search?q="].text-base'); - + final authorLinkElement = + main.querySelector('a[href^="/search?q="].text-base'); + // Publisher dom.Element? publisherLinkElement = authorLinkElement?.nextElementSibling; - if (publisherLinkElement?.localName != 'a' || publisherLinkElement?.attributes['href']?.startsWith('/search?q=') != true) { - publisherLinkElement = null; + if (publisherLinkElement?.localName != 'a' || + publisherLinkElement?.attributes['href']?.startsWith('/search?q=') != + true) { + publisherLinkElement = null; } // Thumbnail final thumbnailElement = main.querySelector('div[id^="list_cover_"] img'); - + // Info/Metadata final infoElement = main.querySelector('div.text-gray-800'); - + // Description dom.Element? descriptionElement; - final descriptionLabel = main.querySelector('div.js-md5-top-box-description div.text-xs.text-gray-500.uppercase'); - + final descriptionLabel = main.querySelector( + 'div.js-md5-top-box-description div.text-xs.text-gray-500.uppercase'); + if (descriptionLabel?.text.trim().toLowerCase() == 'description') { - descriptionElement = descriptionLabel?.nextElementSibling; + descriptionElement = descriptionLabel?.nextElementSibling; } String description = descriptionElement?.text.trim() ?? " "; @@ -210,14 +222,14 @@ class AnnasArchieve { return null; } - final String title = titleElement.text.trim().split('((ref) => true); // Web/Download States final cookieProvider = StateProvider((ref) => ""); final userAgentProvider = StateProvider((ref) => ""); +final baseUrlProvider = + StateProvider((ref) => "https://annas-archive.li"); final webViewLoadingState = StateProvider.autoDispose((ref) => true); -final downloadProgressProvider = StateProvider.autoDispose((ref) => 0.0); +final downloadProgressProvider = + StateProvider.autoDispose((ref) => 0.0); final mirrorStatusProvider = StateProvider.autoDispose((ref) => false); final totalFileSizeInBytes = StateProvider.autoDispose((ref) => 0); final downloadedFileSizeInBytes = StateProvider.autoDispose((ref) => 0); -final downloadState = StateProvider.autoDispose((ref) => ProcessState.waiting); -final checkSumState = StateProvider.autoDispose((ref) => CheckSumProcessState.waiting); +final downloadState = + StateProvider.autoDispose((ref) => ProcessState.waiting); +final checkSumState = StateProvider.autoDispose( + (ref) => CheckSumProcessState.waiting); final cancelCurrentDownload = StateProvider((ref) { return CancelToken(); }); @@ -140,20 +145,22 @@ final getTrendingBooks = FutureProvider>((ref) async { GoodReads goodReads = GoodReads(); // Assuming these classes are available from your project imports // ignore: prefer_const_constructors - final penguinTrending = PenguinRandomHouse(); + final penguinTrending = PenguinRandomHouse(); // ignore: prefer_const_constructors final bookDigits = BookDigits(); - List trendingBooks = await Future.wait>([ + List trendingBooks = + await Future.wait>([ goodReads.trendingBooks(), penguinTrending.trendingBooks(), // openLibrary.trendingBooks(), // Commented out as in the original bookDigits.trendingBooks(), ]).then((List> listOfData) => - listOfData.expand((element) => element).toList()); + listOfData.expand((element) => element).toList()); if (trendingBooks.isEmpty) { - throw Exception('Nothing Trending Today :('); // Use Exception instead of String + throw Exception( + 'Nothing Trending Today :('); // Use Exception instead of String } trendingBooks.shuffle(); return trendingBooks; @@ -175,9 +182,11 @@ final getSubCategoryTypeList = FutureProvider.family // Provider for Anna's Archive Search Results final searchProvider = FutureProvider.family .autoDispose, String>((ref, searchQuery) async { - if (searchQuery.isEmpty) return []; // Return empty list if search query is empty + if (searchQuery.isEmpty) + return []; // Return empty list if search query is empty - final AnnasArchieve annasArchieve = AnnasArchieve(); + final String baseUrl = ref.watch(baseUrlProvider); + final AnnasArchieve annasArchieve = AnnasArchieve(baseUrl: baseUrl); List data = await annasArchieve.searchBooks( searchQuery: searchQuery, content: ref.watch(getTypeValue), @@ -190,7 +199,8 @@ final searchProvider = FutureProvider.family // Provider for Book Info Details final bookInfoProvider = FutureProvider.family((ref, url) async { - final AnnasArchieve annasArchieve = AnnasArchieve(); + final String baseUrl = ref.watch(baseUrlProvider); + final AnnasArchieve annasArchieve = AnnasArchieve(baseUrl: baseUrl); BookInfoData data = await annasArchieve.bookInfo(url: url); return data; }); @@ -236,4 +246,4 @@ Future saveEpubState( String fileName, String? position, WidgetRef ref) async { String pos = position ?? ''; await dataBase.saveBookState(fileName, pos); -} \ No newline at end of file +} diff --git a/lib/ui/settings_page.dart b/lib/ui/settings_page.dart index d4e0740..0b01d5c 100644 --- a/lib/ui/settings_page.dart +++ b/lib/ui/settings_page.dart @@ -21,7 +21,8 @@ import 'package:openlib/state/state.dart' show themeModeProvider, openPdfWithExternalAppProvider, - openEpubWithExternalAppProvider; + openEpubWithExternalAppProvider, + baseUrlProvider; Future requestStoragePermission() async { bool permissionGranted = false; @@ -51,6 +52,83 @@ Future requestStoragePermission() async { print("Storage permission status: $permissionGranted"); } +Future _showBaseUrlDialog( + BuildContext context, WidgetRef ref, MyLibraryDb dataBase) async { + final TextEditingController controller = TextEditingController(); + String currentBaseUrl = ref.read(baseUrlProvider); + controller.text = currentBaseUrl; + + 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'), + ), + ], + ); + }, + ); +} + class SettingsPage extends ConsumerWidget { const SettingsPage({super.key}); @@ -165,6 +243,21 @@ class SettingsPage extends ConsumerWidget { ), Icon(Icons.folder), ]), + _PaddedContainer( + onClick: () async { + await _showBaseUrlDialog(context, ref, dataBase); + }, + children: [ + Text( + "Anna's Archive Base URL", + style: TextStyle( + fontSize: 15, + fontWeight: FontWeight.bold, + color: Theme.of(context).colorScheme.tertiary, + ), + ), + Icon(Icons.link), + ]), _PaddedContainer( onClick: () { Navigator.push(context,