Files
Obtainium/lib/app_sources/neutroncode.dart
Imran Remtulla 4b0c5405df fix: resolve 30+ bugs, safety issues, and code quality problems across the codebase
HIGH severity fixes:
- Fix indexOf -> startsWith for URL scheme detection to prevent corrupting
  non-http URLs (source_provider.dart)
- Fix OOM risk: use XFile(path) instead of readAsBytes() for large APKs
  (apps_provider_install.dart)
- Fix GeneratedFormSwitch.ensureType returning String instead of bool,
  causing runtime _CastError (generated_form_model.dart)
- Fix initForm() called during build() triggering parent setState by moving
  init to didUpdateWidget + postFrameCallback (generated_form_renderer.dart)
- Fix CI sed no-op: update regex to match actual signingConfig pattern in
  build.gradle.kts (release.yml)
- Fix build.sh auto git push on no-args: require explicit arg to sync+build
- Pin all git dependencies to commit SHAs instead of mutable branches

MEDIUM severity fixes:
- Sequence background service start/stop calls to prevent races (main.dart)
- Move loadSystemFont() from build() to init to prevent visible font flash
- Prevent concurrent bgUpdateCheck calls in onRepeatEvent with guard flag
- Extend download retry to SocketException and TimeoutException
- Cap install permission loop at 10 attempts to prevent infinite hang
- Remove dead unused headers variable causing double bcrypt hash (coolapk.dart)
- Remove HTTP 304 from redirect status check (huaweiappgallery.dart)
- Guard result.files.single against empty file picker results (import_export.dart)
- Add error logging to linkFn catch block (add_app.dart)
- Disable continue button when no download URL available (app_detail_widgets.dart)
- Fix DEVELOPER_GUIDE.md: list all git-pinned deps, correct android_package_manager

LOW severity fixes:
- Use 'final' instead of 'late' for FDroid field (izzyondroid.dart)
- Use stable 'name' instead of runtime-type sourceIdentifier (neutroncode.dart)
- Fix typo: finalUrlKey -> urlDataKey (uptodown.dart)
- Fix aptoide constructor field order for consistency
- Remove dead app?.app == null condition (app.dart)
- Reuse SourceProvider instance instead of throwaway (apps.dart)
- Support descending sort for 'as added' column (app_list_tile.dart)
- Remove SDK >= 31 guard causing layout shift for material-you option (settings.dart)
- Secure standardize.mjs: atomic write-then-rename, Object.hasOwn, error handling
- Add flutter_launcher_icons to dev_dependencies
2026-07-02 15:53:06 +01:00

113 lines
3.4 KiB
Dart

import 'package:html/parser.dart';
import 'package:http/http.dart';
import 'package:obtainium/custom_errors.dart';
import 'package:obtainium/providers/source_provider.dart';
class NeutronCode extends AppSource {
NeutronCode() {
name = 'NeutronCode';
hosts = ['neutroncode.com'];
showReleaseDateAsVersionToggle = true;
changeLogPageIsStandardUrl = true;
}
@override
String sourceSpecificStandardizeURL(
String url, {
bool forSelection = false,
}) => standardizeUrlWithRegex(
url,
subdomainPrefix: r'(www\.)?',
pathPattern: r'/downloads/file/[^/]+',
);
static const _monthMap = {
'january': '01',
'february': '02',
'march': '03',
'april': '04',
'may': '05',
'june': '06',
'july': '07',
'august': '08',
'september': '09',
'october': '10',
'november': '11',
'december': '12',
};
String monthNameToNumberString(String s) =>
_monthMap[s.toLowerCase()] ??
(throw ArgumentError('Invalid month name: $s'));
String? formatDateForParsing(String dateString) {
final List<String> parts = dateString.split(' ');
if (parts.length != 3) {
return null;
}
final monthIdx = parts.indexWhere((s) => int.tryParse(s) == null);
if (monthIdx < 0) return null;
final month = monthNameToNumberString(parts[monthIdx]);
final numericParts = [
for (var i = 0; i < 3; i++)
if (i != monthIdx) int.tryParse(parts[i]),
];
if (numericParts.contains(null) || numericParts.length != 2) return null;
final a = numericParts[0]!, b = numericParts[1]!;
final year = a > 31 ? a : (b > 31 ? b : (a.toString().length == 4 ? a : b));
final day = a == year ? b : a;
return '$year-$month-${day.toString().padLeft(2, '0')}';
}
@override
Future<APKDetails> getLatestAPKDetails(
String standardUrl,
Map<String, dynamic> additionalSettings,
) async {
try {
final Response res = await sourceRequest(standardUrl, additionalSettings);
if (res.statusCode == 200) {
final http = parse(res.body);
final name = http.querySelector('.pd-title')?.innerHtml;
final filename = http
.querySelector('.pd-filename .pd-float')
?.innerHtml;
if (filename == null) {
throw NoReleasesError();
}
final version = http
.querySelector('.pd-version-txt')
?.nextElementSibling
?.innerHtml;
if (version == null || version.isEmpty) {
throw NoVersionError();
}
final String apkUrl = 'https://${hosts[0]}/download/$filename';
final dateStringOriginal = http
.querySelector('.pd-date-txt')
?.nextElementSibling
?.innerHtml;
final dateString = dateStringOriginal != null
? (formatDateForParsing(dateStringOriginal))
: null;
final changeLogElements = http.querySelectorAll('.pd-fdesc p');
return APKDetails(
version,
getApkUrlsFromUrls([apkUrl]),
AppNames(this.name, name ?? standardUrl.split('/').last),
releaseDate: dateString != null
? DateTime.tryParse(dateString)
: null,
changeLog: changeLogElements.isNotEmpty
? changeLogElements.last.innerHtml
: null,
);
} else {
throw getObtainiumHttpError(res);
}
} catch (e) {
rethrowOrWrapError(e);
}
}
}