Skip to content

Commit 3f68456

Browse files
authored
Merge pull request #40 from warreth/fix
Fixes
2 parents b2e5a28 + 211f1b8 commit 3f68456

14 files changed

Lines changed: 1865 additions & 284 deletions

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,3 +51,5 @@ app.*.map.json
5151
*.jdk/
5252
openJdk-25.jdk/
5353
oracleJdk-25.jdk/
54+
android/.kotlin/
55+
test/android-test.sh

android/app/build.gradle

Lines changed: 14 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -48,25 +48,24 @@ android {
4848
}
4949

5050
signingConfigs {
51-
release {
52-
// FIX: Use a conditional check (the Elvis operator ?: '')
53-
// to ensure 'file()' never gets a null or missing path string.
54-
// This is the source of the path='null' error.
55-
keyAlias = keystoreProperties['keyAlias']
56-
keyPassword = keystoreProperties['keyPassword']
57-
storeFile = file(keystoreProperties['storeFile'] ?: '') // <--- CRITICAL FIX
58-
storePassword = keystoreProperties['storePassword']
59-
v1SigningEnabled = true
60-
v2SigningEnabled = true
51+
if (keystorePropertiesFile.exists()) {
52+
release {
53+
keyAlias = keystoreProperties['keyAlias']
54+
keyPassword = keystoreProperties['keyPassword']
55+
storeFile = keystoreProperties['storeFile'] ? file(keystoreProperties['storeFile']) : null
56+
storePassword = keystoreProperties['storePassword']
57+
v1SigningEnabled = true
58+
v2SigningEnabled = true
59+
}
6160
}
62-
} // Line 56 is near here, depending on your exact formatting
61+
}
6362

