diff --git a/lib/main.dart b/lib/main.dart index e7b03ee..e333ba8 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -3,26 +3,27 @@ import 'package:provider/provider.dart'; import 'screens/home_screen.dart'; import 'services/google_books_api.dart'; +// Entry point of the Flutter application void main() { runApp(const MyApp()); } +// Root widget of the application class MyApp extends StatelessWidget { const MyApp({Key? key}) : super(key: key); @override Widget build(BuildContext context) { return MultiProvider( + // Providing dependencies to the widget tree using MultiProvider providers: [ + // Registering GoogleBooksApi as a ChangeNotifier for state management ChangeNotifierProvider(create: (_) => GoogleBooksApi()), ], child: MaterialApp( debugShowCheckedModeBanner: false, title: 'EBook reader Application', - - theme: ThemeData( - primarySwatch: Colors.blue, - ), + // Sets the HomeScreen as the initial screen home: const HomeScreen(), ), ); diff --git a/lib/screens/book_details.dart b/lib/screens/book_details.dart new file mode 100644 index 0000000..591fe9f --- /dev/null +++ b/lib/screens/book_details.dart @@ -0,0 +1,182 @@ +import 'package:flutter/material.dart'; +import 'package:url_launcher/url_launcher.dart'; + +class BookDetail extends StatelessWidget { + final Map book; + + const BookDetail({Key? key, required this.book}) : super(key: key); + // Function to open URLs in the browser + Future _launchURL(String url) async { + try { + // Encode the URL to handle special characters + final Uri uri = Uri.parse(url); + if (!await launchUrl(uri)) { + throw Exception('Could not launch $uri'); + } + } catch (e) { + // Show a snackbar with the error message + debugPrint('Error launching URL: $e'); + // You might want to show this error to the user through a SnackBar + // or other UI element + } + } + + @override + Widget build(BuildContext context) { + final volumeInfo = book['volumeInfo']; + + // Extract URLs + final thumbnail = + volumeInfo['imageLinks'] != null + ? volumeInfo['imageLinks']['thumbnail'] + : null; + + final previewLink = volumeInfo['previewLink']; + final downloadLink = + book['accessInfo'] != null + ? book['accessInfo']['pdf'] != null && + book['accessInfo']['pdf']['isAvailable'] + ? book['accessInfo']['pdf']['acsTokenLink'] + : null + : null; + + final buyLink = volumeInfo['infoLink']; + + return Scaffold( + appBar: AppBar(title: Text(volumeInfo['title'] ?? 'No Title')), + body: SingleChildScrollView( + padding: const EdgeInsets.all(8.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Display the image with a placeholder and error handling + thumbnail != null + ? Center( + child: Image.network( + thumbnail, + fit: BoxFit.cover, + loadingBuilder: (context, child, loadingProgress) { + if (loadingProgress == null) { + return child; + } + return const Center(child: CircularProgressIndicator()); + }, + errorBuilder: (context, error, stackTrace) { + return const Icon(Icons.error, size: 100); + }, + ), + ) + : const Icon(Icons.book, size: 100), + const SizedBox(height: 16), + Text( + volumeInfo['title'] ?? 'No Title', + style: const TextStyle(fontSize: 15, fontWeight: FontWeight.bold), + ), + const SizedBox(height: 8), + Text( + 'Author(s): ' + + (volumeInfo['authors'] != null + ? volumeInfo['authors'].join(', ') + : 'Unknown'), + ), + const SizedBox(height: 8), + Text(volumeInfo['description'] ?? 'No Description'), + const SizedBox(height: 16), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + // Preview Button + if (previewLink != null) + ElevatedButton( + onPressed: () { + _launchURL(previewLink); + }, + child: const Text( + 'Preview', + style: TextStyle( + fontSize: 10, + letterSpacing: 1.0, + fontWeight: FontWeight.bold, + ), + textAlign: TextAlign.center, + ), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.green[700], + foregroundColor: Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), + padding: EdgeInsets.only( + right: 20, + left: 25, + top: 15, + bottom: 15, + ), + ), + ), + const SizedBox(width: 10), + // Download Button + if (downloadLink != null) + ElevatedButton( + onPressed: () { + _launchURL(downloadLink); + }, + child: const Text( + 'Download', + style: TextStyle( + fontSize: 10, + letterSpacing: 1.0, + fontWeight: FontWeight.bold, + ), + textAlign: TextAlign.center, + ), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.green[700], + foregroundColor: Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), + padding: EdgeInsets.only( + right: 20, + left: 25, + top: 15, + bottom: 15, + ), + ), + ) + else + ElevatedButton( + onPressed: () { + _launchURL(buyLink); + }, + child: const Text( + 'More Info', + style: TextStyle( + fontSize: 10, + letterSpacing: 1.0, + fontWeight: FontWeight.bold, + ), + textAlign: TextAlign.center, + ), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.green[700], + foregroundColor: Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), + padding: EdgeInsets.only( + right: 20, + left: 25, + top: 15, + bottom: 15, + ), + ), + ), + ], + ), + ], + ), + ), + ); + } +} diff --git a/lib/screens/home_screen.dart b/lib/screens/home_screen.dart index a090aa4..8a6774c 100644 --- a/lib/screens/home_screen.dart +++ b/lib/screens/home_screen.dart @@ -1,43 +1,63 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import '../services/google_books_api.dart'; -import 'book_detail.dart'; // Import added +import './book_details.dart'; +// HomeScreen is the main screen of the application where users can search for books class HomeScreen extends StatelessWidget { const HomeScreen({Key? key}) : super(key: key); @override Widget build(BuildContext context) { + // Access the GoogleBooksApi provider final bookProvider = Provider.of(context); + // Controller for the search input field TextEditingController searchController = TextEditingController(); return Scaffold( appBar: AppBar( centerTitle: true, - title: const Text('EBook reader Application', style: TextStyle(fontSize: 25)), - backgroundColor: Colors.green[700], - toolbarHeight: 70, - elevation: 5, - shadowColor: Colors.green[700], - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(20)) + title: const Text( + 'EBook reader Application', + style: TextStyle(fontSize: 25), + ), + backgroundColor: Colors.green[700], + foregroundColor: Colors.white, + toolbarHeight: 70, + elevation: 5, + shadowColor: Colors.green[700], + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)), ), body: Column( children: [ - const SizedBox(width: 10), + const SizedBox(height: 10), // Adds spacing at the top Padding( padding: const EdgeInsets.all(10.0), child: TextField( + // Binds the controller to the TextField controller: searchController, + // Triggers search on submission + onSubmitted: + (value) => bookProvider.search(searchController.text), decoration: InputDecoration( border: OutlineInputBorder(), - labelText: 'Search for books', - labelStyle: TextStyle( - color: Color.fromARGB(255, 3, 131, 29),// Set the color you want here + focusedBorder: OutlineInputBorder( + borderSide: const BorderSide( + color: Color.fromARGB(255, 3, 131, 29), + ), + borderRadius: BorderRadius.circular(10.0), ), + // Placeholder text + labelText: 'Search for books', + labelStyle: TextStyle(color: Color.fromARGB(255, 3, 131, 29)), suffixIcon: IconButton( - icon: const Icon(Icons.search, size: 30, color: Color.fromARGB(255, 3, 131, 29),), + icon: const Icon( + Icons.search, + size: 30, + color: Color.fromARGB(255, 3, 131, 29), + ), onPressed: () { + // Triggers search on button press bookProvider.search(searchController.text); }, ), @@ -45,29 +65,43 @@ class HomeScreen extends StatelessWidget { ), ), Expanded( - child: bookProvider.isLoading - ? const Center(child: CircularProgressIndicator()) - : ListView.builder( - itemCount: bookProvider.books.length, - itemBuilder: (context, index) { - final book = bookProvider.books[index]; - return ListTile( - title: Text(book['volumeInfo']['title'] ?? 'No Title'), - subtitle: Text(book['volumeInfo']['authors'] != null - ? book['volumeInfo']['authors'].join(', ') - : 'Unknown Author'), - onTap: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => - BookDetail(book: book), - ), - ); - }, - ); - }, - ), + // Checks if data is still loading + child: + bookProvider.isLoading + ? const Center( + child: CircularProgressIndicator( + color: Color.fromARGB(255, 3, 131, 29), + ), + ) + : ListView.builder( + // Number of books to display + itemCount: bookProvider.books.length, + itemBuilder: (context, index) { + // Current book data + final book = bookProvider.books[index]; + return ListTile( + // Book title + title: Text( + book['volumeInfo']['title'] ?? 'No Title', + ), + // Authors list + subtitle: Text( + book['volumeInfo']['authors'] != null + ? book['volumeInfo']['authors'].join(', ') + : 'Unknown Author', // Fallback if no authors are available + ), + onTap: () { + // Navigate to the BookDetail screen when a book is tapped + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => BookDetail(book: book), + ), + ); + }, + ); + }, + ), ), ], ), diff --git a/lib/services/google_books_api.dart b/lib/services/google_books_api.dart index e087b02..41c49d5 100644 --- a/lib/services/google_books_api.dart +++ b/lib/services/google_books_api.dart @@ -2,40 +2,50 @@ import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:http/http.dart' as http; -class GoogleBooksApi extends ChangeNotifier { - List books = []; // List to store fetched books data. - bool isLoading = false; // Boolean to indicate loading state. +class GoogleBooksApi extends ChangeNotifier { + // List to store fetched books data from the API + List books = []; - final String apiKey = 'your_api_key'; // Google Books API key. + // Boolean to indicate whether data is currently being fetched + bool isLoading = false; // Function to search books based on user query Future search(String query) async { - // URL of the Google Books API, with the query string and API key + // Construct the URL for the Google Books API with the query string final url = - 'https://www.googleapis.com/books/v1/volumes?q=${Uri.encodeComponent(query)}&key=$apiKey'; + 'https://www.googleapis.com/books/v1/volumes?q=$query'; - // Set loading state to true (start fetching), notify listeners + // Set loading state to true to indicate data fetching has started isLoading = true; - notifyListeners(); // Notify the UI to show a loading spinner. + // Notify listeners (e.g., UI) to update the loading state + notifyListeners(); try { - // Make HTTP GET request to Google Books API + // Make an HTTP GET request to the Google Books API final response = await http.get(Uri.parse(url)); - // Check if the request was successful (status code 200) + // Log the response status code for debugging purposes + print("response status code ${response.statusCode}"); + + // Check if the request was successful (HTTP status code 200) if (response.statusCode == 200) { - final data = json.decode(response.body); // Parse the JSON response. + // Parse the JSON response body + final data = json.decode(response.body); + // Extract the 'items' field from the response, or set to an empty list if null books = data['items'] ?? []; } else { - books = []; // If request fails, set the books list to empty. + // If the request fails, clear the books list + books = []; } } catch (e) { - // If there's an exception + // Handle any exceptions that occur during the HTTP request + // Clear the books list in case of an error books = []; } - // Set loading state to false (done fetching), notify listeners + // Set loading state to false to indicate data fetching is complete isLoading = false; - notifyListeners(); // Notify the UI to remove the loading spinner. + // Notify listeners (e.g., UI) to update the loading state and display results + notifyListeners(); } -} +} \ No newline at end of file diff --git a/pubspec.lock b/pubspec.lock index fcf7d1b..951a310 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -5,42 +5,42 @@ packages: dependency: transitive description: name: async - sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" + sha256: d2872f9c19731c2e5f10444b14686eb7cc85c76274bd6c16e1816bff9a3bab63 url: "https://pub.dev" source: hosted - version: "2.11.0" + version: "2.12.0" boolean_selector: dependency: transitive description: name: boolean_selector - sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" + sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.2" characters: dependency: transitive description: name: characters - sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" + sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 url: "https://pub.dev" source: hosted - version: "1.3.0" + version: "1.4.0" clock: dependency: transitive description: name: clock - sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf + sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b url: "https://pub.dev" source: hosted - version: "1.1.1" + version: "1.1.2" collection: dependency: transitive description: name: collection - sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a + sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" url: "https://pub.dev" source: hosted - version: "1.18.0" + version: "1.19.1" cupertino_icons: dependency: "direct main" description: @@ -53,10 +53,10 @@ packages: dependency: transitive description: name: fake_async - sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" + sha256: "6a95e56b2449df2273fd8c45a662d6947ce1ebb7aafe80e550a3f68297f3cacc" url: "https://pub.dev" source: hosted - version: "1.3.1" + version: "1.3.2" flutter: dependency: "direct main" description: flutter @@ -100,18 +100,18 @@ packages: dependency: transitive description: name: leak_tracker - sha256: "3f87a60e8c63aecc975dda1ceedbc8f24de75f09e4856ea27daf8958f2f0ce05" + sha256: c35baad643ba394b40aac41080300150a4f08fd0fd6a10378f8f7c6bc161acec url: "https://pub.dev" source: hosted - version: "10.0.5" + version: "10.0.8" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: "932549fb305594d82d7183ecd9fa93463e9914e1b67cacc34bc40906594a1806" + sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573 url: "https://pub.dev" source: hosted - version: "3.0.5" + version: "3.0.9" leak_tracker_testing: dependency: transitive description: @@ -132,10 +132,10 @@ packages: dependency: transitive description: name: matcher - sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb + sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 url: "https://pub.dev" source: hosted - version: "0.12.16+1" + version: "0.12.17" material_color_utilities: dependency: transitive description: @@ -148,10 +148,10 @@ packages: dependency: transitive description: name: meta - sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7 + sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c url: "https://pub.dev" source: hosted - version: "1.15.0" + version: "1.16.0" nested: dependency: transitive description: @@ -164,10 +164,10 @@ packages: dependency: transitive description: name: path - sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" + sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" url: "https://pub.dev" source: hosted - version: "1.9.0" + version: "1.9.1" plugin_platform_interface: dependency: transitive description: @@ -188,55 +188,55 @@ packages: dependency: transitive description: flutter source: sdk - version: "0.0.99" + version: "0.0.0" source_span: dependency: transitive description: name: source_span - sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" + sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c" url: "https://pub.dev" source: hosted - version: "1.10.0" + version: "1.10.1" stack_trace: dependency: transitive description: name: stack_trace - sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" + sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" url: "https://pub.dev" source: hosted - version: "1.11.1" + version: "1.12.1" stream_channel: dependency: transitive description: name: stream_channel - sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 + sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.4" string_scanner: dependency: transitive description: name: string_scanner - sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" + sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.4.1" term_glyph: dependency: transitive description: name: term_glyph - sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 + sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" url: "https://pub.dev" source: hosted - version: "1.2.1" + version: "1.2.2" test_api: dependency: transitive description: name: test_api - sha256: "5b8a98dafc4d5c4c9c72d8b31ab2b23fc13422348d2997120294d3bac86b4ddb" + sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd url: "https://pub.dev" source: hosted - version: "0.7.2" + version: "0.7.4" typed_data: dependency: transitive description: @@ -321,10 +321,10 @@ packages: dependency: transitive description: name: vm_service - sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d" + sha256: "0968250880a6c5fe7edc067ed0a13d4bae1577fe2771dcf3010d52c4a9d3ca14" url: "https://pub.dev" source: hosted - version: "14.2.5" + version: "14.3.1" web: dependency: transitive description: @@ -334,5 +334,5 @@ packages: source: hosted version: "1.1.0" sdks: - dart: ">=3.5.3 <4.0.0" + dart: ">=3.7.0-0 <4.0.0" flutter: ">=3.24.0"