Compare commits

...

8 Commits

Author SHA1 Message Date
Imran Remtulla
05ee0f9c48 Merge pull request #366 from ImranR98/dev
Open changelog inside App for supported Sources (#82)
2023-03-18 23:54:08 -04:00
Imran Remtulla
ef06ae289e Open changelog inside App for supported Sources (#82) 2023-03-18 23:53:42 -04:00
Imran Remtulla
bd0e322465 Updated README sources section 2023-03-18 23:20:04 -04:00
Imran Remtulla
a93a2411fa Merge pull request #365 from ImranR98/dev
Added 2 Sources: Neutron Code #287, Telegram #290 (Official App, not Channels as Sources) + Tiny UI Bugfix
2023-03-18 23:12:05 -04:00
Imran Remtulla
26e6eef72e Increment version 2023-03-18 23:10:14 -04:00
Imran Remtulla
e49a6e311b Added Telegram App as a Source 2023-03-18 23:09:11 -04:00
Imran Remtulla
53d3397651 Remove download notification when download fails 2023-03-18 22:58:37 -04:00
Imran Remtulla
fe540f5e61 Added Neutron Code as a Source 2023-03-18 22:54:58 -04:00
16 changed files with 256 additions and 67 deletions

View File

@@ -19,6 +19,8 @@ Currently supported App sources:
- Third Party F-Droid Repos - Third Party F-Droid Repos
- Any URLs ending with `/fdroid/<word>`, where `<word>` can be anything - most often `repo` - Any URLs ending with `/fdroid/<word>`, where `<word>` can be anything - most often `repo`
- [Steam](https://store.steampowered.com/mobile) - [Steam](https://store.steampowered.com/mobile)
- [Telegram App](https://telegram.org)
- [Neutron Code](https://neutroncode.com)
- "HTML" (Fallback) - "HTML" (Fallback)
- Any other URL that returns an HTML page with links to APK files (if multiple, the last file alphabetically is picked) - Any other URL that returns an HTML page with links to APK files (if multiple, the last file alphabetically is picked)

View File

@@ -118,9 +118,11 @@ class Codeberg extends AppSource {
if (version == null) { if (version == null) {
throw NoVersionError(); throw NoVersionError();
} }
var changeLog = targetRelease['body'].toString();
return APKDetails(version, targetRelease['apkUrls'] as List<String>, return APKDetails(version, targetRelease['apkUrls'] as List<String>,
getAppNames(standardUrl), getAppNames(standardUrl),
releaseDate: releaseDate); releaseDate: releaseDate,
changeLog: changeLog.isEmpty ? null : changeLog);
} else { } else {
throw getObtainiumHttpError(res); throw getObtainiumHttpError(res);
} }

View File

@@ -27,9 +27,6 @@ class FDroid extends AppSource {
return url.substring(0, match.end); return url.substring(0, match.end);
} }
@override
String? changeLogPageFromStandardUrl(String standardUrl) => null;
@override @override
String? tryInferringAppId(String standardUrl, String? tryInferringAppId(String standardUrl,
{Map<String, dynamic> additionalSettings = const {}}) { {Map<String, dynamic> additionalSettings = const {}}) {

View File

@@ -160,9 +160,11 @@ class GitHub extends AppSource {
if (version == null) { if (version == null) {
throw NoVersionError(); throw NoVersionError();
} }
var changeLog = targetRelease['body'].toString();
return APKDetails(version, targetRelease['apkUrls'] as List<String>, return APKDetails(version, targetRelease['apkUrls'] as List<String>,
getAppNames(standardUrl), getAppNames(standardUrl),
releaseDate: releaseDate); releaseDate: releaseDate,
changeLog: changeLog.isEmpty ? null : changeLog);
} else { } else {
rateLimitErrorCheck(res); rateLimitErrorCheck(res);
throw getObtainiumHttpError(res); throw getObtainiumHttpError(res);

View File

@@ -10,9 +10,6 @@ class HTML extends AppSource {
return url; return url;
} }
@override
String? changeLogPageFromStandardUrl(String standardUrl) => null;
@override @override
Future<APKDetails> getLatestAPKDetails( Future<APKDetails> getLatestAPKDetails(
String standardUrl, String standardUrl,

View File

@@ -18,9 +18,6 @@ class IzzyOnDroid extends AppSource {
return url.substring(0, match.end); return url.substring(0, match.end);
} }
@override
String? changeLogPageFromStandardUrl(String standardUrl) => null;
@override @override
String? tryInferringAppId(String standardUrl, String? tryInferringAppId(String standardUrl,
{Map<String, dynamic> additionalSettings = const {}}) { {Map<String, dynamic> additionalSettings = const {}}) {

View File

@@ -0,0 +1,111 @@
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() {
host = 'neutroncode.com';
}
@override
String standardizeURL(String url) {
RegExp standardUrlRegEx = RegExp('^https?://$host/downloads/file/[^/]+');
RegExpMatch? match = standardUrlRegEx.firstMatch(url.toLowerCase());
if (match == null) {
throw InvalidURLError(name);
}
return url.substring(0, match.end);
}
@override
String? changeLogPageFromStandardUrl(String standardUrl) => standardUrl;
String monthNameToNumberString(String s) {
switch (s.toLowerCase()) {
case 'january':
return '01';
case 'february':
return '02';
case 'march':
return '03';
case 'april':
return '04';
case 'may':
return '05';
case 'june':
return '06';
case 'july':
return '07';
case 'august':
return '08';
case 'september':
return '09';
case 'october':
return '10';
case 'november':
return '11';
case 'december':
return '12';
default:
throw ArgumentError('Invalid month name: $s');
}
}
customDateParse(String dateString) {
List<String> parts = dateString.split(' ');
if (parts.length != 3) {
return null;
}
String result = '';
for (var s in parts.reversed) {
try {
try {
int.parse(s);
result += '$s-';
} catch (e) {
result += '${monthNameToNumberString(s)}-';
}
} catch (e) {
return null;
}
}
return result.substring(0, result.length - 1);
}
@override
Future<APKDetails> getLatestAPKDetails(
String standardUrl,
Map<String, dynamic> additionalSettings,
) async {
Response res = await get(Uri.parse(standardUrl));
if (res.statusCode == 200) {
var http = parse(res.body);
var name = http.querySelector('.pd-title')?.innerHtml;
var filename = http.querySelector('.pd-filename .pd-float')?.innerHtml;
if (filename == null) {
throw NoReleasesError();
}
var version =
http.querySelector('.pd-version-txt')?.nextElementSibling?.innerHtml;
if (version == null) {
throw NoVersionError();
}
String? apkUrl = 'https://$host/download/$filename';
var dateStringOriginal =
http.querySelector('.pd-date-txt')?.nextElementSibling?.innerHtml;
var dateString = dateStringOriginal != null
? (customDateParse(dateStringOriginal))
: null;
var changeLogElements = http.querySelectorAll('.pd-fdesc p');
return APKDetails(version, [apkUrl],
AppNames(runtimeType.toString(), name ?? standardUrl.split('/').last),
releaseDate: dateString != null ? DateTime.parse(dateString) : null,
changeLog: changeLogElements.isNotEmpty
? changeLogElements.last.innerHtml
: null);
} else {
throw getObtainiumHttpError(res);
}
}
}

View File

@@ -13,9 +13,6 @@ class Signal extends AppSource {
return 'https://$host'; return 'https://$host';
} }
@override
String? changeLogPageFromStandardUrl(String standardUrl) => null;
@override @override
Future<APKDetails> getLatestAPKDetails( Future<APKDetails> getLatestAPKDetails(
String standardUrl, String standardUrl,

View File

@@ -18,9 +18,6 @@ class SourceForge extends AppSource {
return url.substring(0, match.end); return url.substring(0, match.end);
} }
@override
String? changeLogPageFromStandardUrl(String standardUrl) => null;
@override @override
Future<APKDetails> getLatestAPKDetails( Future<APKDetails> getLatestAPKDetails(
String standardUrl, String standardUrl,

View File

@@ -24,9 +24,6 @@ class SteamMobile extends AppSource {
return 'https://$host'; return 'https://$host';
} }
@override
String? changeLogPageFromStandardUrl(String standardUrl) => null;
@override @override
Future<APKDetails> getLatestAPKDetails( Future<APKDetails> getLatestAPKDetails(
String standardUrl, String standardUrl,

View File

@@ -0,0 +1,40 @@
import 'package:easy_localization/easy_localization.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 TelegramApp extends AppSource {
TelegramApp() {
host = 'telegram.org';
name = 'Telegram ${tr('app')}';
}
@override
String standardizeURL(String url) {
return 'https://$host';
}
@override
Future<APKDetails> getLatestAPKDetails(
String standardUrl,
Map<String, dynamic> additionalSettings,
) async {
Response res = await get(Uri.parse('https://t.me/s/TAndroidAPK'));
if (res.statusCode == 200) {
var http = parse(res.body);
var messages =
http.querySelectorAll('.tgme_widget_message_text.js-message_text');
var version = messages.isNotEmpty
? messages.last.innerHtml.split('\n').first.trim().split(' ').first
: null;
if (version == null) {
throw NoVersionError();
}
String? apkUrl = 'https://telegram.org/dl/android/apk';
return APKDetails(version, [apkUrl], AppNames('Telegram', 'Telegram'));
} else {
throw getObtainiumHttpError(res);
}
}
}

View File

@@ -21,7 +21,7 @@ import 'package:easy_localization/src/easy_localization_controller.dart';
// ignore: implementation_imports // ignore: implementation_imports
import 'package:easy_localization/src/localization.dart'; import 'package:easy_localization/src/localization.dart';
const String currentVersion = '0.11.9'; const String currentVersion = '0.11.11';
const String currentReleaseTag = const String currentReleaseTag =
'v$currentVersion-beta'; // KEEP THIS IN SYNC WITH GITHUB RELEASES 'v$currentVersion-beta'; // KEEP THIS IN SYNC WITH GITHUB RELEASES

View File

@@ -232,6 +232,49 @@ class AppsPageState extends State<AppsPage> {
String? changesUrl = SourceProvider() String? changesUrl = SourceProvider()
.getSource(listedApps[index].app.url) .getSource(listedApps[index].app.url)
.changeLogPageFromStandardUrl(listedApps[index].app.url); .changeLogPageFromStandardUrl(listedApps[index].app.url);
String? changeLog = listedApps[index].app.changeLog;
var showChanges = (changeLog == null && changesUrl == null)
? null
: () {
if (changeLog != null) {
showDialog(
context: context,
builder: (BuildContext context) {
return GeneratedFormModal(
title: tr('changes'),
items: [],
additionalWidgets: [
Text(changeLog),
changesUrl != null
? const SizedBox(
height: 16,
)
: const SizedBox.shrink(),
changesUrl != null
? GestureDetector(
child: Text(
changesUrl,
style: const TextStyle(
decoration:
TextDecoration.underline,
fontStyle: FontStyle.italic),
),
onTap: () {
launchUrlString(changesUrl,
mode: LaunchMode
.externalApplication);
},
)
: const SizedBox.shrink()
],
singleNullReturnButton: tr('ok'),
);
});
} else {
launchUrlString(changesUrl!,
mode: LaunchMode.externalApplication);
}
};
var transparent = const Color.fromARGB(0, 0, 0, 0).value; var transparent = const Color.fromARGB(0, 0, 0, 0).value;
var hasUpdate = listedApps[index].app.installedVersion != null && var hasUpdate = listedApps[index].app.installedVersion != null &&
listedApps[index].app.installedVersion != listedApps[index].app.installedVersion !=
@@ -366,13 +409,7 @@ class AppsPageState extends State<AppsPage> {
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
GestureDetector( GestureDetector(
onTap: changesUrl == null onTap: showChanges,
? null
: () {
launchUrlString(changesUrl,
mode: LaunchMode
.externalApplication);
},
child: Text( child: Text(
listedApps[index].app.releaseDate == listedApps[index].app.releaseDate ==
null null
@@ -381,10 +418,11 @@ class AppsPageState extends State<AppsPage> {
.format(listedApps[index] .format(listedApps[index]
.app .app
.releaseDate!), .releaseDate!),
style: const TextStyle( style: TextStyle(
fontStyle: FontStyle.italic, fontStyle: FontStyle.italic,
decoration: decoration: showChanges != null
TextDecoration.underline), ? TextDecoration.underline
: TextDecoration.none),
)) ))
], ],
), ),

View File

@@ -145,6 +145,9 @@ class AppsProvider with ChangeNotifier {
} }
Future<DownloadedApk> downloadApp(App app, BuildContext? context) async { Future<DownloadedApk> downloadApp(App app, BuildContext? context) async {
NotificationsProvider? notificationsProvider =
context?.read<NotificationsProvider>();
var notifId = DownloadNotification(app.name, 0).id;
if (apps[app.id] != null) { if (apps[app.id] != null) {
apps[app.id]!.downloadProgress = 0; apps[app.id]!.downloadProgress = 0;
notifyListeners(); notifyListeners();
@@ -155,8 +158,6 @@ class AppsProvider with ChangeNotifier {
String downloadUrl = await SourceProvider() String downloadUrl = await SourceProvider()
.getSource(app.url) .getSource(app.url)
.apkUrlPrefetchModifier(app.apkUrls[app.preferredApkIndex]); .apkUrlPrefetchModifier(app.apkUrls[app.preferredApkIndex]);
NotificationsProvider? notificationsProvider =
context?.read<NotificationsProvider>();
var notif = DownloadNotification(app.name, 100); var notif = DownloadNotification(app.name, 100);
notificationsProvider?.cancel(notif.id); notificationsProvider?.cancel(notif.id);
int? prevProg; int? prevProg;
@@ -173,7 +174,6 @@ class AppsProvider with ChangeNotifier {
} }
prevProg = prog; prevProg = prog;
}); });
notificationsProvider?.cancel(notif.id);
// Delete older versions of the APK if any // Delete older versions of the APK if any
for (var file in downloadedFile.parent.listSync()) { for (var file in downloadedFile.parent.listSync()) {
var fn = file.path.split('/').last; var fn = file.path.split('/').last;
@@ -201,6 +201,7 @@ class AppsProvider with ChangeNotifier {
} }
return DownloadedApk(app.id, downloadedFile); return DownloadedApk(app.id, downloadedFile);
} finally { } finally {
notificationsProvider?.cancel(notifId);
if (apps[app.id] != null) { if (apps[app.id] != null) {
apps[app.id]!.downloadProgress = null; apps[app.id]!.downloadProgress = null;
notifyListeners(); notifyListeners();

View File

@@ -15,9 +15,11 @@ import 'package:obtainium/app_sources/gitlab.dart';
import 'package:obtainium/app_sources/izzyondroid.dart'; import 'package:obtainium/app_sources/izzyondroid.dart';
import 'package:obtainium/app_sources/html.dart'; import 'package:obtainium/app_sources/html.dart';
import 'package:obtainium/app_sources/mullvad.dart'; import 'package:obtainium/app_sources/mullvad.dart';
import 'package:obtainium/app_sources/neutroncode.dart';
import 'package:obtainium/app_sources/signal.dart'; import 'package:obtainium/app_sources/signal.dart';
import 'package:obtainium/app_sources/sourceforge.dart'; import 'package:obtainium/app_sources/sourceforge.dart';
import 'package:obtainium/app_sources/steammobile.dart'; import 'package:obtainium/app_sources/steammobile.dart';
import 'package:obtainium/app_sources/telegramapp.dart';
import 'package:obtainium/components/generated_form.dart'; import 'package:obtainium/components/generated_form.dart';
import 'package:obtainium/custom_errors.dart'; import 'package:obtainium/custom_errors.dart';
import 'package:obtainium/mass_app_sources/githubstars.dart'; import 'package:obtainium/mass_app_sources/githubstars.dart';
@@ -34,8 +36,10 @@ class APKDetails {
late List<String> apkUrls; late List<String> apkUrls;
late AppNames names; late AppNames names;
late DateTime? releaseDate; late DateTime? releaseDate;
late String? changeLog;
APKDetails(this.version, this.apkUrls, this.names, {this.releaseDate}); APKDetails(this.version, this.apkUrls, this.names,
{this.releaseDate, this.changeLog});
} }
class App { class App {
@@ -52,6 +56,7 @@ class App {
bool pinned = false; bool pinned = false;
List<String> categories; List<String> categories;
late DateTime? releaseDate; late DateTime? releaseDate;
late String? changeLog;
App( App(
this.id, this.id,
this.url, this.url,
@@ -65,7 +70,8 @@ class App {
this.lastUpdateCheck, this.lastUpdateCheck,
this.pinned, this.pinned,
{this.categories = const [], {this.categories = const [],
this.releaseDate}); this.releaseDate,
this.changeLog});
@override @override
String toString() { String toString() {
@@ -155,7 +161,8 @@ class App {
releaseDate: json['releaseDate'] == null releaseDate: json['releaseDate'] == null
? null ? null
: DateTime.fromMicrosecondsSinceEpoch(json['releaseDate']), : DateTime.fromMicrosecondsSinceEpoch(json['releaseDate']),
); changeLog:
json['changeLog'] == null ? null : json['changeLog'] as String);
} }
Map<String, dynamic> toJson() => { Map<String, dynamic> toJson() => {
@@ -171,7 +178,8 @@ class App {
'lastUpdateCheck': lastUpdateCheck?.microsecondsSinceEpoch, 'lastUpdateCheck': lastUpdateCheck?.microsecondsSinceEpoch,
'pinned': pinned, 'pinned': pinned,
'categories': categories, 'categories': categories,
'releaseDate': releaseDate?.microsecondsSinceEpoch 'releaseDate': releaseDate?.microsecondsSinceEpoch,
'changeLog': changeLog
}; };
} }
@@ -338,6 +346,8 @@ class SourceProvider {
APKMirror(), APKMirror(),
FDroidRepo(), FDroidRepo(),
SteamMobile(), SteamMobile(),
TelegramApp(),
NeutronCode(),
HTML() // This should ALWAYS be the last option as they are tried in order HTML() // This should ALWAYS be the last option as they are tried in order
]; ];
@@ -433,7 +443,8 @@ class SourceProvider {
DateTime.now(), DateTime.now(),
currentApp?.pinned ?? false, currentApp?.pinned ?? false,
categories: currentApp?.categories ?? const [], categories: currentApp?.categories ?? const [],
releaseDate: apk.releaseDate); releaseDate: apk.releaseDate,
changeLog: apk.changeLog);
} }
// Returns errors in [results, errors] instead of throwing them // Returns errors in [results, errors] instead of throwing them

View File

@@ -17,7 +17,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 # 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 # 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. # of the product and file versions while build-number is used as the build suffix.
version: 0.11.9+130 # When changing this, update the tag in main() accordingly version: 0.11.11+132 # When changing this, update the tag in main() accordingly
environment: environment:
sdk: '>=2.18.2 <3.0.0' sdk: '>=2.18.2 <3.0.0'