6463
buildTypes {
6564
release {
66-
// Revert to unconditional assignment. If storeFile is missing,
67-
// Gradle will now error with "storeFile is missing required property",
68-
// which is clearer than the "path='null'" error.
69-
signingConfig signingConfigs.release
65+
// Only use release signing if key.properties exists
66+
if (keystorePropertiesFile.exists()) {
67+
signingConfig signingConfigs.release
68+
}
7069

7170
minifyEnabled = false
7271
shrinkResources = false
Lines changed: 27 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,62 +1,41 @@
1-
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
2-
xmlns:tools="http://schemas.android.com/tools"
3-
>
4-
<uses-permission android:name="android.permission.INTERNET" />
5-
<uses-permission android:name="android.permission.WAKE_LOCK" />
6-
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
7-
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
8-
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
9-
10-
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
11-
<uses-permission android:name="android.permission.READ_MEDIA_VIDEOS" />
12-
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />
13-
14-
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
1+
<manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools">
2+
<uses-permission android:name="android.permission.INTERNET"/>
3+
<uses-permission android:name="android.permission.WAKE_LOCK"/>
4+
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
5+
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
6+
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC"/>
7+
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES"/>
8+
<uses-permission android:name="android.permission.READ_MEDIA_VIDEOS"/>
9+
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO"/>
10+
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"/>
1511
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
16-
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="29"/>
17-
12+
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="29"/><!-- Permission required for installing APK updates -->
13+
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES"/>
1814
<queries>
1915
<intent>
20-
<action android:name="android.intent.action.VIEW" />
21-
<category android:name="android.intent.category.BROWSABLE" />
22-
<data android:scheme="https" />
16+
<action android:name="android.intent.action.VIEW"/>
17+
<category android:name="android.intent.category.BROWSABLE"/>
18+
<data android:scheme="https"/>
2319
</intent>
2420
</queries>
25-
26-
<application
27-
android:label="Openlib"
28-
android:name="${applicationName}"
29-
android:requestLegacyExternalStorage="true"
30-
android:networkSecurityConfig="@xml/network_security_config"
31-
android:icon="@mipmap/launcher_icon"
32-
android:enableOnBackInvokedCallback="true">
33-
34-
<activity
35-
android:name=".MainActivity"
36-
android:exported="true"
37-
android:launchMode="singleTop"
38-
android:theme="@style/LaunchTheme"
39-
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
40-
android:hardwareAccelerated="true"
41-
android:windowSoftInputMode="adjustResize">
42-
<!-- Specifies an Android theme to apply to this Activity as soon as
21+
<application android:label="Openlib" android:name="${applicationName}" android:requestLegacyExternalStorage="true" android:networkSecurityConfig="@xml/network_security_config" android:icon="@mipmap/launcher_icon" android:enableOnBackInvokedCallback="true">
22+
<activity android:name=".MainActivity" android:exported="true" android:launchMode="singleTop" android:theme="@style/LaunchTheme" android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode" android:hardwareAccelerated="true" android:windowSoftInputMode="adjustResize"><!-- Specifies an Android theme to apply to this Activity as soon as
4323
the Android process has started. This theme is visible to the user
4424
while the Flutter UI initializes. After that, this theme continues
4525
to determine the Window background behind the Flutter UI. -->
46-
47-
<meta-data
48-
android:name="io.flutter.embedding.android.NormalTheme"
49-
android:resource="@style/NormalTheme"
50-
/>
26+
<meta-data android:name="io.flutter.embedding.android.NormalTheme" android:resource="@style/NormalTheme"/>
5127
<intent-filter>
5228
<action android:name="android.intent.action.MAIN"/>
5329
<category android:name="android.intent.category.LAUNCHER"/>
30+
</intent-filter><!-- Intent filter for android_package_installer -->
31+
<intent-filter>
32+
<action android:name="com.android_package_installer.content.SESSION_API_PACKAGE_INSTALLED"/>
5433
</intent-filter>
55-
</activity>
56-
<!-- Don't delete the meta-data below.
34+
</activity><!-- Don't delete the meta-data below.
5735
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
58-
<meta-data
59-
android:name="flutterEmbedding"
60-
android:value="2" />
36+
<meta-data android:name="flutterEmbedding" android:value="2"/><!-- FileProvider for sharing files and APK installation -->
37+
<provider android:name="androidx.core.content.FileProvider" android:authorities="${applicationId}.fileprovider" android:exported="false" android:grantUriPermissions="true" tools:replace="android:authorities">
38+
<meta-data android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/file_paths"/>
39+
</provider>
6140
</application>
62-
</manifest>
41+
</manifest>
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<paths xmlns:android="http://schemas.android.com/apk/res/android"><!-- Root path for accessing any file (Android 10+) -->
3+
<root-path name="root" path="."/><!-- Provides access to external storage directory -->
4+
<external-path name="external_files" path="."/><!-- Provides access to files in the app's external files directory -->
5+
<external-files-path name="external_app_files" path="."/><!-- Provides access to cache directory -->
6+
<cache-path name="cache" path="."/><!-- Provides access to external cache directory -->
7+
<external-cache-path name="external_cache" path="."/><!-- Provides access to files directory -->
8+
<files-path name="files" path="."/>
9+
</paths>

lib/main.dart

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -326,6 +326,12 @@ class _MainScreenState extends ConsumerState<MainScreen> {
326326
final isDarkMode = Theme.of(context).brightness == Brightness.dark;
327327
final selectedIndex = ref.watch(selectedIndexProvider);
328328

329+
// Calculate proper header height including status bar on mobile
330+
final statusBarHeight = MediaQuery.of(context).padding.top;
331+
final expandedHeaderHeight = PlatformUtils.isMobile
332+
? kToolbarHeight + statusBarHeight
333+
: kToolbarHeight;
334+
329335
return Scaffold(
330336
body: NotificationListener<UserScrollNotification>(
331337
onNotification: (notification) {
@@ -342,8 +348,9 @@ class _MainScreenState extends ConsumerState<MainScreen> {
342348
children: [
343349
AnimatedContainer(
344350
duration: const Duration(milliseconds: 200),
345-
height: _showExpandedHeader ? kToolbarHeight : 0,
351+
height: _showExpandedHeader ? expandedHeaderHeight : 0,
346352
child: AppBar(
353+
toolbarHeight: kToolbarHeight,
347354
backgroundColor: Theme.of(context).colorScheme.surface,
348355
title: const Text("Openlib"),
349356
titleTextStyle: Theme.of(context).textTheme.displayLarge,

lib/services/annas_archieve.dart

Lines changed: 102 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import 'package:html/dom.dart' as dom;
99
// Project imports:
1010
import 'package:openlib/services/instance_manager.dart';
1111
import 'package:openlib/services/logger.dart';
12+
import 'package:openlib/services/network_error.dart';
1213

1314
// ====================================================================
1415
// DATA MODELS
@@ -73,6 +74,41 @@ class AnnasArchieve {
7374
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36",
7475
};
7576

77+
// Check for Cloudflare block in response
78+
bool _isCloudflareBlocked(Response response) {
79+
// Check cf-mitigated header
80+
if (response.headers.value("cf-mitigated") == "challenge") {
81+
return true;
82+
}
83+
84+
// Check response body for Cloudflare markers
85+
final body = response.data?.toString().toLowerCase() ?? "";
86+
final markers = [
87+
"checking your browser",
88+
"cloudflare",
89+
"cf-browser-verification",
90+
"just a moment",
91+
"enable javascript and cookies",
92+
"ray id:",
93+
"attention required",
94+
"ddos protection",
95+
];
96+
97+
for (final marker in markers) {
98+
if (body.contains(marker)) {
99+
return true;
100+
}
101+
}
102+
return false;
103+
}
104+
105+
// Convert DioException to user-friendly NetworkError with async diagnostics
106+
Future<NetworkError> _handleErrorAsync(dynamic error,
107+
{String? responseBody, String? targetHost}) async {
108+
return await NetworkError.fromExceptionAsync(error,
109+
responseBody: responseBody, targetHost: targetHost);
110+
}
111+
76112
// Try request with optimized retry logic - fast failure and fallback
77113
Future<T> _requestWithRetry<T>(
78114
Future<T> Function(String baseUrl) requestFn,
@@ -85,10 +121,12 @@ class AnnasArchieve {
85121
}
86122

87123
Exception? lastException;
124+
String? lastUsedHost;
88125

89126
// Try each instance - they should already be sorted by speed from auto-ranking
90127
for (int i = 0; i < instances.length; i++) {
91128
final instance = instances[i];
129+
lastUsedHost = instance.baseUrl;
92130

93131
// Fewer retries for subsequent instances (they're slower)
94132
final retriesForThis = i == 0 ? maxRetriesPerInstance : 0;
@@ -127,9 +165,14 @@ class AnnasArchieve {
127165
}
128166
}
129167

130-
// All instances failed
168+
// All instances failed - throw with diagnostic info
131169
_logger.error('All instances failed', tag: 'AnnasArchive');
132-
throw lastException ?? Exception('All instances failed');
170+
171+
// Throw a diagnostic NetworkError instead of the raw exception
172+
throw await _handleErrorAsync(
173+
lastException ?? Exception('All instances failed'),
174+
targetHost: lastUsedHost,
175+
);
133176
}
134177

135178
String getMd5(String url) {
@@ -407,19 +450,40 @@ class AnnasArchieve {
407450
tag: 'AnnasArchive', metadata: {'url': encodedURL});
408451
final response = await dio.get(encodedURL,
409452
options: Options(headers: defaultDioHeaders));
453+
454+
// Check for Cloudflare block in the response
455+
if (_isCloudflareBlocked(response)) {
456+
_logger.warning('Cloudflare block detected in search response',
457+
tag: 'AnnasArchive');
458+
throw NetworkError(
459+
type: NetworkErrorType.cloudflareBlock,
460+
userMessage: "Access blocked by Cloudflare protection",
461+
solution:
462+
"This site is protected and blocking your access.\n\n🔧 Solutions to try:\n• Use a VPN (recommended)\n• Change your DNS to 1.1.1.1 or 8.8.8.8\n• Try a different network\n• Wait a few minutes and retry",
463+
technicalDetails: "Cloudflare challenge detected in response",
464+
rawResponseBody: response.data?.toString(),
465+
);
466+
}
467+
410468
return _parser(response.data, fileType, currentBaseUrl);
411469
});
412470

413471
_logger.info('Search completed',
414472
tag: 'AnnasArchive', metadata: {'results': books.length});
415473
return books;
474+
} on NetworkError {
475+
// Re-throw NetworkError as-is for proper UI handling
476+
rethrow;
416477
} on DioException catch (e) {
417478
_logger.error('Search failed',
418479
tag: 'AnnasArchive', error: e.message ?? e.error);
419-
if (e.type == DioExceptionType.unknown) {
420-
throw "socketException";
421-
}
422-
rethrow;
480+
// Convert to user-friendly NetworkError with diagnostics
481+
throw await _handleErrorAsync(e,
482+
responseBody: e.response?.data?.toString());
483+
} catch (e) {
484+
_logger.error('Unexpected search error',
485+
tag: 'AnnasArchive', error: e.toString());
486+
throw await _handleErrorAsync(e);
423487
}
424488
}
425489

@@ -445,12 +509,33 @@ class AnnasArchieve {
445509
tag: 'AnnasArchive', metadata: {'url': adjustedUrl});
446510
final response = await dio.get(adjustedUrl,
447511
options: Options(headers: defaultDioHeaders));
512+
513+
// Check for Cloudflare block in the response
514+
if (_isCloudflareBlocked(response)) {
515+
_logger.warning('Cloudflare block detected in book info response',
516+
tag: 'AnnasArchive');
517+
throw NetworkError(
518+
type: NetworkErrorType.cloudflareBlock,
519+
userMessage: "Access blocked by Cloudflare protection",
520+
solution:
521+
"This site is protected and blocking your access.\n\n🔧 Solutions to try:\n• Use a VPN (recommended)\n• Change your DNS to 1.1.1.1 or 8.8.8.8\n• Try a different network\n• Wait a few minutes and retry",
522+
technicalDetails: "Cloudflare challenge detected in response",
523+
rawResponseBody: response.data?.toString(),
524+
);
525+
}
526+
448527
BookInfoData? data =
449528
await _bookInfoParser(response.data, adjustedUrl, currentBaseUrl);
450529
if (data != null) {
451530
return data;
452531
} else {
453-
throw 'unable to get data';
532+
throw NetworkError(
533+
type: NetworkErrorType.unknown,
534+
userMessage: "Unable to load book details",
535+
solution:
536+
"The book information could not be retrieved. Try again or try a different mirror in Settings.",
537+
technicalDetails: "Parser returned null for URL: $adjustedUrl",
538+
);
454539
}
455540
});
456541

@@ -462,13 +547,19 @@ class AnnasArchieve {
462547
'hasMirror': data.mirror != null,
463548
});
464549
return data;
550+
} on NetworkError {
551+
// Re-throw NetworkError as-is for proper UI handling
552+
rethrow;
465553
} on DioException catch (e) {
466554
_logger.error('Failed to fetch book info',
467555
tag: 'AnnasArchive', error: e.message ?? e.error);
468-
if (e.type == DioExceptionType.unknown) {
469-
throw "socketException";
470-
}
471-
rethrow;
556+
// Convert to user-friendly NetworkError with diagnostics
557+
throw await _handleErrorAsync(e,
558+
responseBody: e.response?.data?.toString());
559+
} catch (e) {
560+
_logger.error('Unexpected book info error',
561+
tag: 'AnnasArchive', error: e.toString());
562+
throw await _handleErrorAsync(e);
472563
}
473564
}
474565
}

0 commit comments

Comments
 (0)