Compare commits
57 Commits
v1.0-beta.
...
v1.0.3-bet
Author | SHA1 | Date | |
---|---|---|---|
b0579a5c15 | |||
092f6029b6 | |||
51283c3410 | |||
3a95d79c4a | |||
9aeea38192 | |||
76c80be783 | |||
f71535438b | |||
b536e1600a | |||
2ddc71d830 | |||
013cce078f | |||
7f3e255259 | |||
0825857231 | |||
9255201049 | |||
ed8a6e01e9 | |||
9def607737 | |||
6d0d417ce0 | |||
b127683003 | |||
a6775b95a9 | |||
647687c0c9 | |||
9dc7c67440 | |||
ccd260d451 | |||
6ac9f0c77a | |||
546805be20 | |||
250f0f8b7b | |||
f0d0178a2d | |||
f4f86a1353 | |||
93013fe6bb | |||
1de126104d | |||
e4c8ddaa88 | |||
82798bb749 | |||
16fc051219 | |||
da399604bf | |||
1b87e89470 | |||
0db7794455 | |||
6b31a63e86 | |||
27061a1922 | |||
162ea41196 | |||
010dc1dfdb | |||
b2a1be7a7e | |||
1507656944 | |||
9cc3c3dde5 | |||
e7b6a9d40f | |||
60df4afac2 | |||
bcfb35ab14 | |||
4ac80f08d8 | |||
a9fde1c561 | |||
110d089b8d | |||
4445fc4d84 | |||
11d19b3001 | |||
052c5993b1 | |||
bc36a350ab | |||
9102b0be7b | |||
365d29c19e | |||
52c742e8d0 | |||
d2c992bf80 | |||
39a91b9992 | |||
19df3b4f7b |
@ -1,24 +1,30 @@
|
||||
## Openlib Contribution guidelines
|
||||
|
||||
# Code contribution
|
||||
### Getting started
|
||||
|
||||
### Fork the repo
|
||||
- This project is powered by Flutter. Make sure you have the latest version of Flutter.
|
||||
|
||||
- Clone the Openlib repository with git clone https://github.com/dstark5/Openlib.git (or use the link from your own fork, if you want to open a PR)
|
||||
- If you want to fix bug or implement a new feature, that already mention in the [issues](https://github.com/dstark5/Openlib/issues), please, assign this issue to you and/or comment about it.
|
||||
|
||||
|
||||
### Make your update:
|
||||
Make your changes to the file(s) you'd like to update.
|
||||
- Whether you have to implement new feature, please, open an issue or discussion regarding it to ensure it will be accepted.
|
||||
|
||||
### Open a pull request
|
||||
|
||||
When you're done making changes and you'd like to propose them for review, open your PR (pull request). You can use the GitHub user interface for some small changes, like fixing a typo or updating a readme. You can also fork the repo and then clone it locally, to view changes and run your tests on your machine.
|
||||
|
||||
### Before PR Self Review
|
||||
|
||||
Always review your own PR first
|
||||
|
||||
- Confirm the changes doesn't break anything else.
|
||||
- Compare your pull request's source changes to staging to confirm that the output matches the source and that everything is rendering as expected. This helps spot issues like typos, content that doesn't follow the style guide, or content that isn't rendering due to versioning problems.
|
||||
|
||||
### Submit your PR & get it reviewed
|
||||
Once you Submit Your PR We Review The Code And Merge It.
|
||||
- Once you submit your PR, others from the Docs community will review it with you. The first thing you're going to want to do is a [self review](#self-review).
|
||||
- After that, we may have questions, check back on your PR to keep up with the conversation.
|
||||
- We may ask for changes to be made before a PR can be merged. You can make any other changes in your fork, then commit them to your branch.
|
||||
|
||||
### Self review
|
||||
You should always review your own PR first.
|
||||
|
||||
For content changes, make sure that you:
|
||||
- [ ] Confirm the changes doesn't break anything else.
|
||||
- [ ] Compare your pull request's source changes to staging to confirm that the output matches the source and that everything is rendering as expected. This helps spot issues like typos, content that doesn't follow the style guide, or content that isn't rendering due to versioning problems.
|
||||
- [ ] Review the content for technical accuracy.
|
||||
- [ ] Copy-edit the changes for grammar or spelling mistakes.
|
||||
|
||||
### Notes
|
||||
- Please, do not modify readme and other information files (except for typos).
|
||||
- Avoid adding new dependencies unless required. APK size is important.
|
29
README.md
@ -1,9 +1,15 @@
|
||||
<p align="center"><img src="assets/icons/appIcon.png" width="150"></p>
|
||||
<p align="center"><img src="assets/icons/appIcon.png" width="150"></p>
|
||||
<h1 align="center"><b>Openlib</b></h1>
|
||||
|
||||
#### An Open source app to download and read books from shadow library ([Anna’s Archive](https://annas-archive.org/)).
|
||||
|
||||
[](https://flutter.dev/) [](https://opensource.org/licenses/)
|
||||
[](https://flutter.dev/) [](https://opensource.org/licenses/) 
|
||||
|
||||
[<img src="github_releases.png"
|
||||
alt="Download from GitHub"
|
||||
height="60">](https://github.com/dstark5/Openlib/releases) [<img src="https://gitlab.com/IzzyOnDroid/repo/-/raw/master/assets/IzzyOnDroid.png"
|
||||
alt="Get it on IzzyDroid"
|
||||
height="60">](https://android.izzysoft.de/repo/apk/com.app.openlib)
|
||||
|
||||
|
||||
## Note
|
||||
@ -12,6 +18,10 @@
|
||||
|
||||
#### Publishing Openlib, Or Any Fork Of It In The Google Play Store Violates Their Terms And Conditions.
|
||||
|
||||
## Download
|
||||
- Download and install APK from [GitHub Releases](https://github.com/dstark5/Openlib/releases).
|
||||
- Download and install APK from [ IzzyOnDroid ](https://android.izzysoft.de/repo/apk/com.app.openlib).
|
||||
|
||||
|
||||
## Screenshots
|
||||
|
||||
@ -27,24 +37,23 @@
|
||||
## Description
|
||||
##### Openlib Is An Open Source App To Download And Read Books From Shadow Library ([Anna’s Archive](https://annas-archive.org/)) . The App Has Built In Reader to Read Books.
|
||||
|
||||
##### As [Anna’s Archive](https://annas-archive.org/) Doesn't Have An API.The App Works By Sending Request To Anna’s Archive And Parses The Response To objects.The App Extracts The Mirrors From Response And Downloads The Book And Store It In The Application Document Directory.
|
||||
##### As [Anna’s Archive](https://annas-archive.org/) Doesn't Have An API.The App Works By Sending Request To Anna’s Archive And Parses The Response To objects.The App Extracts The Mirrors From Response And Downloads The Book.
|
||||
|
||||
## Features
|
||||
- Trending Books
|
||||
- Trending Books
|
||||
- Download And Read Books With In-Built Viewer
|
||||
- Supports Epub And Pdf Formats
|
||||
- Open Books In Your Favourite Ebook Reader
|
||||
- Filter Books
|
||||
- Sort Books
|
||||
|
||||
|
||||
## Roadmap
|
||||
|
||||
- Additing More Book Format supports (cbz,cbr,azw3,etc...)
|
||||
- Adding More Book Format supports (cbz,cbr,azw3,etc...)
|
||||
- Adding Support For Background Downloads
|
||||
- Adding Support For Multiple Downloads
|
||||
|
||||
## Installation and updates
|
||||
- Download the APK from [GitHub Releases](https://github.com/dstark5/Openlib/releases) and install it.
|
||||
|
||||
## Building from Source
|
||||
|
||||
- If you don't have Flutter SDK installed, please visit official [Flutter](https://flutter.dev) site.
|
||||
@ -69,7 +78,9 @@ flutter build
|
||||
- The Build Will Be In './build/app/outputs/flutter-apk/app-release.apk'
|
||||
|
||||
## Contribution
|
||||
Whether you have ideas, design changes or even major code changes, help is always welcome. The app gets better and better with each contribution, no matter how big or small! If you'd like to get involved just fork the app make a pull request.
|
||||
Whether you have ideas, design changes or even major code changes, help is always welcome. The app gets better and better with each contribution, no matter how big or small!
|
||||
|
||||
If you'd like to get involved See [CONTRIBUTING.md](./CONTRIBUTING.md) for the guidelines.
|
||||
|
||||
## Issues
|
||||
|
||||
|
@ -79,6 +79,8 @@ android {
|
||||
// Signing with the debug keys for now, so `flutter run --release` works.
|
||||
// replace with "debug" when running on debug version
|
||||
signingConfig signingConfigs.release
|
||||
minifyEnabled false
|
||||
shrinkResources false
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -89,4 +91,4 @@ flutter {
|
||||
|
||||
dependencies {
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
||||
}
|
||||
}
|
@ -1,11 +1,34 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
>
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||
|
||||
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
|
||||
<uses-permission android:name="android.permission.READ_MEDIA_VIDEOS" />
|
||||
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />
|
||||
|
||||
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="29"/>
|
||||
|
||||
<queries>
|
||||
<intent>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data android:scheme="https" />
|
||||
</intent>
|
||||
</queries>
|
||||
|
||||
<application
|
||||
android:label="Openlib"
|
||||
android:name="${applicationName}"
|
||||
android:requestLegacyExternalStorage="true"
|
||||
android:usesCleartextTraffic="true"
|
||||
android:networkSecurityConfig="@xml/network_security_config"
|
||||
android:icon="@mipmap/launcher_icon">
|
||||
<activity
|
||||
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
android:launchMode="singleTop"
|
||||
@ -17,6 +40,7 @@
|
||||
the Android process has started. This theme is visible to the user
|
||||
while the Flutter UI initializes. After that, this theme continues
|
||||
to determine the Window background behind the Flutter UI. -->
|
||||
|
||||
<meta-data
|
||||
android:name="io.flutter.embedding.android.NormalTheme"
|
||||
android:resource="@style/NormalTheme"
|
||||
|
1
assets/captcha.svg
Normal file
After Width: | Height: | Size: 11 KiB |
15
fastlane/metadata/android/en-US/full_description.txt
Normal file
@ -0,0 +1,15 @@
|
||||
<p>
|
||||
<i>Openlib</i> is an open source app to download and read books from shadow library (<a href='https://annas-archive.org/' target='_blank' rel='nofollow noopener'>Anna’s Archive</a>). The App Has Built In Reader to Read Books.
|
||||
</p>
|
||||
<p>
|
||||
As <i>Anna’s Archive</i> doesn't have an API, the app works by sending requests to <i>Anna’s Archive</i> and parses the response to objects. The app extracts the mirrors from the responses, downloads the book and stores it in the application's document directory.
|
||||
</p>
|
||||
<p>Main Features:</p>
|
||||
<ul>
|
||||
<li>Trending Books</li>
|
||||
<li>Download And Read Books With In-Built Viewer</li>
|
||||
<li>Supports Epub And Pdf Formats</li>
|
||||
<li>Open Books With Your Favourite Ebooks Reader</li>
|
||||
<li>Filter Books</li>
|
||||
<li>Sort Books</li>
|
||||
</ul>
|
BIN
fastlane/metadata/android/en-US/images/icon.png
Normal file
After Width: | Height: | Size: 10 KiB |
After Width: | Height: | Size: 46 KiB |
After Width: | Height: | Size: 15 KiB |
After Width: | Height: | Size: 29 KiB |
After Width: | Height: | Size: 34 KiB |
After Width: | Height: | Size: 23 KiB |
After Width: | Height: | Size: 34 KiB |
After Width: | Height: | Size: 20 KiB |
After Width: | Height: | Size: 17 KiB |
1
fastlane/metadata/android/en-US/short_description.txt
Normal file
@ -0,0 +1 @@
|
||||
download and read books from shadow library (Anna’s Archive)
|
BIN
github_releases.png
Normal file
After Width: | Height: | Size: 15 KiB |
143
lib/main.dart
@ -1,29 +1,77 @@
|
||||
import 'dart:io' show Platform;
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:google_nav_bar/google_nav_bar.dart';
|
||||
import 'package:sqflite/sqflite.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import 'package:sqflite_common_ffi/sqflite_ffi.dart';
|
||||
|
||||
import 'package:openlib/ui/extensions.dart';
|
||||
import 'package:openlib/ui/themes.dart';
|
||||
import 'package:openlib/ui/trending_page.dart';
|
||||
import 'package:openlib/ui/search_page.dart';
|
||||
import 'package:openlib/ui/mylibrary_page.dart';
|
||||
import 'package:openlib/ui/settings_page.dart';
|
||||
import 'package:openlib/services/database.dart' show Sqlite, MyLibraryDb;
|
||||
import 'package:openlib/services/files.dart'
|
||||
show moveFilesToAndroidInternalStorage;
|
||||
import 'package:openlib/state/state.dart'
|
||||
show selectedIndexProvider, dbProvider;
|
||||
show
|
||||
selectedIndexProvider,
|
||||
themeModeProvider,
|
||||
openPdfWithExternalAppProvider,
|
||||
openEpubWithExternalAppProvider,
|
||||
userAgentProvider,
|
||||
cookieProvider,
|
||||
dbProvider;
|
||||
|
||||
void main() async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
Database db = await Sqlite.initDb();
|
||||
runApp(ProviderScope(
|
||||
overrides: [dbProvider.overrideWithValue(MyLibraryDb(dbInstance: db))],
|
||||
child: const MyApp()));
|
||||
|
||||
if (Platform.isWindows || Platform.isLinux || Platform.isMacOS) {
|
||||
sqfliteFfiInit();
|
||||
databaseFactory = databaseFactoryFfi;
|
||||
}
|
||||
|
||||
Database initDb = await Sqlite.initDb();
|
||||
MyLibraryDb dataBase = MyLibraryDb(dbInstance: initDb);
|
||||
bool isDarkMode = await dataBase.getPreference('darkMode');
|
||||
bool openPdfwithExternalapp =
|
||||
await dataBase.getPreference('openPdfwithExternalApp');
|
||||
bool openEpubwithExternalapp =
|
||||
await dataBase.getPreference('openEpubwithExternalApp');
|
||||
|
||||
String browserUserAgent = await dataBase.getBrowserOptions('userAgent');
|
||||
String browserCookie = await dataBase.getBrowserOptions('cookie');
|
||||
|
||||
if (Platform.isAndroid) {
|
||||
//[SystemChrome] Also change colors in settings page Theme colors if any change
|
||||
SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle(
|
||||
systemNavigationBarColor:
|
||||
isDarkMode ? Colors.black : Colors.grey.shade200));
|
||||
await moveFilesToAndroidInternalStorage();
|
||||
}
|
||||
|
||||
runApp(
|
||||
ProviderScope(
|
||||
overrides: [
|
||||
dbProvider.overrideWithValue(dataBase),
|
||||
themeModeProvider.overrideWith(
|
||||
(ref) => isDarkMode ? ThemeMode.dark : ThemeMode.light),
|
||||
openPdfWithExternalAppProvider
|
||||
.overrideWith((ref) => openPdfwithExternalapp),
|
||||
openEpubWithExternalAppProvider
|
||||
.overrideWith((ref) => openEpubwithExternalapp),
|
||||
userAgentProvider.overrideWith((ref) => browserUserAgent),
|
||||
cookieProvider.overrideWith((ref) => browserCookie),
|
||||
],
|
||||
child: const MyApp(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
class MyApp extends StatelessWidget {
|
||||
class MyApp extends ConsumerWidget {
|
||||
const MyApp({super.key});
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
return MaterialApp(
|
||||
builder: (BuildContext context, Widget? child) {
|
||||
return MediaQuery(
|
||||
@ -35,32 +83,9 @@ class MyApp extends StatelessWidget {
|
||||
},
|
||||
debugShowCheckedModeBanner: false,
|
||||
title: 'Openlib',
|
||||
theme: ThemeData(
|
||||
primaryColor: Colors.white,
|
||||
colorScheme: ColorScheme.light(
|
||||
primary: Colors.white,
|
||||
secondary: '#FB0101'.toColor(),
|
||||
tertiary: Colors.black,
|
||||
tertiaryContainer: '#F2F2F2'.toColor(),
|
||||
),
|
||||
textTheme: const TextTheme(
|
||||
displayLarge: TextStyle(
|
||||
color: Colors.black,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 21,
|
||||
),
|
||||
displayMedium: TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.black,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
fontFamily: GoogleFonts.nunito().fontFamily,
|
||||
useMaterial3: true,
|
||||
textSelectionTheme: TextSelectionThemeData(
|
||||
selectionColor: '#FB0101'.toColor(),
|
||||
selectionHandleColor: '#FB0101'.toColor())),
|
||||
theme: lightTheme,
|
||||
darkTheme: darkTheme,
|
||||
themeMode: ref.watch(themeModeProvider),
|
||||
home: const HomePage(),
|
||||
);
|
||||
}
|
||||
@ -77,11 +102,14 @@ class _HomePageState extends ConsumerState<HomePage> {
|
||||
static const List<Widget> _widgetOptions = <Widget>[
|
||||
TrendingPage(),
|
||||
SearchPage(),
|
||||
MyLibraryPage()
|
||||
MyLibraryPage(),
|
||||
SettingsPage()
|
||||
];
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isDarkMode = Theme.of(context).brightness == Brightness.dark;
|
||||
|
||||
final selectedIndex = ref.watch(selectedIndexProvider);
|
||||
|
||||
return Scaffold(
|
||||
@ -93,51 +121,60 @@ class _HomePageState extends ConsumerState<HomePage> {
|
||||
body: _widgetOptions.elementAt(selectedIndex),
|
||||
bottomNavigationBar: SafeArea(
|
||||
child: GNav(
|
||||
rippleColor: Colors.redAccent,
|
||||
backgroundColor: Colors.black,
|
||||
backgroundColor: isDarkMode ? Colors.black : Colors.grey.shade200,
|
||||
haptic: true,
|
||||
tabBorderRadius: 50,
|
||||
tabActiveBorder: Border.all(
|
||||
color: Theme.of(context).colorScheme.secondary,
|
||||
),
|
||||
tabMargin: const EdgeInsets.fromLTRB(13, 6, 13, 2.5),
|
||||
curve: Curves.easeInOut, // tab animation curves
|
||||
duration: const Duration(milliseconds: 150),
|
||||
curve: Curves.fastLinearToSlowEaseIn,
|
||||
duration: const Duration(milliseconds: 25),
|
||||
gap: 5,
|
||||
color: const Color.fromARGB(255, 255, 255, 255),
|
||||
activeColor: const Color.fromARGB(255, 255, 255, 255),
|
||||
iconSize: 21, // tab button icon size
|
||||
iconSize: 19, // tab button icon size
|
||||
tabBackgroundColor: Theme.of(context).colorScheme.secondary,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 15, vertical: 6.5),
|
||||
tabs: const [
|
||||
padding: const EdgeInsets.symmetric(horizontal: 13, vertical: 6.5),
|
||||
tabs: [
|
||||
GButton(
|
||||
icon: Icons.trending_up,
|
||||
text: 'Trending',
|
||||
iconColor: Colors.white,
|
||||
textStyle: TextStyle(
|
||||
iconColor: isDarkMode ? Colors.white : Colors.black,
|
||||
textStyle: const TextStyle(
|
||||
fontWeight: FontWeight.w900,
|
||||
color: Colors.white,
|
||||
fontSize: 13,
|
||||
fontSize: 11,
|
||||
),
|
||||
),
|
||||
GButton(
|
||||
icon: Icons.search,
|
||||
text: 'Search',
|
||||
iconColor: Colors.white,
|
||||
textStyle: TextStyle(
|
||||
iconColor: isDarkMode ? Colors.white : Colors.black,
|
||||
textStyle: const TextStyle(
|
||||
fontWeight: FontWeight.w900,
|
||||
color: Colors.white,
|
||||
fontSize: 13,
|
||||
fontSize: 11,
|
||||
),
|
||||
),
|
||||
GButton(
|
||||
icon: Icons.collections_bookmark,
|
||||
text: 'My Library',
|
||||
iconColor: Colors.white,
|
||||
textStyle: TextStyle(
|
||||
iconColor: isDarkMode ? Colors.white : Colors.black,
|
||||
textStyle: const TextStyle(
|
||||
fontWeight: FontWeight.w900,
|
||||
color: Colors.white,
|
||||
fontSize: 13,
|
||||
fontSize: 11,
|
||||
),
|
||||
),
|
||||
GButton(
|
||||
icon: Icons.build,
|
||||
text: 'Settings',
|
||||
iconColor: isDarkMode ? Colors.white : Colors.black,
|
||||
textStyle: const TextStyle(
|
||||
fontWeight: FontWeight.w900,
|
||||
color: Colors.white,
|
||||
fontSize: 11,
|
||||
),
|
||||
),
|
||||
],
|
||||
|
@ -1,4 +1,5 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'dart:convert';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:html/parser.dart' show parse;
|
||||
|
||||
@ -48,23 +49,25 @@ class BookInfoData extends BookData {
|
||||
}
|
||||
|
||||
class AnnasArchieve {
|
||||
String baseUrl = "https://annas-archive.gs";
|
||||
String baseUrl = "https://annas-archive.org";
|
||||
|
||||
final Dio dio = Dio(BaseOptions(headers: {
|
||||
'User-Agent':
|
||||
'Mozilla/5.0 (iPhone; CPU iPhone OS 12_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) FxiOS/7.0.4 Mobile/16B91 Safari/605.1.15'
|
||||
}));
|
||||
final Dio dio = Dio();
|
||||
|
||||
Map<String, dynamic> defaultDioHeaders = {
|
||||
"user-agent":
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.100 Safari/537.36",
|
||||
};
|
||||
|
||||
String getMd5(String url) {
|
||||
String md5 = url.toString().split('/').last;
|
||||
return md5;
|
||||
}
|
||||
|
||||
List<BookData> _parser(resData) {
|
||||
List<BookData> _parser(resData, String fileType) {
|
||||
var document =
|
||||
parse(resData.toString().replaceAll(RegExp(r"<!--|-->"), ''));
|
||||
var books = document.querySelectorAll(
|
||||
'a[class="js-vim-focus custom-a flex items-center relative left-[-10px] w-[calc(100%+20px)] px-[10px] py-2 outline-offset-[-2px] outline-2 rounded-[3px] hover:bg-[#00000011] focus:outline "]');
|
||||
'a[class="js-vim-focus custom-a flex items-center relative left-[-10px] w-[calc(100%+20px)] px-[10px] outline-offset-[-2px] outline-2 rounded-[3px] hover:bg-[#00000011] focus:outline "]');
|
||||
|
||||
List<BookData> bookList = [];
|
||||
|
||||
@ -73,21 +76,32 @@ class AnnasArchieve {
|
||||
'title': element.querySelector('h3')?.text,
|
||||
'thumbnail': element.querySelector('img')?.attributes['src'],
|
||||
'link': element.attributes['href'],
|
||||
'author': element.querySelector('div[class="truncate italic"]')?.text ??
|
||||
'author': element
|
||||
.querySelector(
|
||||
'div[class="max-lg:line-clamp-[2] lg:truncate leading-[1.2] lg:leading-[1.35] max-lg:text-sm italic"]')
|
||||
?.text ??
|
||||
'unknown',
|
||||
'publisher':
|
||||
element.querySelector('div[class="truncate text-sm"]')?.text ??
|
||||
"unknown",
|
||||
'publisher': element
|
||||
.querySelector(
|
||||
'div[class="truncate leading-[1.2] lg:leading-[1.35] max-lg:text-xs"]')
|
||||
?.text ??
|
||||
"unknown",
|
||||
'info': element
|
||||
.querySelector('div[class="truncate text-xs text-gray-500"]')
|
||||
.querySelector(
|
||||
'div[class="line-clamp-[2] leading-[1.2] text-[10px] lg:text-xs text-gray-500"]')
|
||||
?.text ??
|
||||
''
|
||||
};
|
||||
|
||||
if ((data['title'] != null && data['title'] != '') &&
|
||||
(data['link'] != null && data['link'] != '') &&
|
||||
(data['info'] != null &&
|
||||
(data['info']!.contains('pdf') ||
|
||||
data['info']!.contains('epub')))) {
|
||||
((fileType == "") &&
|
||||
(data['info']!.contains('pdf') ||
|
||||
data['info']!.contains('epub') ||
|
||||
data['info']!.contains('cbr') ||
|
||||
data['info']!.contains('cbz')) ||
|
||||
((fileType != "") && data['info']!.contains(fileType))))) {
|
||||
String link = baseUrl + data['link']!;
|
||||
String publisher = ((data['publisher']?.contains('0') == true &&
|
||||
data['publisher']!.length < 2) ||
|
||||
@ -115,46 +129,71 @@ class AnnasArchieve {
|
||||
if (info.contains('pdf') == true) {
|
||||
return 'pdf';
|
||||
} else {
|
||||
return 'epub';
|
||||
if (info.contains('cbr')) return "cbr";
|
||||
if (info.contains('cbz')) return "cbz";
|
||||
return "epub";
|
||||
}
|
||||
}
|
||||
|
||||
Future<String?> _getMirrorLink(String url) async {
|
||||
Future<String?> _getMirrorLink(
|
||||
String url, String userAgent, String cookie) async {
|
||||
try {
|
||||
final response = await dio.get(url);
|
||||
var document = parse(response.toString());
|
||||
var pTag = document.querySelector('p[class="mb-4"]');
|
||||
String? link = pTag?.querySelector('a')?.attributes['href'];
|
||||
final response = await dio.get(url,
|
||||
options: Options(extra: {
|
||||
'withCredentials': true
|
||||
}, headers: {
|
||||
"Host": "annas-archive.org",
|
||||
"Origin": "https://annas-archive.org",
|
||||
"Upgrade-Insecure-Requests": "1",
|
||||
"Sec-Fetch-Dest": "secure",
|
||||
"Sec-Fetch-Mode": "navigate",
|
||||
"Sec-Fetch-Site": "same-site",
|
||||
"Cookie": cookie,
|
||||
"User-Agent": userAgent
|
||||
}));
|
||||
|
||||
var document = parse(response.data.toString());
|
||||
|
||||
var pTag = document.querySelectorAll('p[class="mb-4"]');
|
||||
String? link = pTag[1].querySelector('a')?.attributes['href'];
|
||||
return link;
|
||||
} catch (e) {
|
||||
print(e);
|
||||
// print('${url} ${e}');
|
||||
if (e.toString().contains("403")) {
|
||||
throw jsonEncode({"code": "403", "url": url});
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Future<BookInfoData?> _bookInfoParser(resData, url) async {
|
||||
Future<BookInfoData?> _bookInfoParser(resData, url, userAgent, cookie) async {
|
||||
var document = parse(resData.toString());
|
||||
var main = document.querySelector('main[class="main"]');
|
||||
var ul = main?.querySelector('ul[class="mb-4"]');
|
||||
var ul = main?.querySelectorAll('ul[class="mb-4"]');
|
||||
|
||||
List<String> mirrors = [];
|
||||
|
||||
if (ul != null) {
|
||||
var a = ul.querySelectorAll('a');
|
||||
var anchorTags = [];
|
||||
if (ul.length == 2) {
|
||||
anchorTags = ul[1].querySelectorAll('a');
|
||||
} else {
|
||||
anchorTags = ul[0].querySelectorAll('a');
|
||||
}
|
||||
|
||||
for (var element in a) {
|
||||
for (var element in anchorTags) {
|
||||
if (element.attributes['href']!.startsWith('https://')) {
|
||||
if (element.attributes['href'] != null) {
|
||||
if (element.attributes['href'] != null &&
|
||||
element.attributes['href'].startsWith('https://1lib.sk') !=
|
||||
true) {
|
||||
mirrors.add(element.attributes['href']!);
|
||||
}
|
||||
} else if (element.attributes['href'] != null &&
|
||||
element.attributes['href']!.startsWith('/slow_download')) {
|
||||
if (element.text.contains('Slow Partner Server #1') != true) {
|
||||
String? url =
|
||||
await _getMirrorLink('$baseUrl${element.attributes['href']!}');
|
||||
if (url != null && url.isNotEmpty) {
|
||||
mirrors.add(url);
|
||||
}
|
||||
String? url = await _getMirrorLink(
|
||||
'$baseUrl${element.attributes['href']!}', userAgent, cookie);
|
||||
if (url != null && url.isNotEmpty) {
|
||||
mirrors.add(url);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -210,12 +249,16 @@ class AnnasArchieve {
|
||||
Future<List<BookData>> searchBooks(
|
||||
{required String searchQuery,
|
||||
String content = "",
|
||||
String sort = ""}) async {
|
||||
String sort = "",
|
||||
String fileType = ""}) async {
|
||||
try {
|
||||
final response = await dio.get(
|
||||
'$baseUrl/search?lang=&content=$content&ext=&sort=$sort&q=$searchQuery',
|
||||
);
|
||||
return _parser(response.data);
|
||||
final String encodedURL = content == ""
|
||||
? '$baseUrl/search?index=&q=$searchQuery&ext=$fileType&sort=$sort'
|
||||
: '$baseUrl/search?index=&q=$searchQuery&content=$content&ext=$fileType&sort=$sort';
|
||||
|
||||
final response = await dio.get(encodedURL,
|
||||
options: Options(headers: defaultDioHeaders));
|
||||
return _parser(response.data, fileType);
|
||||
} on DioException catch (e) {
|
||||
if (e.type == DioExceptionType.unknown) {
|
||||
throw "socketException";
|
||||
@ -224,10 +267,15 @@ class AnnasArchieve {
|
||||
}
|
||||
}
|
||||
|
||||
Future<BookInfoData> bookInfo({required String url}) async {
|
||||
Future<BookInfoData> bookInfo(
|
||||
{required String url,
|
||||
required String userAgent,
|
||||
required String cookie}) async {
|
||||
try {
|
||||
final response = await dio.get(url);
|
||||
BookInfoData? data = await _bookInfoParser(response.data, url);
|
||||
final response =
|
||||
await dio.get(url, options: Options(headers: defaultDioHeaders));
|
||||
BookInfoData? data =
|
||||
await _bookInfoParser(response.data, url, userAgent, cookie);
|
||||
if (data != null) {
|
||||
return data;
|
||||
} else {
|
||||
|
@ -1,15 +1,48 @@
|
||||
import 'dart:io';
|
||||
import 'package:sqflite/sqflite.dart';
|
||||
|
||||
class Sqlite {
|
||||
static Future<Database> initDb() async {
|
||||
var databasesPath = await getDatabasesPath();
|
||||
String path = '$databasesPath/mylibrary.db';
|
||||
bool isMobile = Platform.isAndroid || Platform.isIOS;
|
||||
|
||||
Database dbInstance = await openDatabase(path, version: 1,
|
||||
onCreate: (Database db, int version) async {
|
||||
await db.execute(
|
||||
'CREATE TABLE mybooks (id TEXT PRIMARY KEY, title TEXT,author TEXT,thumbnail TEXT,link TEXT,publisher TEXT,info TEXT,format TEXT,description TEXT)');
|
||||
});
|
||||
Database dbInstance = await openDatabase(
|
||||
path,
|
||||
version: 5,
|
||||
onCreate: (Database db, int version) async {
|
||||
await db.execute(
|
||||
'CREATE TABLE mybooks (id TEXT PRIMARY KEY, title TEXT,author TEXT,thumbnail TEXT,link TEXT,publisher TEXT,info TEXT,format TEXT,description TEXT)');
|
||||
await db.execute(
|
||||
'CREATE TABLE preferences (name TEXT PRIMARY KEY,value BOOLEAN)');
|
||||
if (isMobile) {
|
||||
await db.execute(
|
||||
'CREATE TABLE bookposition (fileName TEXT PRIMARY KEY,position TEXT)');
|
||||
await db.execute(
|
||||
'CREATE TABLE browserOptions (name TEXT PRIMARY KEY,value TEXT)');
|
||||
}
|
||||
},
|
||||
onUpgrade: (db, oldVersion, newVersion) async {
|
||||
List<dynamic> isTableExist = await db.query('sqlite_master',
|
||||
where: 'name = ?', whereArgs: ['bookposition']);
|
||||
List<dynamic> isPreferenceTableExist = await db.query('sqlite_master',
|
||||
where: 'name = ?', whereArgs: ['preferences']);
|
||||
List<dynamic> isbrowserOptionsExist = await db.query('sqlite_master',
|
||||
where: 'name = ?', whereArgs: ['browserOptions']);
|
||||
if (isPreferenceTableExist.isEmpty) {
|
||||
await db.execute(
|
||||
'CREATE TABLE preferences (name TEXT PRIMARY KEY,value BOOLEAN)');
|
||||
}
|
||||
if (isMobile && isTableExist.isEmpty) {
|
||||
await db.execute(
|
||||
'CREATE TABLE bookposition (fileName TEXT PRIMARY KEY,position TEXT)');
|
||||
}
|
||||
if (isMobile && isbrowserOptionsExist.isEmpty) {
|
||||
await db.execute(
|
||||
'CREATE TABLE browserOptions (name TEXT PRIMARY KEY,value TEXT)');
|
||||
}
|
||||
},
|
||||
);
|
||||
return dbInstance;
|
||||
}
|
||||
}
|
||||
@ -118,4 +151,76 @@ class MyLibraryDb {
|
||||
});
|
||||
return myBookList.reversed.toList();
|
||||
}
|
||||
|
||||
Future<void> saveBookState(String fileName, String position) async {
|
||||
await dbInstance.insert(
|
||||
'bookposition',
|
||||
{'fileName': fileName, 'position': position},
|
||||
conflictAlgorithm: ConflictAlgorithm.replace,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> deleteBookState(String fileName) async {
|
||||
await dbInstance.delete(
|
||||
'bookposition',
|
||||
where: 'fileName = ?',
|
||||
whereArgs: [fileName],
|
||||
);
|
||||
}
|
||||
|
||||
Future<String?> getBookState(String fileName) async {
|
||||
List<Map<String, dynamic>> data = await dbInstance
|
||||
.query('bookposition', where: 'fileName = ?', whereArgs: [fileName]);
|
||||
List<dynamic> dataList = List.generate(data.length, (i) {
|
||||
return {'fileName': data[i]['fileName'], 'position': data[i]['position']};
|
||||
});
|
||||
if (dataList.isNotEmpty) {
|
||||
return dataList[0]['position'];
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> savePreference(String name, bool value) async {
|
||||
int boolInt = value ? 1 : 0;
|
||||
await dbInstance.insert(
|
||||
'preferences',
|
||||
{'name': name, 'value': boolInt},
|
||||
conflictAlgorithm: ConflictAlgorithm.replace,
|
||||
);
|
||||
}
|
||||
|
||||
Future<bool> getPreference(String name) async {
|
||||
List<Map<String, dynamic>> data = await dbInstance
|
||||
.query('preferences', where: 'name = ?', whereArgs: [name]);
|
||||
List<dynamic> dataList = List.generate(data.length, (i) {
|
||||
return {'name': data[i]['name'], 'value': data[i]['value']};
|
||||
});
|
||||
if (dataList.isNotEmpty) {
|
||||
return dataList[0]['value'] == 0 ? false : true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> setBrowserOptions(String name, String value) async {
|
||||
await dbInstance.insert(
|
||||
'browserOptions',
|
||||
{'name': name, 'value': value},
|
||||
conflictAlgorithm: ConflictAlgorithm.replace,
|
||||
);
|
||||
}
|
||||
|
||||
Future<String> getBrowserOptions(String name) async {
|
||||
List<Map<String, dynamic>> data = await dbInstance
|
||||
.query('browserOptions', where: 'name = ?', whereArgs: [name]);
|
||||
List<dynamic> dataList = List.generate(data.length, (i) {
|
||||
return {'name': data[i]['name'], 'value': data[i]['value']};
|
||||
});
|
||||
if (dataList.isNotEmpty) {
|
||||
return dataList[0]['value'];
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,9 +1,9 @@
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'files.dart';
|
||||
|
||||
Future<String> _getFilePath(String fileName) async {
|
||||
final path = await getApplicationDocumentsDirectory();
|
||||
return '${path.path}/$fileName';
|
||||
final path = await getAppDirectoryPath;
|
||||
return '$path/$fileName';
|
||||
}
|
||||
|
||||
List<String> _reorderMirrors(List<String> mirrors) {
|
||||
@ -14,7 +14,8 @@ List<String> _reorderMirrors(List<String> mirrors) {
|
||||
if (element.contains('ipfs') == true) {
|
||||
ipfsMirrors.add(element);
|
||||
} else {
|
||||
if (element.startsWith('https://annas-archive.gs') != true) {
|
||||
if (element.startsWith('https://annas-archive.org') != true &&
|
||||
element.startsWith('https://1lib.sk') != true) {
|
||||
httpsMirrors.add(element);
|
||||
}
|
||||
}
|
||||
@ -22,45 +23,72 @@ List<String> _reorderMirrors(List<String> mirrors) {
|
||||
return [...ipfsMirrors, ...httpsMirrors];
|
||||
}
|
||||
|
||||
Future<String?> _getAliveMirror(List<String> mirrors, Dio dio) async {
|
||||
for (var url in mirrors) {
|
||||
try {
|
||||
final response = await dio.head(url,
|
||||
options: Options(receiveTimeout: const Duration(seconds: 5)));
|
||||
if (response.statusCode == 200) {
|
||||
return url;
|
||||
}
|
||||
} catch (e) {
|
||||
// print("timeOut");
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
Future<void> downloadFile(
|
||||
{required List<String> mirrors,
|
||||
required String md5,
|
||||
required String format,
|
||||
required Function onProgress,
|
||||
required Function cancelDownlaod,
|
||||
required Function mirrorStatus,
|
||||
required Function onDownlaodFailed}) async {
|
||||
Dio dio = Dio();
|
||||
|
||||
String path = await _getFilePath('$md5.$format');
|
||||
List<String> orderedMirrors = _reorderMirrors(mirrors);
|
||||
|
||||
String? workingMirror = await _getAliveMirror(orderedMirrors, dio);
|
||||
|
||||
// print(workingMirror);
|
||||
// print(path);
|
||||
// print(orderedMirrors);
|
||||
// print(orderedMirrors[0]);
|
||||
|
||||
try {
|
||||
CancelToken cancelToken = CancelToken();
|
||||
if (workingMirror != null) {
|
||||
try {
|
||||
CancelToken cancelToken = CancelToken();
|
||||
|
||||
dio.download(
|
||||
orderedMirrors[0],
|
||||
path,
|
||||
options: Options(headers: {
|
||||
'Connection': 'Keep-Alive',
|
||||
'User-Agent':
|
||||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.100 Safari/537.36'
|
||||
}),
|
||||
onReceiveProgress: (rcv, total) {
|
||||
onProgress(rcv, total);
|
||||
},
|
||||
deleteOnError: true,
|
||||
cancelToken: cancelToken,
|
||||
).catchError((err) {
|
||||
if (err.type != DioExceptionType.cancel) {
|
||||
onDownlaodFailed();
|
||||
}
|
||||
throw err;
|
||||
});
|
||||
dio.download(
|
||||
workingMirror,
|
||||
path,
|
||||
options: Options(headers: {
|
||||
'Connection': 'Keep-Alive',
|
||||
'User-Agent':
|
||||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.100 Safari/537.36'
|
||||
}),
|
||||
onReceiveProgress: (rcv, total) {
|
||||
onProgress(rcv, total);
|
||||
},
|
||||
deleteOnError: true,
|
||||
cancelToken: cancelToken,
|
||||
).catchError((err) {
|
||||
if (err.type != DioExceptionType.cancel) {
|
||||
onDownlaodFailed();
|
||||
}
|
||||
throw err;
|
||||
});
|
||||
|
||||
cancelDownlaod(cancelToken);
|
||||
} catch (e) {
|
||||
mirrorStatus(true);
|
||||
|
||||
cancelDownlaod(cancelToken);
|
||||
} catch (e) {
|
||||
onDownlaodFailed();
|
||||
}
|
||||
} else {
|
||||
onDownlaodFailed();
|
||||
}
|
||||
}
|
||||
|
@ -4,8 +4,32 @@ import 'package:path_provider/path_provider.dart';
|
||||
import 'package:openlib/state/state.dart' show dbProvider, myLibraryProvider;
|
||||
|
||||
Future<String> get getAppDirectoryPath async {
|
||||
final directory = await getApplicationDocumentsDirectory();
|
||||
return directory.path;
|
||||
if (Platform.isAndroid) {
|
||||
final directory = await getExternalStorageDirectory();
|
||||
return directory!.path;
|
||||
} else {
|
||||
final directory = await getApplicationDocumentsDirectory();
|
||||
return directory.path;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> moveFilesToAndroidInternalStorage() async {
|
||||
try {
|
||||
final directory = await getApplicationDocumentsDirectory();
|
||||
final directoryExternal = await getExternalStorageDirectory();
|
||||
List<FileSystemEntity> files = Directory(directory.path).listSync();
|
||||
for (var element in files) {
|
||||
if ((element.path.contains('pdf')) || element.path.contains('epub')) {
|
||||
String fileName = element.path.split('/').last;
|
||||
File file = File(element.path);
|
||||
file.copySync('${directoryExternal!.path}/$fileName');
|
||||
file.deleteSync();
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// ignore: avoid_print
|
||||
print(e);
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> isFileExists(String filePath) async {
|
||||
@ -35,9 +59,11 @@ Future<void> deleteFileWithDbData(
|
||||
String appDirPath = await getAppDirectoryPath;
|
||||
await deleteFile('$appDirPath/$fileName');
|
||||
await ref.read(dbProvider).delete(md5);
|
||||
await ref.read(dbProvider).deleteBookState(fileName);
|
||||
// ignore: unused_result
|
||||
ref.refresh(myLibraryProvider);
|
||||
} catch (e) {
|
||||
print(e);
|
||||
// print(e);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
import 'dart:math';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:openlib/services/database.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
@ -29,8 +30,12 @@ Map<String, String> sortValues = {
|
||||
'Smallest': 'smallest',
|
||||
};
|
||||
|
||||
List<String> fileType = ["All", "PDF", "Epub", "Cbr", "Cbz"];
|
||||
|
||||
final selectedIndexProvider = StateProvider<int>((ref) => 0);
|
||||
|
||||
final themeModeProvider = StateProvider<ThemeMode>((ref) => ThemeMode.light);
|
||||
|
||||
final selectedTypeState = StateProvider<String>((ref) => "All");
|
||||
|
||||
final getTypeValue = Provider.autoDispose<String>((ref) {
|
||||
@ -43,6 +48,15 @@ final getSortValue = Provider.autoDispose<String>((ref) {
|
||||
return sortValues[ref.read(selectedSortState)] ?? '';
|
||||
});
|
||||
|
||||
final selectedFileTypeState = StateProvider<String>((ref) => "All");
|
||||
|
||||
final getFileTypeValue = Provider.autoDispose<String>((ref) {
|
||||
if (ref.read(selectedFileTypeState) == "All") {
|
||||
return '';
|
||||
}
|
||||
return ref.read(selectedFileTypeState).toLowerCase();
|
||||
});
|
||||
|
||||
//searchQueryProvider
|
||||
|
||||
final searchQueryProvider = StateProvider<String>((ref) => "");
|
||||
@ -61,21 +75,30 @@ final searchProvider = FutureProvider.family
|
||||
List<BookData> data = await annasArchieve.searchBooks(
|
||||
searchQuery: searchQuery,
|
||||
content: ref.watch(getTypeValue),
|
||||
sort: ref.watch(getSortValue));
|
||||
sort: ref.watch(getSortValue),
|
||||
fileType: ref.watch(getFileTypeValue));
|
||||
return data;
|
||||
});
|
||||
|
||||
final cookieProvider = StateProvider<String>((ref) => "");
|
||||
final userAgentProvider = StateProvider<String>((ref) => "");
|
||||
|
||||
//Provider for Book Info
|
||||
final bookInfoProvider =
|
||||
FutureProvider.family<BookInfoData, String>((ref, url) async {
|
||||
AnnasArchieve annasArchieve = AnnasArchieve();
|
||||
BookInfoData data = await annasArchieve.bookInfo(url: url);
|
||||
BookInfoData data = await annasArchieve.bookInfo(
|
||||
url: url,
|
||||
userAgent: ref.read(userAgentProvider),
|
||||
cookie: ref.read(cookieProvider));
|
||||
return data;
|
||||
});
|
||||
|
||||
final downloadProgressProvider =
|
||||
StateProvider.autoDispose<double>((ref) => 0.0);
|
||||
|
||||
final mirrorStatusProvider = StateProvider.autoDispose<bool>((ref) => false);
|
||||
|
||||
final totalFileSizeInBytes = StateProvider.autoDispose<int>((ref) => 0);
|
||||
final downloadedFileSizeInBytes = StateProvider.autoDispose<int>((ref) => 0);
|
||||
|
||||
@ -125,6 +148,25 @@ final deleteFileFromMyLib =
|
||||
final pdfCurrentPage = StateProvider.autoDispose<int>((ref) => 0);
|
||||
final totalPdfPage = StateProvider.autoDispose<int>((ref) => 0);
|
||||
|
||||
Future<void> savePdfState(String fileName, WidgetRef ref) async {
|
||||
String position = ref.watch(pdfCurrentPage).toString();
|
||||
await ref.watch(dbProvider).saveBookState(fileName, position);
|
||||
}
|
||||
|
||||
Future<void> saveEpubState(
|
||||
String fileName, String? position, WidgetRef ref) async {
|
||||
String pos = position ?? '';
|
||||
await ref.watch(dbProvider).saveBookState(fileName, pos);
|
||||
}
|
||||
|
||||
final getBookPosition =
|
||||
FutureProvider.family.autoDispose<String?, String>((ref, fileName) async {
|
||||
return await ref.read(dbProvider).getBookState(fileName);
|
||||
});
|
||||
|
||||
final openPdfWithExternalAppProvider = StateProvider<bool>((ref) => false);
|
||||
final openEpubWithExternalAppProvider = StateProvider<bool>((ref) => false);
|
||||
|
||||
final filePathProvider =
|
||||
FutureProvider.family<String, String>((ref, fileName) async {
|
||||
String path = await getFilePath(fileName);
|
||||
|
131
lib/ui/about_page.dart
Normal file
@ -0,0 +1,131 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
import 'package:openlib/ui/components/snack_bar_widget.dart';
|
||||
import 'package:openlib/ui/components/page_title_widget.dart';
|
||||
|
||||
class AboutPage extends StatelessWidget {
|
||||
const AboutPage({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
backgroundColor: Theme.of(context).colorScheme.primary,
|
||||
title: const Text("Openlib"),
|
||||
titleTextStyle: Theme.of(context).textTheme.displayLarge,
|
||||
),
|
||||
body: const SingleChildScrollView(
|
||||
physics: BouncingScrollPhysics(),
|
||||
scrollDirection: Axis.vertical,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(left: 5, right: 5, top: 10),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
TitleText("About"),
|
||||
Padding(
|
||||
padding:
|
||||
EdgeInsets.only(left: 7, right: 7, top: 13, bottom: 10),
|
||||
child: Text(
|
||||
"An Open source app to download and read books from shadow library (Anna`s Archive)",
|
||||
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: EdgeInsets.only(left: 7, right: 7, top: 10),
|
||||
child: Text(
|
||||
"Version",
|
||||
style: TextStyle(fontSize: 19, fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: EdgeInsets.only(left: 7, right: 7, top: 5),
|
||||
child: Text(
|
||||
"1.0.3",
|
||||
style: TextStyle(
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.grey),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: EdgeInsets.only(left: 7, right: 7, top: 15),
|
||||
child: Text(
|
||||
"Github",
|
||||
style: TextStyle(fontSize: 19, fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
_UrlText(
|
||||
text: 'Open Github Page',
|
||||
url: 'https://github.com/dstark5/Openlib',
|
||||
),
|
||||
_UrlText(
|
||||
text: 'Contribute To Openlib',
|
||||
url:
|
||||
'https://github.com/dstark5/Openlib/blob/main/CONTRIBUTING.md'),
|
||||
_UrlText(
|
||||
text: 'Report An Issue',
|
||||
url: 'https://github.com/dstark5/Openlib/issues'),
|
||||
Padding(
|
||||
padding: EdgeInsets.only(left: 7, right: 7, top: 15),
|
||||
child: Text(
|
||||
"Licence",
|
||||
style: TextStyle(fontSize: 19, fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
_UrlText(
|
||||
text: "GPL v3.0 license",
|
||||
url: 'https://www.gnu.org/licenses/gpl-3.0.en.html'),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _UrlText extends StatelessWidget {
|
||||
const _UrlText({Key? key, required this.text, required this.url})
|
||||
: super(key: key);
|
||||
|
||||
final String url;
|
||||
final String text;
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(left: 7, right: 7, top: 5),
|
||||
child: InkWell(
|
||||
onTap: () async {
|
||||
final Uri uri = Uri.parse(url);
|
||||
if (!await launchUrl(uri, mode: LaunchMode.externalApplication)) {
|
||||
// ignore: use_build_context_synchronously
|
||||
showSnackBar(context: context, message: 'Could not launch $uri');
|
||||
}
|
||||
},
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
text,
|
||||
style: const TextStyle(
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.blueAccent,
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
width: 2,
|
||||
),
|
||||
const Icon(
|
||||
Icons.launch,
|
||||
size: 17,
|
||||
color: Colors.blueAccent,
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -1,5 +1,7 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'dart:convert';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/scheduler.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import 'package:openlib/services/database.dart';
|
||||
@ -15,12 +17,15 @@ import 'package:openlib/state/state.dart'
|
||||
getTotalFileSize,
|
||||
getDownloadedFileSize,
|
||||
cancelCurrentDownload,
|
||||
mirrorStatusProvider,
|
||||
dbProvider,
|
||||
checkIdExists,
|
||||
myLibraryProvider;
|
||||
import 'package:openlib/ui/components/book_info_widget.dart';
|
||||
import 'package:openlib/ui/components/file_buttons_widget.dart';
|
||||
import 'package:openlib/ui/components/snack_bar_widget.dart';
|
||||
import 'package:flutter_svg/svg.dart';
|
||||
import 'package:openlib/ui/webview_page.dart';
|
||||
|
||||
class BookInfoPage extends ConsumerWidget {
|
||||
const BookInfoPage({Key? key, required this.url}) : super(key: key);
|
||||
@ -37,19 +42,97 @@ class BookInfoPage extends ConsumerWidget {
|
||||
titleTextStyle: Theme.of(context).textTheme.displayLarge,
|
||||
),
|
||||
body: bookInfo.when(
|
||||
skipLoadingOnRefresh: false,
|
||||
data: (data) {
|
||||
return BookInfoWidget(
|
||||
data: data, child: ActionButtonWidget(data: data));
|
||||
},
|
||||
error: (err, _) {
|
||||
return CustomErrorWidget(
|
||||
error: err,
|
||||
stackTrace: _,
|
||||
onRefresh: () {
|
||||
// ignore: unused_result
|
||||
ref.refresh(bookInfoProvider(url));
|
||||
},
|
||||
);
|
||||
if (err.toString().contains("403")) {
|
||||
var errJson = jsonDecode(err.toString());
|
||||
|
||||
if (SchedulerBinding.instance.schedulerPhase ==
|
||||
SchedulerPhase.persistentCallbacks) {
|
||||
SchedulerBinding.instance.addPostFrameCallback((_) {
|
||||
Future.delayed(
|
||||
const Duration(seconds: 3),
|
||||
() => Navigator.pushReplacement(context,
|
||||
MaterialPageRoute(builder: (BuildContext context) {
|
||||
return Webview(url: errJson["url"]);
|
||||
})));
|
||||
});
|
||||
}
|
||||
|
||||
return Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 210,
|
||||
child: SvgPicture.asset(
|
||||
'assets/captcha.svg',
|
||||
width: 210,
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 30,
|
||||
),
|
||||
Text(
|
||||
"Captcha required",
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Theme.of(context).textTheme.headlineMedium?.color,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Text(
|
||||
"you will be redirected to solve captcha",
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Theme.of(context).textTheme.headlineSmall?.color,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(30, 15, 30, 10),
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
decoration: BoxDecoration(
|
||||
color: const Color.fromARGB(255, 255, 186, 186),
|
||||
borderRadius: BorderRadius.circular(5)),
|
||||
child: const Padding(
|
||||
padding: EdgeInsets.all(10.0),
|
||||
child: Text(
|
||||
"If you have solved the captcha then you will be automatically redirected to the results page . In case you seeing this page even after completing try using a VPN .",
|
||||
textAlign: TextAlign.start,
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.black,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
||||
} else {
|
||||
return CustomErrorWidget(
|
||||
error: err,
|
||||
stackTrace: _,
|
||||
onRefresh: () {
|
||||
// ignore: unused_result
|
||||
ref.refresh(bookInfoProvider(url));
|
||||
},
|
||||
);
|
||||
}
|
||||
},
|
||||
loading: () {
|
||||
return Center(
|
||||
@ -161,6 +244,9 @@ Future<void> downloadFileWidget(
|
||||
cancelDownlaod: (CancelToken downloadToken) {
|
||||
ref.read(cancelCurrentDownload.notifier).state = downloadToken;
|
||||
},
|
||||
mirrorStatus: (val) {
|
||||
ref.read(mirrorStatusProvider.notifier).state = val;
|
||||
},
|
||||
onDownlaodFailed: () {
|
||||
Navigator.of(context).pop();
|
||||
showSnackBar(
|
||||
@ -185,6 +271,7 @@ class _ShowDialog extends ConsumerWidget {
|
||||
final downloadProgress = ref.watch(downloadProgressProvider);
|
||||
final fileSize = ref.watch(getTotalFileSize);
|
||||
final downloadedFileSize = ref.watch(getDownloadedFileSize);
|
||||
final mirrorStatus = ref.watch(mirrorStatusProvider);
|
||||
|
||||
if (downloadProgress == 1.0) {
|
||||
Navigator.of(context).pop();
|
||||
@ -197,10 +284,10 @@ class _ShowDialog extends ConsumerWidget {
|
||||
padding: const EdgeInsets.all(15.0),
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
height: 255,
|
||||
height: 285,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(15),
|
||||
color: Colors.white,
|
||||
color: Theme.of(context).colorScheme.tertiaryContainer,
|
||||
),
|
||||
padding: const EdgeInsets.fromLTRB(20, 20, 20, 20),
|
||||
child: SingleChildScrollView(
|
||||
@ -208,14 +295,14 @@ class _ShowDialog extends ConsumerWidget {
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Padding(
|
||||
padding: EdgeInsets.all(8),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: Text(
|
||||
"Downloading Book",
|
||||
style: TextStyle(
|
||||
fontSize: 19,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Color.fromARGB(255, 54, 54, 54),
|
||||
color: Theme.of(context).colorScheme.tertiary,
|
||||
decoration: TextDecoration.none),
|
||||
),
|
||||
),
|
||||
@ -223,16 +310,58 @@ class _ShowDialog extends ConsumerWidget {
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: Text(
|
||||
title,
|
||||
style: const TextStyle(
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.black54,
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.tertiary
|
||||
.withAlpha(170),
|
||||
decoration: TextDecoration.none),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
maxLines: 2,
|
||||
textAlign: TextAlign.start,
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
mirrorStatus
|
||||
? const Icon(
|
||||
Icons.check_circle,
|
||||
size: 15,
|
||||
color: Colors.green,
|
||||
)
|
||||
: SizedBox(
|
||||
width: 9,
|
||||
height: 9,
|
||||
child: CircularProgressIndicator(
|
||||
color:
|
||||
Theme.of(context).colorScheme.secondary,
|
||||
strokeWidth: 2.5,
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
width: 3,
|
||||
),
|
||||
Text(
|
||||
"Checking mirror availability",
|
||||
style: TextStyle(
|
||||
fontSize: 11.5,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.tertiary
|
||||
.withAlpha(140),
|
||||
decoration: TextDecoration.none),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
maxLines: 2,
|
||||
textAlign: TextAlign.start,
|
||||
),
|
||||
]),
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
@ -259,7 +388,10 @@ class _ShowDialog extends ConsumerWidget {
|
||||
borderRadius: const BorderRadius.all(Radius.circular(50)),
|
||||
child: LinearProgressIndicator(
|
||||
color: Theme.of(context).colorScheme.secondary,
|
||||
backgroundColor: Colors.black26,
|
||||
backgroundColor: Theme.of(context)
|
||||
.colorScheme
|
||||
.tertiary
|
||||
.withAlpha(50),
|
||||
value: downloadProgress,
|
||||
minHeight: 4,
|
||||
),
|
||||
|
@ -2,6 +2,18 @@ import 'package:flutter/material.dart';
|
||||
import 'package:openlib/ui/extensions.dart';
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
|
||||
String? getFileType(String? info) {
|
||||
if (info != null && info.isNotEmpty) {
|
||||
info = info.toLowerCase();
|
||||
if (info.contains('pdf')) return "PDF";
|
||||
if (info.contains('epub')) return "Epub";
|
||||
if (info.contains('cbr')) return "Cbr";
|
||||
if (info.contains('cbz')) return "Cbz";
|
||||
return null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
class BookInfoCard extends StatelessWidget {
|
||||
const BookInfoCard(
|
||||
{Key? key,
|
||||
@ -9,6 +21,7 @@ class BookInfoCard extends StatelessWidget {
|
||||
required this.author,
|
||||
required this.publisher,
|
||||
required this.thumbnail,
|
||||
required this.info,
|
||||
required this.link,
|
||||
required this.onClick})
|
||||
: super(key: key);
|
||||
@ -17,11 +30,14 @@ class BookInfoCard extends StatelessWidget {
|
||||
final String author;
|
||||
final String publisher;
|
||||
final String? thumbnail;
|
||||
final String? info;
|
||||
final String link;
|
||||
final VoidCallback onClick;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
String? fileType = getFileType(info);
|
||||
|
||||
return InkWell(
|
||||
onTap: onClick,
|
||||
child: Container(
|
||||
@ -81,10 +97,10 @@ class BookInfoCard extends StatelessWidget {
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
style: const TextStyle(
|
||||
style: TextStyle(
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.black,
|
||||
color: Theme.of(context).colorScheme.tertiary,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
maxLines: 2,
|
||||
@ -94,20 +110,56 @@ class BookInfoCard extends StatelessWidget {
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: "#4D4D4D".toColor(),
|
||||
color:
|
||||
Theme.of(context).textTheme.headlineMedium?.color,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
maxLines: 1,
|
||||
),
|
||||
Text(
|
||||
author,
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: "#7B7B7B".toColor(),
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
maxLines: 1,
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
if (fileType != null)
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: "#a5a5a5".toColor(),
|
||||
borderRadius: BorderRadius.circular(2.5),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(3, 2, 3, 2),
|
||||
child: Text(
|
||||
fileType,
|
||||
style: const TextStyle(
|
||||
fontSize: 8.5,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
letterSpacing: 0.5,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
maxLines: 1,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (fileType != null)
|
||||
const SizedBox(
|
||||
width: 3,
|
||||
),
|
||||
Expanded(
|
||||
child: Text(
|
||||
author,
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Theme.of(context)
|
||||
.textTheme
|
||||
.headlineSmall
|
||||
?.color,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
maxLines: 1,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
|
@ -1,5 +1,4 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:openlib/ui/extensions.dart';
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
|
||||
class BookInfoWidget extends StatelessWidget {
|
||||
@ -11,6 +10,9 @@ class BookInfoWidget extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
String description = data.description.toString().length < 3
|
||||
? "No Description available"
|
||||
: data.description.toString();
|
||||
return SingleChildScrollView(
|
||||
physics: const BouncingScrollPhysics(),
|
||||
scrollDirection: Axis.vertical,
|
||||
@ -65,28 +67,32 @@ class BookInfoWidget extends StatelessWidget {
|
||||
text: data.title,
|
||||
fontSize: 19,
|
||||
topPadding: 15,
|
||||
color: Colors.black,
|
||||
color: Theme.of(context).colorScheme.tertiary,
|
||||
maxLines: 7,
|
||||
),
|
||||
_TopPaddedText(
|
||||
text: data.publisher ?? "unknown",
|
||||
fontSize: 15,
|
||||
topPadding: 7,
|
||||
color: "#595E60".toColor(),
|
||||
color: Theme.of(context).textTheme.headlineMedium!.color!,
|
||||
maxLines: 4,
|
||||
),
|
||||
_TopPaddedText(
|
||||
text: data.author ?? "unknown",
|
||||
fontSize: 13,
|
||||
topPadding: 7,
|
||||
color: "#7F7F7F".toColor(),
|
||||
color: Theme.of(context).textTheme.headlineSmall!.color!,
|
||||
maxLines: 3,
|
||||
),
|
||||
_TopPaddedText(
|
||||
text: data.info ?? "",
|
||||
fontSize: 11,
|
||||
topPadding: 9,
|
||||
color: "#A9A8A2".toColor(),
|
||||
color: Theme.of(context)
|
||||
.textTheme
|
||||
.headlineSmall!
|
||||
.color!
|
||||
.withAlpha(155),
|
||||
maxLines: 4,
|
||||
),
|
||||
// child slot of page
|
||||
@ -108,11 +114,12 @@ class BookInfoWidget extends StatelessWidget {
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 7, bottom: 10),
|
||||
child: Text(
|
||||
data.description ?? "",
|
||||
description,
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: "#6B6B6B".toColor(),
|
||||
color:
|
||||
Theme.of(context).colorScheme.tertiary.withAlpha(150),
|
||||
letterSpacing: 1.5,
|
||||
),
|
||||
),
|
||||
|
@ -26,7 +26,7 @@ class ShowDeleteDialog extends ConsumerWidget {
|
||||
height: 219,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(15),
|
||||
color: Colors.white,
|
||||
color: Theme.of(context).colorScheme.tertiaryContainer,
|
||||
),
|
||||
padding: const EdgeInsets.fromLTRB(20, 50, 20, 20),
|
||||
child: SingleChildScrollView(
|
||||
@ -34,25 +34,28 @@ class ShowDeleteDialog extends ConsumerWidget {
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Padding(
|
||||
padding: EdgeInsets.all(8),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: Text(
|
||||
"Delete Book",
|
||||
style: TextStyle(
|
||||
fontSize: 19,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Color.fromARGB(255, 54, 54, 54),
|
||||
color: Theme.of(context).colorScheme.tertiary,
|
||||
decoration: TextDecoration.none),
|
||||
),
|
||||
),
|
||||
const Padding(
|
||||
padding: EdgeInsets.all(8),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: Text(
|
||||
"This is permanent and cannot be undone",
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.black54,
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.tertiary
|
||||
.withAlpha(170),
|
||||
decoration: TextDecoration.none),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
maxLines: 2,
|
||||
@ -88,14 +91,14 @@ class ShowDeleteDialog extends ConsumerWidget {
|
||||
|
||||
onDelete();
|
||||
},
|
||||
child: const Padding(
|
||||
padding: EdgeInsets.all(5.0),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(5.0),
|
||||
child: Text(
|
||||
'Delete',
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.black,
|
||||
color: Theme.of(context).colorScheme.tertiary,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
@ -1,10 +1,14 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:open_file/open_file.dart';
|
||||
|
||||
import 'package:openlib/services/files.dart' show getFilePath;
|
||||
import 'package:openlib/ui/components/snack_bar_widget.dart';
|
||||
import 'package:openlib/ui/components/delete_dialog_widget.dart';
|
||||
import 'package:openlib/ui/epub_viewer.dart';
|
||||
import 'package:openlib/ui/pdf_viewer.dart';
|
||||
import 'package:openlib/ui/epub_viewer.dart' show launchEpubViewer;
|
||||
import 'package:openlib/ui/pdf_viewer.dart' show launchPdfViewer;
|
||||
|
||||
class FileOpenAndDeleteButtons extends StatelessWidget {
|
||||
class FileOpenAndDeleteButtons extends ConsumerWidget {
|
||||
final String id;
|
||||
final String format;
|
||||
final Function onDelete;
|
||||
@ -17,7 +21,7 @@ class FileOpenAndDeleteButtons extends StatelessWidget {
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(top: 21, bottom: 21),
|
||||
child: Row(
|
||||
@ -27,30 +31,21 @@ class FileOpenAndDeleteButtons extends StatelessWidget {
|
||||
TextButton(
|
||||
style: TextButton.styleFrom(
|
||||
backgroundColor: Theme.of(context).colorScheme.secondary,
|
||||
textStyle: const TextStyle(
|
||||
textStyle: TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w900,
|
||||
color: Colors.white,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
)),
|
||||
onPressed: () => {
|
||||
if (format == 'pdf')
|
||||
{
|
||||
Navigator.push(context,
|
||||
MaterialPageRoute(builder: (BuildContext context) {
|
||||
return PdfView(
|
||||
fileName: '$id.$format',
|
||||
);
|
||||
}))
|
||||
}
|
||||
else
|
||||
{
|
||||
Navigator.push(context,
|
||||
MaterialPageRoute(builder: (BuildContext context) {
|
||||
return EpubViewerWidget(
|
||||
fileName: '$id.$format',
|
||||
);
|
||||
}))
|
||||
}
|
||||
onPressed: () async {
|
||||
if (format == 'pdf') {
|
||||
await launchPdfViewer(
|
||||
fileName: '$id.$format', context: context, ref: ref);
|
||||
} else if (format == 'epub') {
|
||||
await launchEpubViewer(
|
||||
fileName: '$id.$format', context: context, ref: ref);
|
||||
} else {
|
||||
await openCbrAndCbz(fileName: '$id.$format', context: context);
|
||||
}
|
||||
},
|
||||
child: const Padding(
|
||||
padding: EdgeInsets.fromLTRB(17, 8, 17, 8),
|
||||
@ -82,14 +77,14 @@ class FileOpenAndDeleteButtons extends StatelessWidget {
|
||||
);
|
||||
});
|
||||
},
|
||||
child: const Padding(
|
||||
padding: EdgeInsets.all(5.3),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(5.3),
|
||||
child: Text(
|
||||
'Delete',
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.black,
|
||||
color: Theme.of(context).colorScheme.tertiary,
|
||||
),
|
||||
),
|
||||
),
|
||||
@ -99,3 +94,16 @@ class FileOpenAndDeleteButtons extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> openCbrAndCbz(
|
||||
{required String fileName, required BuildContext context}) async {
|
||||
try {
|
||||
String path = await getFilePath(fileName);
|
||||
await OpenFile.open(path);
|
||||
} catch (e) {
|
||||
// ignore: avoid_print
|
||||
// print(e);
|
||||
// ignore: use_build_context_synchronously
|
||||
showSnackBar(context: context, message: 'Unable to open pdf!');
|
||||
}
|
||||
}
|
||||
|
@ -1,9 +1,67 @@
|
||||
import 'dart:io';
|
||||
import 'dart:convert';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:epub_view/epub_view.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:vocsy_epub_viewer/epub_viewer.dart';
|
||||
import 'package:open_file/open_file.dart';
|
||||
|
||||
import 'package:openlib/state/state.dart' show filePathProvider;
|
||||
import 'package:openlib/services/files.dart' show getFilePath;
|
||||
import 'package:openlib/ui/components/snack_bar_widget.dart';
|
||||
import 'package:openlib/state/state.dart'
|
||||
show
|
||||
filePathProvider,
|
||||
saveEpubState,
|
||||
dbProvider,
|
||||
getBookPosition,
|
||||
openEpubWithExternalAppProvider;
|
||||
|
||||
Future<void> launchEpubViewer(
|
||||
{required String fileName,
|
||||
required BuildContext context,
|
||||
required WidgetRef ref}) async {
|
||||
if (Platform.isAndroid || Platform.isIOS) {
|
||||
String path = await getFilePath(fileName);
|
||||
String? epubConfig = await ref.read(dbProvider).getBookState(fileName);
|
||||
bool openWithExternalApp = ref.watch(openEpubWithExternalAppProvider);
|
||||
|
||||
if (openWithExternalApp) {
|
||||
await OpenFile.open(path);
|
||||
} else {
|
||||
try {
|
||||
VocsyEpub.setConfig(
|
||||
// ignore: use_build_context_synchronously
|
||||
themeColor: const Color.fromARGB(255, 210, 15, 1),
|
||||
identifier: "iosBook",
|
||||
scrollDirection: EpubScrollDirection.HORIZONTAL,
|
||||
);
|
||||
|
||||
if ((epubConfig?.isNotEmpty ?? true) &&
|
||||
(epubConfig != null) &&
|
||||
(!(epubConfig.startsWith('epubcfi')))) {
|
||||
VocsyEpub.open(path,
|
||||
lastLocation: EpubLocator.fromJson(json.decode(epubConfig)));
|
||||
} else {
|
||||
VocsyEpub.open(path);
|
||||
}
|
||||
|
||||
VocsyEpub.locatorStream.listen((locator) async {
|
||||
await saveEpubState(fileName, locator, ref);
|
||||
// convert locator from string to json and save to your database to be retrieved later
|
||||
});
|
||||
} catch (e) {
|
||||
// ignore: use_build_context_synchronously
|
||||
showSnackBar(context: context, message: 'Unable to open pdf!');
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Navigator.push(context, MaterialPageRoute(builder: (BuildContext context) {
|
||||
return EpubViewerWidget(
|
||||
fileName: fileName,
|
||||
);
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
class EpubViewerWidget extends ConsumerStatefulWidget {
|
||||
const EpubViewerWidget({super.key, required this.fileName});
|
||||
@ -19,7 +77,7 @@ class _EpubViewState extends ConsumerState<EpubViewerWidget> {
|
||||
Widget build(BuildContext context) {
|
||||
final filePath = ref.watch(filePathProvider(widget.fileName));
|
||||
return filePath.when(data: (data) {
|
||||
return EpubViewer(filePath: data);
|
||||
return EpubViewer(filePath: data, fileName: widget.fileName);
|
||||
}, error: (error, stack) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
@ -37,30 +95,34 @@ class _EpubViewState extends ConsumerState<EpubViewerWidget> {
|
||||
titleTextStyle: Theme.of(context).textTheme.displayLarge,
|
||||
),
|
||||
body: Center(
|
||||
child: SizedBox(
|
||||
width: 25,
|
||||
height: 25,
|
||||
child: CircularProgressIndicator(
|
||||
color: Theme.of(context).colorScheme.secondary,
|
||||
child: SizedBox(
|
||||
width: 25,
|
||||
height: 25,
|
||||
child: CircularProgressIndicator(
|
||||
color: Theme.of(context).colorScheme.secondary,
|
||||
),
|
||||
),
|
||||
)),
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class EpubViewer extends StatefulWidget {
|
||||
const EpubViewer({Key? key, required this.filePath}) : super(key: key);
|
||||
class EpubViewer extends ConsumerStatefulWidget {
|
||||
const EpubViewer({Key? key, required this.filePath, required this.fileName})
|
||||
: super(key: key);
|
||||
|
||||
final String filePath;
|
||||
final String fileName;
|
||||
|
||||
@override
|
||||
// ignore: library_private_types_in_public_api
|
||||
_EpubViewerState createState() => _EpubViewerState();
|
||||
}
|
||||
|
||||
class _EpubViewerState extends State<EpubViewer> {
|
||||
class _EpubViewerState extends ConsumerState<EpubViewer> {
|
||||
late EpubController _epubReaderController;
|
||||
String? epubConf;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@ -70,6 +132,14 @@ class _EpubViewerState extends State<EpubViewer> {
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void deactivate() {
|
||||
if (Platform.isAndroid || Platform.isIOS) {
|
||||
saveEpubState(widget.fileName, epubConf, ref);
|
||||
}
|
||||
super.deactivate();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_epubReaderController.dispose();
|
||||
@ -78,6 +148,7 @@ class _EpubViewerState extends State<EpubViewer> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final position = ref.watch(getBookPosition(widget.fileName));
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
backgroundColor: Theme.of(context).colorScheme.primary,
|
||||
@ -87,14 +158,48 @@ class _EpubViewerState extends State<EpubViewer> {
|
||||
endDrawer: Drawer(
|
||||
child: EpubViewTableOfContents(controller: _epubReaderController),
|
||||
),
|
||||
body: EpubView(
|
||||
onDocumentLoaded: (doc) {},
|
||||
onChapterChanged: (value) {},
|
||||
builders: EpubViewBuilders<DefaultBuilderOptions>(
|
||||
options: const DefaultBuilderOptions(),
|
||||
chapterDividerBuilder: (_) => const Divider(),
|
||||
),
|
||||
controller: _epubReaderController,
|
||||
body: position.when(
|
||||
data: (data) {
|
||||
return EpubView(
|
||||
onDocumentLoaded: (doc) {
|
||||
Future.delayed(const Duration(milliseconds: 20), () {
|
||||
String pos = data ?? "";
|
||||
_epubReaderController.gotoEpubCfi(pos);
|
||||
});
|
||||
},
|
||||
onChapterChanged: (value) {
|
||||
epubConf = _epubReaderController.generateEpubCfi();
|
||||
},
|
||||
builders: EpubViewBuilders<DefaultBuilderOptions>(
|
||||
options: const DefaultBuilderOptions(),
|
||||
chapterDividerBuilder: (_) => const Divider(),
|
||||
),
|
||||
controller: _epubReaderController,
|
||||
);
|
||||
},
|
||||
error: (err, _) {
|
||||
return EpubView(
|
||||
onChapterChanged: (value) {
|
||||
epubConf = _epubReaderController.generateEpubCfi();
|
||||
},
|
||||
builders: EpubViewBuilders<DefaultBuilderOptions>(
|
||||
options: const DefaultBuilderOptions(),
|
||||
chapterDividerBuilder: (_) => const Divider(),
|
||||
),
|
||||
controller: _epubReaderController,
|
||||
);
|
||||
},
|
||||
loading: () {
|
||||
return Center(
|
||||
child: SizedBox(
|
||||
width: 25,
|
||||
height: 25,
|
||||
child: CircularProgressIndicator(
|
||||
color: Theme.of(context).colorScheme.secondary,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -21,6 +21,7 @@ class MyLibraryPage extends ConsumerWidget {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(left: 5, right: 5, top: 10),
|
||||
child: CustomScrollView(
|
||||
physics: const BouncingScrollPhysics(),
|
||||
slivers: <Widget>[
|
||||
const SliverToBoxAdapter(
|
||||
child: TitleText("My Library"),
|
||||
@ -34,6 +35,7 @@ class MyLibraryPage extends ConsumerWidget {
|
||||
author: i.author ?? "",
|
||||
publisher: i.publisher ?? "",
|
||||
thumbnail: i.thumbnail,
|
||||
info: i.info,
|
||||
link: i.link,
|
||||
onClick: () {
|
||||
Navigator.push(context, MaterialPageRoute(
|
||||
|
@ -2,7 +2,35 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_pdfview/flutter_pdfview.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:openlib/state/state.dart'
|
||||
show filePathProvider, pdfCurrentPage, totalPdfPage;
|
||||
show
|
||||
filePathProvider,
|
||||
pdfCurrentPage,
|
||||
totalPdfPage,
|
||||
savePdfState,
|
||||
openPdfWithExternalAppProvider,
|
||||
getBookPosition;
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
import 'package:open_file/open_file.dart';
|
||||
import 'dart:io' show Platform;
|
||||
|
||||
import 'package:openlib/services/files.dart' show getFilePath;
|
||||
|
||||
Future<void> launchPdfViewer(
|
||||
{required String fileName,
|
||||
required BuildContext context,
|
||||
required WidgetRef ref}) async {
|
||||
bool openWithExternalApp = ref.watch(openPdfWithExternalAppProvider);
|
||||
if (openWithExternalApp) {
|
||||
String path = await getFilePath(fileName);
|
||||
await OpenFile.open(path);
|
||||
} else {
|
||||
Navigator.push(context, MaterialPageRoute(builder: (BuildContext context) {
|
||||
return PdfView(
|
||||
fileName: fileName,
|
||||
);
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
class PdfView extends ConsumerStatefulWidget {
|
||||
const PdfView({super.key, required this.fileName});
|
||||
@ -18,7 +46,7 @@ class _PdfViewState extends ConsumerState<PdfView> {
|
||||
Widget build(BuildContext context) {
|
||||
final filePath = ref.watch(filePathProvider(widget.fileName));
|
||||
return filePath.when(data: (data) {
|
||||
return PdfViewer(filePath: data);
|
||||
return PdfViewer(filePath: data, fileName: widget.fileName);
|
||||
}, error: (error, stack) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
@ -49,9 +77,11 @@ class _PdfViewState extends ConsumerState<PdfView> {
|
||||
}
|
||||
|
||||
class PdfViewer extends ConsumerStatefulWidget {
|
||||
const PdfViewer({Key? key, required this.filePath}) : super(key: key);
|
||||
const PdfViewer({Key? key, required this.filePath, required this.fileName})
|
||||
: super(key: key);
|
||||
|
||||
final String filePath;
|
||||
final String fileName;
|
||||
|
||||
@override
|
||||
ConsumerState<ConsumerStatefulWidget> createState() => _PdfViewerState();
|
||||
@ -60,8 +90,44 @@ class PdfViewer extends ConsumerStatefulWidget {
|
||||
class _PdfViewerState extends ConsumerState<PdfViewer> {
|
||||
late PDFViewController controller;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void deactivate() {
|
||||
if (Platform.isAndroid || Platform.isIOS) {
|
||||
savePdfState(widget.fileName, ref);
|
||||
}
|
||||
super.deactivate();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _openPdfWithDefaultViewer(String fileName) async {
|
||||
debugPrint("Opening : $fileName");
|
||||
final fileUrl = Uri.parse(fileName);
|
||||
if (await canLaunchUrl(fileUrl)) {
|
||||
await launchUrl(fileUrl);
|
||||
} else {
|
||||
// ignore: use_build_context_synchronously
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text(
|
||||
'Could not open the PDF',
|
||||
textAlign: TextAlign.center,
|
||||
)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
bool isMobile = Platform.isAndroid || Platform.isIOS;
|
||||
final currentPage = ref.watch(pdfCurrentPage);
|
||||
final totalPages = ref.watch(totalPdfPage);
|
||||
return Scaffold(
|
||||
@ -69,51 +135,109 @@ class _PdfViewerState extends ConsumerState<PdfViewer> {
|
||||
backgroundColor: Theme.of(context).colorScheme.primary,
|
||||
title: const Text("Openlib"),
|
||||
titleTextStyle: Theme.of(context).textTheme.displayLarge,
|
||||
actions: [
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
if (currentPage != 0) {
|
||||
ref.read(pdfCurrentPage.notifier).state = currentPage - 1;
|
||||
controller.setPage(currentPage - 1);
|
||||
} else {
|
||||
ref.read(pdfCurrentPage.notifier).state = totalPages;
|
||||
controller.setPage(totalPages - 1);
|
||||
}
|
||||
},
|
||||
icon: const Icon(
|
||||
Icons.arrow_left,
|
||||
size: 25,
|
||||
)),
|
||||
Text('${(currentPage + 1).toString()} / ${totalPages.toString()}'),
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
if (currentPage + 1 < totalPages) {
|
||||
ref.read(pdfCurrentPage.notifier).state = currentPage + 1;
|
||||
controller.setPage(currentPage + 1);
|
||||
} else {
|
||||
ref.read(pdfCurrentPage.notifier).state = 0;
|
||||
controller.setPage(0);
|
||||
}
|
||||
},
|
||||
icon: const Icon(
|
||||
Icons.arrow_right,
|
||||
size: 25,
|
||||
)),
|
||||
],
|
||||
),
|
||||
body: PDFView(
|
||||
swipeHorizontal: true,
|
||||
fitEachPage: true,
|
||||
fitPolicy: FitPolicy.BOTH,
|
||||
filePath: widget.filePath,
|
||||
onViewCreated: (controller) {
|
||||
this.controller = controller;
|
||||
},
|
||||
onPageChanged: (page, total) {
|
||||
ref.read(pdfCurrentPage.notifier).state = page ?? 0;
|
||||
ref.read(totalPdfPage.notifier).state = total ?? 0;
|
||||
},
|
||||
actions: isMobile
|
||||
? [
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
if (currentPage != 0) {
|
||||
ref.read(pdfCurrentPage.notifier).state =
|
||||
currentPage - 1;
|
||||
controller.setPage(currentPage - 1);
|
||||
} else {
|
||||
ref.read(pdfCurrentPage.notifier).state = totalPages;
|
||||
controller.setPage(totalPages - 1);
|
||||
}
|
||||
},
|
||||
icon: const Icon(
|
||||
Icons.arrow_left,
|
||||
size: 25,
|
||||
)),
|
||||
Text(
|
||||
'${(currentPage + 1).toString()} / ${totalPages.toString()}'),
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
if (currentPage + 1 < totalPages) {
|
||||
ref.read(pdfCurrentPage.notifier).state =
|
||||
currentPage + 1;
|
||||
controller.setPage(currentPage + 1);
|
||||
} else {
|
||||
ref.read(pdfCurrentPage.notifier).state = 0;
|
||||
controller.setPage(0);
|
||||
}
|
||||
},
|
||||
icon: const Icon(
|
||||
Icons.arrow_right,
|
||||
size: 25,
|
||||
)),
|
||||
]
|
||||
: [],
|
||||
),
|
||||
body: isMobile
|
||||
? ref.watch(getBookPosition(widget.fileName)).when(
|
||||
data: (data) {
|
||||
return PDFView(
|
||||
swipeHorizontal: true,
|
||||
fitEachPage: true,
|
||||
fitPolicy: FitPolicy.BOTH,
|
||||
filePath: widget.filePath,
|
||||
onViewCreated: (controller) {
|
||||
this.controller = controller;
|
||||
},
|
||||
defaultPage: int.parse(data ?? '0'),
|
||||
onPageChanged: (page, total) {
|
||||
ref.read(pdfCurrentPage.notifier).state = page ?? 0;
|
||||
ref.read(totalPdfPage.notifier).state = total ?? 0;
|
||||
},
|
||||
);
|
||||
},
|
||||
error: (error, stackTrace) {
|
||||
return PDFView(
|
||||
swipeHorizontal: true,
|
||||
fitEachPage: true,
|
||||
fitPolicy: FitPolicy.BOTH,
|
||||
filePath: widget.filePath,
|
||||
onViewCreated: (controller) {
|
||||
this.controller = controller;
|
||||
},
|
||||
onPageChanged: (page, total) {
|
||||
ref.read(pdfCurrentPage.notifier).state = page ?? 0;
|
||||
ref.read(totalPdfPage.notifier).state = total ?? 0;
|
||||
},
|
||||
);
|
||||
},
|
||||
loading: () {
|
||||
return Center(
|
||||
child: SizedBox(
|
||||
width: 25,
|
||||
height: 25,
|
||||
child: CircularProgressIndicator(
|
||||
color: Theme.of(context).colorScheme.secondary,
|
||||
),
|
||||
));
|
||||
},
|
||||
)
|
||||
: Center(
|
||||
child: TextButton(
|
||||
style: TextButton.styleFrom(
|
||||
backgroundColor: Theme.of(context).colorScheme.secondary,
|
||||
textStyle: const TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w900,
|
||||
color: Colors.white,
|
||||
)),
|
||||
onPressed: () async {
|
||||
await _openPdfWithDefaultViewer("file://${widget.filePath}");
|
||||
// ignore: use_build_context_synchronously
|
||||
Navigator.pop(context);
|
||||
},
|
||||
child: const Padding(
|
||||
padding: EdgeInsets.all(8.0),
|
||||
child: Text(
|
||||
"Open with System's PDF Viewer",
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -46,6 +46,7 @@ class ResultPage extends ConsumerWidget {
|
||||
author: i.author ?? "unknown",
|
||||
publisher: i.publisher ?? "unknown",
|
||||
thumbnail: i.thumbnail!,
|
||||
info: i.info,
|
||||
link: i.link,
|
||||
onClick: () {
|
||||
Navigator.push(context, MaterialPageRoute(
|
||||
|
@ -7,24 +7,33 @@ import 'package:openlib/state/state.dart'
|
||||
searchQueryProvider,
|
||||
selectedTypeState,
|
||||
selectedSortState,
|
||||
selectedFileTypeState,
|
||||
typeValues,
|
||||
fileType,
|
||||
sortValues;
|
||||
import 'components/snack_bar_widget.dart';
|
||||
|
||||
class SearchPage extends ConsumerWidget {
|
||||
const SearchPage({Key? key}) : super(key: key);
|
||||
|
||||
void onSubmit(BuildContext context, WidgetRef ref) {
|
||||
Navigator.push(context, MaterialPageRoute(builder: (BuildContext context) {
|
||||
return ResultPage(
|
||||
searchQuery: ref.read(searchQueryProvider),
|
||||
);
|
||||
}));
|
||||
if (ref.read(searchQueryProvider).isNotEmpty) {
|
||||
Navigator.push(context,
|
||||
MaterialPageRoute(builder: (BuildContext context) {
|
||||
return ResultPage(
|
||||
searchQuery: ref.read(searchQueryProvider),
|
||||
);
|
||||
}));
|
||||
} else {
|
||||
showSnackBar(context: context, message: 'Search field is empty');
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final dropdownTypeValue = ref.watch(selectedTypeState);
|
||||
final dropdownSortValue = ref.watch(selectedSortState);
|
||||
final dropDownFileTypeValue = ref.watch(selectedFileTypeState);
|
||||
|
||||
return SingleChildScrollView(
|
||||
scrollDirection: Axis.vertical,
|
||||
@ -42,12 +51,14 @@ class SearchPage extends ConsumerWidget {
|
||||
cursorColor: Theme.of(context).colorScheme.secondary,
|
||||
decoration: InputDecoration(
|
||||
enabledBorder: const OutlineInputBorder(
|
||||
borderSide: BorderSide(color: Colors.black45, width: 2),
|
||||
borderSide: BorderSide(color: Colors.grey, width: 2),
|
||||
borderRadius: BorderRadius.all(Radius.circular(50)),
|
||||
),
|
||||
focusedBorder: const OutlineInputBorder(
|
||||
borderSide: BorderSide(color: Colors.black54, width: 2),
|
||||
borderRadius: BorderRadius.all(Radius.circular(50)),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderSide: BorderSide(
|
||||
color: Theme.of(context).colorScheme.tertiary,
|
||||
width: 2),
|
||||
borderRadius: const BorderRadius.all(Radius.circular(50)),
|
||||
),
|
||||
suffixIcon: IconButton(
|
||||
padding: const EdgeInsets.only(right: 5),
|
||||
@ -88,12 +99,14 @@ class SearchPage extends ConsumerWidget {
|
||||
color: Theme.of(context).colorScheme.secondary,
|
||||
),
|
||||
enabledBorder: const OutlineInputBorder(
|
||||
borderSide: BorderSide(color: Colors.black54, width: 2),
|
||||
borderSide: BorderSide(color: Colors.grey, width: 2),
|
||||
borderRadius: BorderRadius.all(Radius.circular(50)),
|
||||
),
|
||||
focusedBorder: const OutlineInputBorder(
|
||||
borderSide: BorderSide(color: Colors.black54, width: 2),
|
||||
borderRadius: BorderRadius.all(Radius.circular(50)),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderSide: BorderSide(
|
||||
color: Theme.of(context).colorScheme.tertiary,
|
||||
width: 2),
|
||||
borderRadius: const BorderRadius.all(Radius.circular(50)),
|
||||
),
|
||||
),
|
||||
icon: const Icon(Icons.arrow_drop_down),
|
||||
@ -105,7 +118,8 @@ class SearchPage extends ConsumerWidget {
|
||||
value: value,
|
||||
child: Text(
|
||||
value,
|
||||
style: const TextStyle(fontSize: 12),
|
||||
style: const TextStyle(
|
||||
fontSize: 12, fontWeight: FontWeight.bold),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
@ -128,12 +142,14 @@ class SearchPage extends ConsumerWidget {
|
||||
color: Theme.of(context).colorScheme.secondary,
|
||||
),
|
||||
enabledBorder: const OutlineInputBorder(
|
||||
borderSide: BorderSide(color: Colors.black45, width: 2),
|
||||
borderSide: BorderSide(color: Colors.grey, width: 2),
|
||||
borderRadius: BorderRadius.all(Radius.circular(50)),
|
||||
),
|
||||
focusedBorder: const OutlineInputBorder(
|
||||
borderSide: BorderSide(color: Colors.black54, width: 2),
|
||||
borderRadius: BorderRadius.all(Radius.circular(50)),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderSide: BorderSide(
|
||||
color: Theme.of(context).colorScheme.tertiary,
|
||||
width: 2),
|
||||
borderRadius: const BorderRadius.all(Radius.circular(50)),
|
||||
),
|
||||
),
|
||||
value: dropdownSortValue,
|
||||
@ -144,7 +160,8 @@ class SearchPage extends ConsumerWidget {
|
||||
value: value,
|
||||
child: Text(
|
||||
value,
|
||||
style: const TextStyle(fontSize: 12),
|
||||
style: const TextStyle(
|
||||
fontSize: 12, fontWeight: FontWeight.bold),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
@ -153,7 +170,48 @@ class SearchPage extends ConsumerWidget {
|
||||
},
|
||||
),
|
||||
),
|
||||
)
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 7, right: 7, top: 19),
|
||||
child: SizedBox(
|
||||
width: 165,
|
||||
child: DropdownButtonFormField(
|
||||
decoration: InputDecoration(
|
||||
labelText: 'File type',
|
||||
labelStyle: TextStyle(
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Theme.of(context).colorScheme.secondary,
|
||||
),
|
||||
enabledBorder: const OutlineInputBorder(
|
||||
borderSide: BorderSide(color: Colors.grey, width: 2),
|
||||
borderRadius: BorderRadius.all(Radius.circular(50)),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderSide: BorderSide(
|
||||
color: Theme.of(context).colorScheme.tertiary,
|
||||
width: 2),
|
||||
borderRadius: const BorderRadius.all(Radius.circular(50)),
|
||||
),
|
||||
),
|
||||
value: dropDownFileTypeValue,
|
||||
items: fileType.map<DropdownMenuItem<String>>((String value) {
|
||||
return DropdownMenuItem<String>(
|
||||
value: value,
|
||||
child: Text(
|
||||
value,
|
||||
style: const TextStyle(
|
||||
fontSize: 12, fontWeight: FontWeight.bold),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
onChanged: (String? val) {
|
||||
ref.read(selectedFileTypeState.notifier).state =
|
||||
val ?? 'All';
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
163
lib/ui/settings_page.dart
Normal file
@ -0,0 +1,163 @@
|
||||
import 'dart:io';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import 'package:openlib/ui/components/page_title_widget.dart';
|
||||
import 'package:openlib/ui/about_page.dart';
|
||||
import 'package:openlib/state/state.dart'
|
||||
show
|
||||
themeModeProvider,
|
||||
openPdfWithExternalAppProvider,
|
||||
openEpubWithExternalAppProvider,
|
||||
dbProvider;
|
||||
|
||||
class SettingsPage extends ConsumerWidget {
|
||||
const SettingsPage({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(left: 5, right: 5, top: 10),
|
||||
child: SingleChildScrollView(
|
||||
scrollDirection: Axis.vertical,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const TitleText("Settings"),
|
||||
_PaddedContainer(
|
||||
children: [
|
||||
Text(
|
||||
"Dark Mode",
|
||||
style: TextStyle(
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Theme.of(context).colorScheme.tertiary,
|
||||
),
|
||||
),
|
||||
Switch(
|
||||
// This bool value toggles the switch.
|
||||
value: ref.watch(themeModeProvider) == ThemeMode.dark,
|
||||
activeColor: Colors.red,
|
||||
onChanged: (bool value) {
|
||||
ref.read(themeModeProvider.notifier).state =
|
||||
value == true ? ThemeMode.dark : ThemeMode.light;
|
||||
ref.read(dbProvider).savePreference('darkMode', value);
|
||||
|
||||
if (Platform.isAndroid) {
|
||||
SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle(
|
||||
systemNavigationBarColor:
|
||||
value ? Colors.black : Colors.grey.shade200));
|
||||
}
|
||||
},
|
||||
)
|
||||
],
|
||||
),
|
||||
_PaddedContainer(
|
||||
children: [
|
||||
Text(
|
||||
"Open PDF with External Reader",
|
||||
style: TextStyle(
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Theme.of(context).colorScheme.tertiary,
|
||||
),
|
||||
),
|
||||
Switch(
|
||||
// This bool value toggles the switch.
|
||||
value: ref.watch(openPdfWithExternalAppProvider),
|
||||
activeColor: Colors.red,
|
||||
onChanged: (bool value) {
|
||||
ref.read(openPdfWithExternalAppProvider.notifier).state =
|
||||
value;
|
||||
ref
|
||||
.read(dbProvider)
|
||||
.savePreference('openPdfwithExternalApp', value);
|
||||
},
|
||||
)
|
||||
],
|
||||
),
|
||||
_PaddedContainer(
|
||||
children: [
|
||||
Text(
|
||||
"Open Epub with External Reader",
|
||||
style: TextStyle(
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Theme.of(context).colorScheme.tertiary,
|
||||
),
|
||||
),
|
||||
Switch(
|
||||
// This bool value toggles the switch.
|
||||
value: ref.watch(
|
||||
openEpubWithExternalAppProvider,
|
||||
),
|
||||
activeColor: Colors.red,
|
||||
onChanged: (bool value) {
|
||||
ref.read(openEpubWithExternalAppProvider.notifier).state =
|
||||
value;
|
||||
ref
|
||||
.read(dbProvider)
|
||||
.savePreference('openEpubwithExternalApp', value);
|
||||
},
|
||||
)
|
||||
],
|
||||
),
|
||||
_PaddedContainer(
|
||||
onClick: () {
|
||||
Navigator.push(context,
|
||||
MaterialPageRoute(builder: (BuildContext context) {
|
||||
return const AboutPage();
|
||||
}));
|
||||
},
|
||||
children: [
|
||||
Text(
|
||||
"About",
|
||||
style: TextStyle(
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Theme.of(context).colorScheme.tertiary,
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _PaddedContainer extends StatelessWidget {
|
||||
const _PaddedContainer({Key? key, this.onClick, required this.children})
|
||||
: super(key: key);
|
||||
|
||||
final VoidCallback? onClick;
|
||||
final List<Widget> children;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(left: 5, right: 5, top: 10),
|
||||
child: InkWell(
|
||||
onTap: onClick,
|
||||
child: Container(
|
||||
height: 61,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(5),
|
||||
color: Theme.of(context).colorScheme.tertiaryContainer,
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: children,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
72
lib/ui/themes.dart
Normal file
@ -0,0 +1,72 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:openlib/ui/extensions.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
|
||||
ThemeData lightTheme = ThemeData(
|
||||
primaryColor: Colors.white,
|
||||
colorScheme: ColorScheme.light(
|
||||
primary: Colors.white,
|
||||
secondary: '#FB0101'.toColor(),
|
||||
tertiary: Colors.black,
|
||||
tertiaryContainer: '#F2F2F2'.toColor(),
|
||||
),
|
||||
textTheme: TextTheme(
|
||||
displayLarge: const TextStyle(
|
||||
color: Colors.black,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 21,
|
||||
),
|
||||
displayMedium: const TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.black,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
headlineMedium: TextStyle(
|
||||
color: "#595E60".toColor(),
|
||||
),
|
||||
headlineSmall: TextStyle(
|
||||
color: "#7F7F7F".toColor(),
|
||||
)),
|
||||
fontFamily: GoogleFonts.nunito().fontFamily,
|
||||
useMaterial3: true,
|
||||
textSelectionTheme: TextSelectionThemeData(
|
||||
selectionColor: '#FB0101'.toColor(),
|
||||
selectionHandleColor: '#FB0101'.toColor(),
|
||||
),
|
||||
);
|
||||
|
||||
ThemeData darkTheme = ThemeData(
|
||||
primaryColor: Colors.black,
|
||||
colorScheme: ColorScheme.dark(
|
||||
primary: Colors.black,
|
||||
secondary: '#FB0101'.toColor(),
|
||||
tertiary: Colors.white,
|
||||
tertiaryContainer: '#2B2B2B'.toColor(),
|
||||
),
|
||||
textTheme: TextTheme(
|
||||
displayLarge: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 21,
|
||||
),
|
||||
displayMedium: const TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
headlineMedium: TextStyle(
|
||||
color: "#F5F5F5".toColor(),
|
||||
),
|
||||
headlineSmall: TextStyle(
|
||||
color: "#E8E2E2".toColor(),
|
||||
),
|
||||
),
|
||||
fontFamily: GoogleFonts.nunito().fontFamily,
|
||||
useMaterial3: true,
|
||||
textSelectionTheme: TextSelectionThemeData(
|
||||
selectionColor: '#FB0101'.toColor(),
|
||||
selectionHandleColor: '#FB0101'.toColor(),
|
||||
),
|
||||
);
|
@ -17,127 +17,133 @@ class TrendingPage extends ConsumerWidget {
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final trendingBooks = ref.watch(getTrendingBooks);
|
||||
return trendingBooks.when(data: (data) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(left: 5, right: 5, top: 10),
|
||||
child: CustomScrollView(
|
||||
slivers: [
|
||||
const SliverToBoxAdapter(
|
||||
child: TitleText("Trending"),
|
||||
),
|
||||
SliverPadding(
|
||||
padding: const EdgeInsets.all(5),
|
||||
sliver: SliverGrid(
|
||||
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 3,
|
||||
mainAxisSpacing: 10.0,
|
||||
crossAxisSpacing: 13.0,
|
||||
mainAxisExtent: 205,
|
||||
return trendingBooks.when(
|
||||
skipLoadingOnRefresh: false,
|
||||
data: (data) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(left: 5, right: 5, top: 10),
|
||||
child: CustomScrollView(
|
||||
physics: const BouncingScrollPhysics(),
|
||||
slivers: [
|
||||
const SliverToBoxAdapter(
|
||||
child: TitleText("Trending"),
|
||||
),
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
(BuildContext context, int index) {
|
||||
return InkWell(
|
||||
onTap: () {
|
||||
Navigator.push(context,
|
||||
MaterialPageRoute(builder: (BuildContext context) {
|
||||
return ResultPage(
|
||||
searchQuery: data[index].title!,
|
||||
);
|
||||
}));
|
||||
},
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
height: double.infinity,
|
||||
color: const Color.fromARGB(255, 255, 255, 255),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
CachedNetworkImage(
|
||||
height: imageHeight,
|
||||
width: imageWidth,
|
||||
imageUrl: data[index].thumbnail!,
|
||||
imageBuilder: (context, imageProvider) =>
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
boxShadow: const [
|
||||
BoxShadow(
|
||||
color: Colors.grey,
|
||||
spreadRadius: 0.1,
|
||||
blurRadius: 1)
|
||||
],
|
||||
borderRadius: const BorderRadius.all(
|
||||
Radius.circular(5)),
|
||||
image: DecorationImage(
|
||||
image: imageProvider,
|
||||
fit: BoxFit.fill,
|
||||
),
|
||||
),
|
||||
),
|
||||
placeholder: (context, url) => Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(5),
|
||||
color: "#E3E8E9".toColor(),
|
||||
),
|
||||
height: imageHeight,
|
||||
width: imageWidth,
|
||||
),
|
||||
errorWidget: (context, url, error) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(5),
|
||||
color: Colors.grey,
|
||||
),
|
||||
SliverPadding(
|
||||
padding: const EdgeInsets.all(5),
|
||||
sliver: SliverGrid(
|
||||
gridDelegate:
|
||||
const SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 3,
|
||||
mainAxisSpacing: 10.0,
|
||||
crossAxisSpacing: 13.0,
|
||||
mainAxisExtent: 205,
|
||||
),
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
(BuildContext context, int index) {
|
||||
return InkWell(
|
||||
onTap: () {
|
||||
Navigator.push(context, MaterialPageRoute(
|
||||
builder: (BuildContext context) {
|
||||
return ResultPage(
|
||||
searchQuery: data[index].title!,
|
||||
);
|
||||
}));
|
||||
},
|
||||
child: SizedBox(
|
||||
width: double.infinity,
|
||||
height: double.infinity,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
CachedNetworkImage(
|
||||
height: imageHeight,
|
||||
width: imageWidth,
|
||||
child: const Center(
|
||||
child: Icon(Icons.image_rounded),
|
||||
imageUrl: data[index].thumbnail!,
|
||||
imageBuilder: (context, imageProvider) =>
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
boxShadow: const [
|
||||
BoxShadow(
|
||||
color: Colors.grey,
|
||||
spreadRadius: 0.1,
|
||||
blurRadius: 1)
|
||||
],
|
||||
borderRadius: const BorderRadius.all(
|
||||
Radius.circular(5)),
|
||||
image: DecorationImage(
|
||||
image: imageProvider,
|
||||
fit: BoxFit.fill,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 4),
|
||||
child: SizedBox(
|
||||
width: imageWidth,
|
||||
child: Text(
|
||||
data[index].title!,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.displayMedium,
|
||||
maxLines: 2,
|
||||
placeholder: (context, url) => Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(5),
|
||||
color: "#E3E8E9".toColor(),
|
||||
),
|
||||
height: imageHeight,
|
||||
width: imageWidth,
|
||||
),
|
||||
errorWidget: (context, url, error) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius:
|
||||
BorderRadius.circular(5),
|
||||
color: Colors.grey,
|
||||
),
|
||||
height: imageHeight,
|
||||
width: imageWidth,
|
||||
child: const Center(
|
||||
child: Icon(Icons.image_rounded),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
]),
|
||||
),
|
||||
);
|
||||
},
|
||||
childCount: data.length,
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 4),
|
||||
child: SizedBox(
|
||||
width: imageWidth,
|
||||
child: Text(
|
||||
data[index].title!,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.displayMedium,
|
||||
maxLines: 2,
|
||||
),
|
||||
),
|
||||
),
|
||||
]),
|
||||
),
|
||||
);
|
||||
},
|
||||
childCount: data.length,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}, error: (error, _) {
|
||||
return CustomErrorWidget(
|
||||
error: error,
|
||||
stackTrace: _,
|
||||
onRefresh: () {
|
||||
// ignore: unused_result
|
||||
ref.refresh(getTrendingBooks);
|
||||
);
|
||||
},
|
||||
);
|
||||
}, loading: () {
|
||||
return Center(
|
||||
child: SizedBox(
|
||||
width: 25,
|
||||
height: 25,
|
||||
child: CircularProgressIndicator(
|
||||
color: Theme.of(context).colorScheme.secondary,
|
||||
),
|
||||
));
|
||||
});
|
||||
error: (error, _) {
|
||||
return CustomErrorWidget(
|
||||
error: error,
|
||||
stackTrace: _,
|
||||
onRefresh: () {
|
||||
// ignore: unused_result
|
||||
ref.refresh(getTrendingBooks);
|
||||
},
|
||||
);
|
||||
},
|
||||
loading: () {
|
||||
return Center(
|
||||
child: SizedBox(
|
||||
width: 25,
|
||||
height: 25,
|
||||
child: CircularProgressIndicator(
|
||||
color: Theme.of(context).colorScheme.secondary,
|
||||
),
|
||||
));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
75
lib/ui/webview_page.dart
Normal file
@ -0,0 +1,75 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:webview_flutter/webview_flutter.dart';
|
||||
import 'package:webview_cookie_manager/webview_cookie_manager.dart'
|
||||
as cookiejar;
|
||||
|
||||
import 'package:openlib/state/state.dart'
|
||||
show cookieProvider, userAgentProvider, dbProvider, bookInfoProvider;
|
||||
|
||||
class Webview extends ConsumerStatefulWidget {
|
||||
const Webview({Key? key, required this.url}) : super(key: key);
|
||||
final String url;
|
||||
@override
|
||||
// ignore: library_private_types_in_public_api
|
||||
_WebviewState createState() => _WebviewState();
|
||||
}
|
||||
|
||||
class _WebviewState extends ConsumerState<Webview> {
|
||||
WebViewController controller = WebViewController();
|
||||
|
||||
final cookieManager = cookiejar.WebviewCookieManager();
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
automaticallyImplyLeading: false,
|
||||
title: const Text("Solve Captcha"),
|
||||
),
|
||||
body: SafeArea(
|
||||
child: WebViewWidget(
|
||||
controller: controller
|
||||
..setJavaScriptMode(JavaScriptMode.unrestricted)
|
||||
..setBackgroundColor(const Color(0x00000000))
|
||||
..loadRequest(Uri.parse(widget.url))
|
||||
..getUserAgent().then((value) {
|
||||
ref.read(userAgentProvider.notifier).state = value!;
|
||||
ref.read(dbProvider).setBrowserOptions('userAgent', value);
|
||||
})
|
||||
..setNavigationDelegate(NavigationDelegate(
|
||||
onPageStarted: (url) async {
|
||||
var urlStatusCode = await controller.runJavaScriptReturningResult(
|
||||
"var xhr = new XMLHttpRequest();xhr.open('GET', window.location.href, false);xhr.send(null);xhr.status;");
|
||||
|
||||
if (urlStatusCode.toString().contains('200')) {
|
||||
final cookies = await cookieManager
|
||||
.getCookies("https://annas-archive.org");
|
||||
|
||||
List<String> cookie = [];
|
||||
for (var element in cookies) {
|
||||
if (element.name == 'cf_clearance' ||
|
||||
element.name == 'cf_chl_2') {
|
||||
cookie.add(element.toString().split(';')[0]);
|
||||
}
|
||||
}
|
||||
|
||||
String cfClearance = cookie.join('; ');
|
||||
|
||||
ref.read(cookieProvider.notifier).state = cfClearance;
|
||||
|
||||
await ref
|
||||
.read(dbProvider)
|
||||
.setBrowserOptions('cookie', cfClearance);
|
||||
|
||||
ref.invalidate(bookInfoProvider);
|
||||
|
||||
// ignore: use_build_context_synchronously
|
||||
Navigator.pop(context);
|
||||
}
|
||||
},
|
||||
)),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -6,6 +6,10 @@
|
||||
|
||||
#include "generated_plugin_registrant.h"
|
||||
|
||||
#include <url_launcher_linux/url_launcher_plugin.h>
|
||||
|
||||
void fl_register_plugins(FlPluginRegistry* registry) {
|
||||
g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar =
|
||||
fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin");
|
||||
url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar);
|
||||
}
|
||||
|
@ -3,6 +3,7 @@
|
||||
#
|
||||
|
||||
list(APPEND FLUTTER_PLUGIN_LIST
|
||||
url_launcher_linux
|
||||
)
|
||||
|
||||
list(APPEND FLUTTER_FFI_PLUGIN_LIST
|
||||
|
@ -7,8 +7,10 @@ import Foundation
|
||||
|
||||
import path_provider_foundation
|
||||
import sqflite
|
||||
import url_launcher_macos
|
||||
|
||||
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
|
||||
SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin"))
|
||||
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
|
||||
}
|
||||
|
297
pubspec.lock
@ -5,10 +5,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: archive
|
||||
sha256: "0c8368c9b3f0abbc193b9d6133649a614204b528982bebc7026372d61677ce3a"
|
||||
sha256: "49b1fad315e57ab0bbc15bcbb874e83116a1d78f77ebd500a4af6c9407d6b28e"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.3.7"
|
||||
version: "3.3.8"
|
||||
args:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -93,10 +93,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: collection
|
||||
sha256: "4a07be6cb69c84d677a6c3096fcf960cc3285a8330b4603e0d463d15d9bd934c"
|
||||
sha256: f092b211a4319e98e5ff58223576de6c2803db36221657b46c82574721240687
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.17.1"
|
||||
version: "1.17.2"
|
||||
convert:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -125,10 +125,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: cupertino_icons
|
||||
sha256: e35129dc44c9118cee2a5603506d823bab99c68393879edb440e0090d07586be
|
||||
sha256: d57953e10f9f8327ce64a508a355f0b1ec902193f66288e8cb5070e7c47eeb2d
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.5"
|
||||
version: "1.0.6"
|
||||
dev:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -141,10 +141,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: dio
|
||||
sha256: "3866d67f93523161b643187af65f5ac08bc991a5bcdaf41a2d587fe4ccb49993"
|
||||
sha256: ce75a1b40947fea0a0e16ce73337122a86762e38b982e1ccb909daa3b9bc4197
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.3.0"
|
||||
version: "5.3.2"
|
||||
epub_view:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -181,10 +181,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: ffi
|
||||
sha256: ed5337a5660c506388a9f012be0288fb38b49020ce2b45fe1f8b8323fe429f99
|
||||
sha256: "7bf0adc28a23d395f19f3f1eb21dd7cfd1dd9f8e1c50051c069122e6853bc878"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.2"
|
||||
version: "2.1.0"
|
||||
file:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -234,10 +234,10 @@ packages:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: flutter_lints
|
||||
sha256: "2118df84ef0c3ca93f96123a616ae8540879991b8b57af2f81b76a7ada49b2a4"
|
||||
sha256: a25a15ebbdfc33ab1cd26c63a6ee519df92338a9c10f122adda92938253bef04
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.2"
|
||||
version: "2.0.3"
|
||||
flutter_pdfview:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -250,10 +250,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_riverpod
|
||||
sha256: b83ac5827baadefd331ea1d85110f34645827ea234ccabf53a655f41901a9bf4
|
||||
sha256: b04d4e9435a563673746ccb328d22018c6c9496bb547e11dd56c1b0cc9829fe5
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.6"
|
||||
version: "2.3.10"
|
||||
flutter_svg:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -267,6 +267,11 @@ packages:
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
flutter_web_plugins:
|
||||
dependency: transitive
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
gestures:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -359,18 +364,18 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: matcher
|
||||
sha256: "6501fbd55da300384b768785b83e5ce66991266cec21af89ab9ae7f5ce1c4cbb"
|
||||
sha256: "1803e76e6653768d64ed8ff2e1e67bea3ad4b923eb5c56a295c3e634bad5960e"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.12.15"
|
||||
version: "0.12.16"
|
||||
material_color_utilities:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: material_color_utilities
|
||||
sha256: d92141dc6fe1dad30722f9aa826c7fbc896d021d792f80678280601aff8cf724
|
||||
sha256: "9528f2f296073ff54cb9fee677df673ace1218163c3bc7628093e7eed5203d41"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.2.0"
|
||||
version: "0.5.0"
|
||||
meta:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -387,6 +392,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.2"
|
||||
open_file:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: open_file
|
||||
sha256: a5a32d44acb7c899987d0999e1e3cbb0a0f1adebbf41ac813ec6d2d8faa0af20
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.3.2"
|
||||
path:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -407,50 +420,90 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: path_provider
|
||||
sha256: "3087813781ab814e4157b172f1a11c46be20179fcc9bea043e0fba36bc0acaa2"
|
||||
sha256: a1aa8aaa2542a6bc57e381f132af822420216c80d4781f7aa085ca3229208aaa
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.15"
|
||||
version: "2.1.1"
|
||||
path_provider_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_android
|
||||
sha256: "2cec049d282c7f13c594b4a73976b0b4f2d7a1838a6dd5aaf7bd9719196bee86"
|
||||
sha256: "6b8b19bd80da4f11ce91b2d1fb931f3006911477cec227cce23d3253d80df3f1"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.27"
|
||||
version: "2.2.0"
|
||||
path_provider_foundation:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_foundation
|
||||
sha256: "916731ccbdce44d545414dd9961f26ba5fbaa74bcbb55237d8e65a623a8c7297"
|
||||
sha256: "19314d595120f82aca0ba62787d58dde2cc6b5df7d2f0daf72489e38d1b57f2d"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.4"
|
||||
version: "2.3.1"
|
||||
path_provider_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_linux
|
||||
sha256: ffbb8cc9ed2c9ec0e4b7a541e56fd79b138e8f47d2fb86815f15358a349b3b57
|
||||
sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.11"
|
||||
version: "2.2.1"
|
||||
path_provider_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_platform_interface
|
||||
sha256: "57585299a729335f1298b43245842678cb9f43a6310351b18fb577d6e33165ec"
|
||||
sha256: "94b1e0dd80970c1ce43d5d4e050a9918fce4f4a775e6142424c30a29a363265c"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.6"
|
||||
version: "2.1.1"
|
||||
path_provider_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_windows
|
||||
sha256: "1cb68ba4cd3a795033de62ba1b7b4564dace301f952de6bfb3cd91b202b6ee96"
|
||||
sha256: "8bc9f22eee8690981c22aa7fc602f5c85b497a6fb2ceb35ee5a5e5ed85ad8170"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.7"
|
||||
version: "2.2.1"
|
||||
permission_handler:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: permission_handler
|
||||
sha256: "63e5216aae014a72fe9579ccd027323395ce7a98271d9defa9d57320d001af81"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "10.4.3"
|
||||
permission_handler_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: permission_handler_android
|
||||
sha256: d74e77a5ecd38649905db0a7d05ef16bed42ff263b9efb73ed794317c5764ec3
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "10.3.4"
|
||||
permission_handler_apple:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: permission_handler_apple
|
||||
sha256: "99e220bce3f8877c78e4ace901082fb29fa1b4ebde529ad0932d8d664b34f3f5"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "9.1.4"
|
||||
permission_handler_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: permission_handler_platform_interface
|
||||
sha256: "7c6b1500385dd1d2ca61bb89e2488ca178e274a69144d26bbd65e33eae7c02a9"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.11.3"
|
||||
permission_handler_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: permission_handler_windows
|
||||
sha256: cc074aace208760f1eee6aa4fae766b45d947df85bc831cde77009cdb4720098
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.1.3"
|
||||
petitparser:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -463,18 +516,18 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: platform
|
||||
sha256: "4a451831508d7d6ca779f7ac6e212b4023dd5a7d08a27a63da33756410e32b76"
|
||||
sha256: ae68c7bfcd7383af3629daafb32fb4e8681c7154428da4febcff06200585f102
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.0"
|
||||
version: "3.1.2"
|
||||
plugin_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: plugin_platform_interface
|
||||
sha256: "6a2128648c854906c53fa8e33986fc0247a1116122f9534dd20e3ab9e16a32bc"
|
||||
sha256: da3fdfeccc4d4ff2da8f8c556704c08f912542c5fb3cf2233ed75372384a034d
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.4"
|
||||
version: "2.1.6"
|
||||
pointycastle:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -495,10 +548,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: riverpod
|
||||
sha256: "80e48bebc83010d5e67a11c9514af6b44bbac1ec77b4333c8ea65dbc79e2d8ef"
|
||||
sha256: "6c0a2c30c04206ac05494bcccd8148b76866e1a9248a5a8c84ca7b16fbcb3f6a"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.6"
|
||||
version: "2.3.10"
|
||||
rxdart:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -524,10 +577,18 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: source_span
|
||||
sha256: dd904f795d4b4f3b870833847c461801f6750a9fa8e61ea5ac53f9422b31f250
|
||||
sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.9.1"
|
||||
version: "1.10.0"
|
||||
sprintf:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: sprintf
|
||||
sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "7.0.0"
|
||||
sqflite:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -544,6 +605,22 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.5.0"
|
||||
sqflite_common_ffi:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: sqflite_common_ffi
|
||||
sha256: "0d5cc1be2eb18400ac6701c31211d44164393aa75886093002ecdd947be04f93"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.0+2"
|
||||
sqlite3:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: sqlite3
|
||||
sha256: db65233e6b99e99b2548932f55a987961bc06d82a31a0665451fa0b4fff4c3fb
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.0"
|
||||
stack_trace:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -556,10 +633,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: state_notifier
|
||||
sha256: "8fe42610f179b843b12371e40db58c9444f8757f8b69d181c97e50787caed289"
|
||||
sha256: b8677376aa54f2d7c58280d5a007f9e8774f1968d1fb1c096adcb4792fba29bb
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.7.2+1"
|
||||
version: "1.0.0"
|
||||
stream_channel:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -596,10 +673,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: test_api
|
||||
sha256: eb6ac1540b26de412b3403a163d919ba86f6a973fe6cc50ae3541b80092fdcfb
|
||||
sha256: "75760ffd7786fffdfb9597c35c5b27eaeec82be8edfb6d71d32651128ed7aab8"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.5.1"
|
||||
version: "0.6.0"
|
||||
typed_data:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -616,14 +693,78 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.0"
|
||||
url_launcher:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: url_launcher
|
||||
sha256: "47e208a6711459d813ba18af120d9663c20bdf6985d6ad39fe165d2538378d27"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.1.14"
|
||||
url_launcher_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_android
|
||||
sha256: b04af59516ab45762b2ca6da40fa830d72d0f6045cd97744450b73493fa76330
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.1.0"
|
||||
url_launcher_ios:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_ios
|
||||
sha256: "7c65021d5dee51813d652357bc65b8dd4a6177082a9966bc8ba6ee477baa795f"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.1.5"
|
||||
url_launcher_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_linux
|
||||
sha256: b651aad005e0cb06a01dbd84b428a301916dc75f0e7ea6165f80057fee2d8e8e
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.6"
|
||||
url_launcher_macos:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_macos
|
||||
sha256: b55486791f666e62e0e8ff825e58a023fd6b1f71c49926483f1128d3bbd8fe88
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.7"
|
||||
url_launcher_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_platform_interface
|
||||
sha256: "95465b39f83bfe95fcb9d174829d6476216f2d548b79c38ab2506e0458787618"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.5"
|
||||
url_launcher_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_web
|
||||
sha256: "2942294a500b4fa0b918685aff406773ba0a4cd34b7f42198742a94083020ce5"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.20"
|
||||
url_launcher_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_windows
|
||||
sha256: "95fef3129dc7cfaba2bc3d5ba2e16063bb561fc6d78e63eee16162bc70029069"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.8"
|
||||
uuid:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: uuid
|
||||
sha256: "648e103079f7c64a36dc7d39369cabb358d377078a051d6ae2ad3aa539519313"
|
||||
sha256: e03928880bdbcbf496fb415573f5ab7b1ea99b9b04f669c01104d085893c3134
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.7"
|
||||
version: "4.0.0"
|
||||
vector_graphics:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -656,22 +797,78 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.4"
|
||||
vocsy_epub_viewer:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: vocsy_epub_viewer
|
||||
sha256: "30aab4a5be22e8c98f22fed3345f6989caaca861178c38cab2a8b8cd66ad53de"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.0"
|
||||
web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: web
|
||||
sha256: dc8ccd225a2005c1be616fe02951e2e342092edf968cf0844220383757ef8f10
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.1.4-beta"
|
||||
webview_cookie_manager:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: webview_cookie_manager
|
||||
sha256: "425a9feac5cd2cb62a71da3dda5ac2eaf9ece5481ee8d79f3868dc5ba8223ad3"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.6"
|
||||
webview_flutter:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: webview_flutter
|
||||
sha256: c1ab9b81090705c6069197d9fdc1625e587b52b8d70cdde2339d177ad0dbb98e
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.4.1"
|
||||
webview_flutter_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: webview_flutter_android
|
||||
sha256: b0cd33dd7d3dd8e5f664e11a19e17ba12c352647269921a3b568406b001f1dff
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.12.0"
|
||||
webview_flutter_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: webview_flutter_platform_interface
|
||||
sha256: "6d9213c65f1060116757a7c473247c60f3f7f332cac33dc417c9e362a9a13e4f"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.6.0"
|
||||
webview_flutter_wkwebview:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: webview_flutter_wkwebview
|
||||
sha256: "30b9af6bdd457b44c08748b9190d23208b5165357cc2eb57914fee1366c42974"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.9.1"
|
||||
win32:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: win32
|
||||
sha256: dfdf0136e0aa7a1b474ea133e67cb0154a0acd2599c4f3ada3b49d38d38793ee
|
||||
sha256: "9e82a402b7f3d518fb9c02d0e9ae45952df31b9bf34d77baf19da2de03fc2aaa"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.0.5"
|
||||
version: "5.0.7"
|
||||
xdg_directories:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: xdg_directories
|
||||
sha256: e0b1147eec179d3911f1f19b59206448f78195ca1d20514134e10641b7d7fbff
|
||||
sha256: "589ada45ba9e39405c198fe34eb0f607cddb2108527e658136120892beac46d2"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.1"
|
||||
version: "1.0.3"
|
||||
xml:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -689,5 +886,5 @@ packages:
|
||||
source: hosted
|
||||
version: "3.1.2"
|
||||
sdks:
|
||||
dart: ">=3.0.5 <4.0.0"
|
||||
flutter: ">=3.7.0-0"
|
||||
dart: ">=3.1.0 <4.0.0"
|
||||
flutter: ">=3.13.0"
|
||||
|
12
pubspec.yaml
@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
|
||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||
# In Windows, build-name is used as the major, minor, and patch parts
|
||||
# of the product and file versions while build-number is used as the build suffix.
|
||||
version: 1.0.0+1
|
||||
version: 1.0.3+5
|
||||
|
||||
environment:
|
||||
sdk: '>=3.0.5 <4.0.0'
|
||||
@ -36,6 +36,7 @@ dependencies:
|
||||
|
||||
epub_view: ^3.2.0
|
||||
flutter_pdfview: ^1.2.7
|
||||
vocsy_epub_viewer: ^2.0.0
|
||||
# syncfusion_flutter_pdfviewer: ^22.2.5
|
||||
# pdfx: ^2.4.0
|
||||
|
||||
@ -44,11 +45,18 @@ dependencies:
|
||||
|
||||
sqflite: ^2.3.0
|
||||
path_provider: ^2.0.15
|
||||
permission_handler: ^10.4.3
|
||||
open_file: ^3.3.2
|
||||
webview_flutter: ^4.4.1
|
||||
webview_cookie_manager: ^2.0.6
|
||||
|
||||
flutter_svg: ^2.0.7
|
||||
google_fonts: ^5.1.0
|
||||
|
||||
cached_network_image: ^3.2.3
|
||||
|
||||
sqflite_common_ffi: ^2.3.0+2
|
||||
url_launcher: ^6.1.12
|
||||
# The following adds the Cupertino Icons font to your application.
|
||||
# Use with the CupertinoIcons class for iOS style icons.
|
||||
cupertino_icons: ^1.0.2
|
||||
@ -115,4 +123,4 @@ flutter:
|
||||
# weight: 700
|
||||
#
|
||||
# For details regarding fonts from package dependencies,
|
||||
# see https://flutter.dev/custom-fonts/#from-packages
|
||||
# see https://flutter.dev/custom-fonts/#from-packages
|
@ -6,6 +6,12 @@
|
||||
|
||||
#include "generated_plugin_registrant.h"
|
||||
|
||||
#include <permission_handler_windows/permission_handler_windows_plugin.h>
|
||||
#include <url_launcher_windows/url_launcher_windows.h>
|
||||
|
||||
void RegisterPlugins(flutter::PluginRegistry* registry) {
|
||||
PermissionHandlerWindowsPluginRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin"));
|
||||
UrlLauncherWindowsRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("UrlLauncherWindows"));
|
||||
}
|
||||
|
@ -3,6 +3,8 @@
|
||||
#
|
||||
|
||||
list(APPEND FLUTTER_PLUGIN_LIST
|
||||
permission_handler_windows
|
||||
url_launcher_windows
|
||||
)
|
||||
|
||||
list(APPEND FLUTTER_FFI_PLUGIN_LIST
|
||||
|
@ -31,6 +31,11 @@ bool FlutterWindow::OnCreate() {
|
||||
this->Show();
|
||||
});
|
||||
|
||||
// Flutter can complete the first frame before the "show window" callback is
|
||||
// registered. The following call ensures a frame is pending to ensure the
|
||||
// window is shown. It is a no-op if the first frame hasn't completed yet.
|
||||
flutter_controller_->ForceRedraw();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|