Compare commits

...

12 Commits

Author SHA1 Message Date
6a5e7942ee Merge pull request #301 from ImranR98/dev
App edit bugfixes
2023-02-18 21:39:55 -05:00
859158e84a App edit bugfixes 2023-02-18 21:39:26 -05:00
435116e10b Merge pull request #300 from ImranR98/dev
Release Date Support for Some Sources (#210 + #298) + UI Changes (#274) + Bugfix (#299)
2023-02-18 21:24:36 -05:00
a788d9d7cd Increment version 2023-02-18 21:22:36 -05:00
4be3478b97 Added release date support to APKMirror 2023-02-18 21:16:28 -05:00
fe0126095a Added release date support to third part f-droid repos 2023-02-18 21:03:22 -05:00
d5fdf28a98 Added release date support to Codeberg 2023-02-18 20:58:08 -05:00
f06d245e20 Added release date support to GitLab 2023-02-18 20:55:23 -05:00
2b4f94b407 Date sort bugfix 2023-02-18 20:49:45 -05:00
5f7e342e6b Added rel. date sort 2023-02-18 20:47:29 -05:00
191776d0d5 Initial release date support 2023-02-18 20:37:30 -05:00
ea81b0e66e Bugfix for different ID same URL Apps (#299) 2023-02-18 18:31:42 -05:00
22 changed files with 323 additions and 143 deletions

View File

@ -213,6 +213,10 @@
"removeFromObtainium": "Remove from Obtainium", "removeFromObtainium": "Remove from Obtainium",
"uninstallFromDevice": "Uninstall from Device", "uninstallFromDevice": "Uninstall from Device",
"onlyWorksWithNonVersionDetectApps": "Only works for Apps with version detection disabled.", "onlyWorksWithNonVersionDetectApps": "Only works for Apps with version detection disabled.",
"useReleaseDateAsVersion": "Use Release Date as Version",
"releaseDateAsVersionExplanation": "This option should only be used for Apps where version detection does not work correctly, but a release date is available.",
"changes": "Changes",
"releaseDate": "Release Date",
"removeAppQuestion": { "removeAppQuestion": {
"one": "App entfernen?", "one": "App entfernen?",
"other": "App entfernen?" "other": "App entfernen?"

View File

@ -213,6 +213,10 @@
"removeFromObtainium": "Remove from Obtainium", "removeFromObtainium": "Remove from Obtainium",
"uninstallFromDevice": "Uninstall from Device", "uninstallFromDevice": "Uninstall from Device",
"onlyWorksWithNonVersionDetectApps": "Only works for Apps with version detection disabled.", "onlyWorksWithNonVersionDetectApps": "Only works for Apps with version detection disabled.",
"useReleaseDateAsVersion": "Use Release Date as Version",
"releaseDateAsVersionExplanation": "This option should only be used for Apps where version detection does not work correctly, but a release date is available.",
"changes": "Changes",
"releaseDate": "Release Date",
"removeAppQuestion": { "removeAppQuestion": {
"one": "Remove App?", "one": "Remove App?",
"other": "Remove Apps?" "other": "Remove Apps?"

View File

@ -213,6 +213,10 @@
"removeFromObtainium": "از Obtainium حذف کنید", "removeFromObtainium": "از Obtainium حذف کنید",
"uninstallFromDevice": "حذف نصب از دستگاه", "uninstallFromDevice": "حذف نصب از دستگاه",
"onlyWorksWithNonVersionDetectApps": "فقط برای برنامه‌هایی کار می‌کند که تشخیص نسخه غیرفعال است.", "onlyWorksWithNonVersionDetectApps": "فقط برای برنامه‌هایی کار می‌کند که تشخیص نسخه غیرفعال است.",
"useReleaseDateAsVersion": "Use Release Date as Version",
"releaseDateAsVersionExplanation": "This option should only be used for Apps where version detection does not work correctly, but a release date is available.",
"changes": "Changes",
"releaseDate": "Release Date",
"removeAppQuestion": { "removeAppQuestion": {
"one": "برنامه حذف شود؟", "one": "برنامه حذف شود؟",
"other": "برنامه ها حذف شوند؟" "other": "برنامه ها حذف شوند؟"

View File

@ -212,6 +212,10 @@
"removeFromObtainium": "Eltávolítás az Obtainiumból", "removeFromObtainium": "Eltávolítás az Obtainiumból",
"uninstallFromDevice": "Eltávolítás a készülékről", "uninstallFromDevice": "Eltávolítás a készülékről",
"onlyWorksWithNonVersionDetectApps": "Csak azoknál az alkalmazásoknál működik, amelyeknél a verzióérzékelés le van tiltva.", "onlyWorksWithNonVersionDetectApps": "Csak azoknál az alkalmazásoknál működik, amelyeknél a verzióérzékelés le van tiltva.",
"useReleaseDateAsVersion": "Use Release Date as Version",
"releaseDateAsVersionExplanation": "This option should only be used for Apps where version detection does not work correctly, but a release date is available.",
"changes": "Changes",
"releaseDate": "Release Date",
"removeAppQuestion": { "removeAppQuestion": {
"one": "Eltávolítja az alkalmazást?", "one": "Eltávolítja az alkalmazást?",
"other": "Eltávolítja az alkalmazást?" "other": "Eltávolítja az alkalmazást?"

View File

@ -213,6 +213,10 @@
"removeFromObtainium": "Rimuovi da Obtainium", "removeFromObtainium": "Rimuovi da Obtainium",
"uninstallFromDevice": "Disinstalla dal dispositivo", "uninstallFromDevice": "Disinstalla dal dispositivo",
"onlyWorksWithNonVersionDetectApps": "Funziona solo per le App con il rilevamento della versione disattivato.", "onlyWorksWithNonVersionDetectApps": "Funziona solo per le App con il rilevamento della versione disattivato.",
"useReleaseDateAsVersion": "Use Release Date as Version",
"releaseDateAsVersionExplanation": "This option should only be used for Apps where version detection does not work correctly, but a release date is available.",
"changes": "Changes",
"releaseDate": "Release Date",
"removeAppQuestion": { "removeAppQuestion": {
"one": "Rimuovere l'App?", "one": "Rimuovere l'App?",
"other": "Rimuovere le App?" "other": "Rimuovere le App?"

View File

@ -213,6 +213,10 @@
"removeFromObtainium": "Obtainiumから削除する", "removeFromObtainium": "Obtainiumから削除する",
"uninstallFromDevice": "デバイスからアンインストールする", "uninstallFromDevice": "デバイスからアンインストールする",
"onlyWorksWithNonVersionDetectApps": "バージョン検出を無効にしているアプリにのみ動作します。", "onlyWorksWithNonVersionDetectApps": "バージョン検出を無効にしているアプリにのみ動作します。",
"useReleaseDateAsVersion": "Use Release Date as Version",
"releaseDateAsVersionExplanation": "This option should only be used for Apps where version detection does not work correctly, but a release date is available.",
"changes": "Changes",
"releaseDate": "Release Date",
"removeAppQuestion": { "removeAppQuestion": {
"one": "アプリを削除しますか?", "one": "アプリを削除しますか?",
"other": "アプリを削除しますか?" "other": "アプリを削除しますか?"

View File

@ -213,6 +213,10 @@
"filterAPKsByRegEx": "Filter APKs by Regular Expression", "filterAPKsByRegEx": "Filter APKs by Regular Expression",
"removeFromObtainium": "Remove from Obtainium", "removeFromObtainium": "Remove from Obtainium",
"uninstallFromDevice": "Uninstall from Device", "uninstallFromDevice": "Uninstall from Device",
"useReleaseDateAsVersion": "Use Release Date as Version",
"releaseDateAsVersionExplanation": "This option should only be used for Apps where version detection does not work correctly, but a release date is available.",
"changes": "Changes",
"releaseDate": "Release Date",
"removeAppQuestion": { "removeAppQuestion": {
"one": "删除应用?", "one": "删除应用?",
"other": "删除应用?" "other": "删除应用?"

View File

@ -1,3 +1,5 @@
import 'dart:io';
import 'package:html/parser.dart'; import 'package:html/parser.dart';
import 'package:http/http.dart'; import 'package:http/http.dart';
import 'package:obtainium/custom_errors.dart'; import 'package:obtainium/custom_errors.dart';
@ -30,10 +32,16 @@ class APKMirror extends AppSource {
) async { ) async {
Response res = await get(Uri.parse('$standardUrl/feed')); Response res = await get(Uri.parse('$standardUrl/feed'));
if (res.statusCode == 200) { if (res.statusCode == 200) {
String? titleString = parse(res.body) var item = parse(res.body).querySelector('item');
.querySelector('item') String? titleString = item?.querySelector('title')?.innerHtml;
?.querySelector('title') String? dateString = item
?.innerHtml; ?.querySelector('pubDate')
?.innerHtml
.split(' ')
.sublist(0, 5)
.join(' ');
DateTime? releaseDate =
dateString != null ? HttpDate.parse('$dateString GMT') : null;
String? version = titleString String? version = titleString
?.substring(RegExp('[0-9]').firstMatch(titleString)?.start ?? 0, ?.substring(RegExp('[0-9]').firstMatch(titleString)?.start ?? 0,
RegExp(' by ').firstMatch(titleString)?.start ?? 0) RegExp(' by ').firstMatch(titleString)?.start ?? 0)
@ -44,7 +52,8 @@ class APKMirror extends AppSource {
if (version == null || version.isEmpty) { if (version == null || version.isEmpty) {
throw NoVersionError(); throw NoVersionError();
} }
return APKDetails(version, [], getAppNames(standardUrl)); return APKDetails(version, [], getAppNames(standardUrl),
releaseDate: releaseDate);
} else { } else {
throw getObtainiumHttpError(res); throw getObtainiumHttpError(res);
} }

View File

@ -112,11 +112,15 @@ class Codeberg extends AppSource {
throw NoReleasesError(); throw NoReleasesError();
} }
String? version = targetRelease['tag_name']; String? version = targetRelease['tag_name'];
DateTime? releaseDate = targetRelease['published_at'] != null
? DateTime.parse(targetRelease['published_at'])
: null;
if (version == null) { if (version == null) {
throw NoVersionError(); throw NoVersionError();
} }
return APKDetails(version, targetRelease['apkUrls'] as List<String>, return APKDetails(version, targetRelease['apkUrls'] as List<String>,
getAppNames(standardUrl)); getAppNames(standardUrl),
releaseDate: releaseDate);
} else { } else {
throw getObtainiumHttpError(res); throw getObtainiumHttpError(res);
} }

View File

@ -69,6 +69,8 @@ class FDroidRepo extends AppSource {
foundApps[0].querySelector('name')?.innerHtml ?? appIdOrName; foundApps[0].querySelector('name')?.innerHtml ?? appIdOrName;
var releases = foundApps[0].querySelectorAll('package'); var releases = foundApps[0].querySelectorAll('package');
String? latestVersion = releases[0].querySelector('version')?.innerHtml; String? latestVersion = releases[0].querySelector('version')?.innerHtml;
String? added = releases[0].querySelector('added')?.innerHtml;
DateTime? releaseDate = added != null ? DateTime.parse(added) : null;
if (latestVersion == null) { if (latestVersion == null) {
throw NoVersionError(); throw NoVersionError();
} }
@ -78,7 +80,8 @@ class FDroidRepo extends AppSource {
element.querySelector('apkname') != null) element.querySelector('apkname') != null)
.map((e) => '$standardUrl/${e.querySelector('apkname')!.innerHtml}') .map((e) => '$standardUrl/${e.querySelector('apkname')!.innerHtml}')
.toList(); .toList();
return APKDetails(latestVersion, apkUrls, AppNames(authorName, appName)); return APKDetails(latestVersion, apkUrls, AppNames(authorName, appName),
releaseDate: releaseDate);
} else { } else {
throw getObtainiumHttpError(res); throw getObtainiumHttpError(res);
} }

View File

@ -154,11 +154,15 @@ class GitHub extends AppSource {
throw NoReleasesError(); throw NoReleasesError();
} }
String? version = targetRelease['tag_name']; String? version = targetRelease['tag_name'];
DateTime? releaseDate = targetRelease['published_at'] != null
? DateTime.parse(targetRelease['published_at'])
: null;
if (version == null) { if (version == null) {
throw NoVersionError(); throw NoVersionError();
} }
return APKDetails(version, targetRelease['apkUrls'] as List<String>, return APKDetails(version, targetRelease['apkUrls'] as List<String>,
getAppNames(standardUrl)); getAppNames(standardUrl),
releaseDate: releaseDate);
} else { } else {
rateLimitErrorCheck(res); rateLimitErrorCheck(res);
throw getObtainiumHttpError(res); throw getObtainiumHttpError(res);

View File

@ -54,10 +54,14 @@ class GitLab extends AppSource {
var entryId = entry?.querySelector('id')?.innerHtml; var entryId = entry?.querySelector('id')?.innerHtml;
var version = var version =
entryId == null ? null : Uri.parse(entryId).pathSegments.last; entryId == null ? null : Uri.parse(entryId).pathSegments.last;
var releaseDateString = entry?.querySelector('updated')?.innerHtml;
DateTime? releaseDate =
releaseDateString != null ? DateTime.parse(releaseDateString) : null;
if (version == null) { if (version == null) {
throw NoVersionError(); throw NoVersionError();
} }
return APKDetails(version, apkUrls, GitHub().getAppNames(standardUrl)); return APKDetails(version, apkUrls, GitHub().getAppNames(standardUrl),
releaseDate: releaseDate);
} else { } else {
throw getObtainiumHttpError(res); 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.10.12'; const String currentVersion = '0.11.0';
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

@ -73,6 +73,8 @@ class _AddAppPageState extends State<AddAppPage> {
var userPickedTrackOnly = additionalSettings['trackOnly'] == true; var userPickedTrackOnly = additionalSettings['trackOnly'] == true;
var userPickedNoVersionDetection = var userPickedNoVersionDetection =
additionalSettings['noVersionDetection'] == true; additionalSettings['noVersionDetection'] == true;
var userPickedReleaseDateAsVersion =
additionalSettings['releaseDateAsVersion'] == true;
var cont = true; var cont = true;
if ((userPickedTrackOnly || pickedSource!.enforceTrackOnly) && if ((userPickedTrackOnly || pickedSource!.enforceTrackOnly) &&
// ignore: use_build_context_synchronously // ignore: use_build_context_synchronously
@ -93,7 +95,22 @@ class _AddAppPageState extends State<AddAppPage> {
null) { null) {
cont = false; cont = false;
} }
if (userPickedNoVersionDetection && if (userPickedReleaseDateAsVersion && // ignore: use_build_context_synchronously
// ignore: use_build_context_synchronously
await showDialog(
context: context,
builder: (BuildContext ctx) {
return GeneratedFormModal(
title: tr('useReleaseDateAsVersion'),
items: const [],
message: tr('releaseDateAsVersionExplanation'),
);
}) ==
null) {
cont = false;
}
if (!userPickedReleaseDateAsVersion &&
userPickedNoVersionDetection &&
// ignore: use_build_context_synchronously // ignore: use_build_context_synchronously
await showDialog( await showDialog(
context: context, context: context,
@ -113,12 +130,13 @@ class _AddAppPageState extends State<AddAppPage> {
App app = await sourceProvider.getApp( App app = await sourceProvider.getApp(
pickedSource!, userInput, additionalSettings, pickedSource!, userInput, additionalSettings,
trackOnlyOverride: trackOnly, trackOnlyOverride: trackOnly,
noVersionDetectionOverride: userPickedNoVersionDetection); noVersionDetectionOverride: userPickedNoVersionDetection,
releaseDateAsVersionOverride: userPickedReleaseDateAsVersion);
if (!trackOnly) { if (!trackOnly) {
await settingsProvider.getInstallPermission(); await settingsProvider.getInstallPermission();
} }
// Only download the APK here if you need to for the package ID // Only download the APK here if you need to for the package ID
if (sourceProvider.isTempId(app.id) && if (sourceProvider.isTempId(app) &&
app.additionalSettings['trackOnly'] != true) { app.additionalSettings['trackOnly'] != true) {
// ignore: use_build_context_synchronously // ignore: use_build_context_synchronously
var apkUrl = await appsProvider.confirmApkUrl(app, context); var apkUrl = await appsProvider.confirmApkUrl(app, context);

View File

@ -113,7 +113,7 @@ class _AppPageState extends State<AppPage> {
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
children: [ children: [
const SizedBox(height: 150), const SizedBox(height: 125),
app?.installedInfo != null app?.installedInfo != null
? Row(mainAxisAlignment: MainAxisAlignment.center, children: [ ? Row(mainAxisAlignment: MainAxisAlignment.center, children: [
Image.memory( Image.memory(
@ -136,6 +136,21 @@ class _AppPageState extends State<AppPage> {
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: Theme.of(context).textTheme.headlineMedium, style: Theme.of(context).textTheme.headlineMedium,
), ),
const SizedBox(
height: 8,
),
Text(
app?.app.id ?? '',
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.labelSmall,
),
app?.app.releaseDate == null
? const SizedBox.shrink()
: Text(
app!.app.releaseDate.toString(),
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.labelSmall,
),
const SizedBox( const SizedBox(
height: 32, height: 32,
), ),
@ -268,19 +283,53 @@ class _AppPageState extends State<AppPage> {
); );
}).then((values) { }).then((values) {
if (app != null && values != null) { if (app != null && values != null) {
var changedApp = app.app; Map<String, dynamic>
changedApp.additionalSettings = originalSettings =
values; app.app.additionalSettings;
app.app.additionalSettings = values;
if (source.enforceTrackOnly) { if (source.enforceTrackOnly) {
changedApp.additionalSettings[ app.app.additionalSettings[
'trackOnly'] = true; 'trackOnly'] = true;
showError( showError(
tr('appsFromSourceAreTrackOnly'), tr('appsFromSourceAreTrackOnly'),
context); context);
} }
appsProvider.saveApps( if (app.app.additionalSettings[
[changedApp]).then((value) { 'releaseDateAsVersion'] ==
getUpdate(changedApp.id); true) {
app.app.additionalSettings[
'noVersionDetection'] = true;
if (originalSettings[
'releaseDateAsVersion'] !=
true) {
if (app.app.releaseDate != null) {
bool isUpdated =
app.app.installedVersion ==
app.app.latestVersion;
app.app.latestVersion = app
.app
.releaseDate!
.microsecondsSinceEpoch
.toString();
if (isUpdated) {
app.app.installedVersion =
app.app.latestVersion;
}
}
}
} else if (originalSettings[
'releaseDateAsVersion'] ==
true) {
app.app.additionalSettings[
'noVersionDetection'] = false;
app.app.installedVersion = app
.installedInfo
?.versionName ??
app.app.installedVersion;
}
appsProvider.saveApps([app.app]).then(
(value) {
getUpdate(app.app.id);
}); });
} }
}); });

View File

@ -54,12 +54,12 @@ class AppsPageState extends State<AppsPage> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
var appsProvider = context.watch<AppsProvider>(); var appsProvider = context.watch<AppsProvider>();
var settingsProvider = context.watch<SettingsProvider>(); var settingsProvider = context.watch<SettingsProvider>();
var sortedApps = appsProvider.apps.values.toList(); var listedApps = appsProvider.apps.values.toList();
var currentFilterIsUpdatesOnly = var currentFilterIsUpdatesOnly =
filter.isIdenticalTo(updatesOnlyFilter, settingsProvider); filter.isIdenticalTo(updatesOnlyFilter, settingsProvider);
selectedApps = selectedApps selectedApps = selectedApps
.where((element) => sortedApps.map((e) => e.app).contains(element)) .where((element) => listedApps.map((e) => e.app).contains(element))
.toSet(); .toSet();
toggleAppSelected(App app) { toggleAppSelected(App app) {
@ -72,7 +72,7 @@ class AppsPageState extends State<AppsPage> {
}); });
} }
sortedApps = sortedApps.where((app) { listedApps = listedApps.where((app) {
if (app.app.installedVersion == app.app.latestVersion && if (app.app.installedVersion == app.app.latestVersion &&
!(filter.includeUptodate)) { !(filter.includeUptodate)) {
return false; return false;
@ -111,7 +111,7 @@ class AppsPageState extends State<AppsPage> {
return true; return true;
}).toList(); }).toList();
sortedApps.sort((a, b) { listedApps.sort((a, b) {
var nameA = a.installedInfo?.name ?? a.app.name; var nameA = a.installedInfo?.name ?? a.app.name;
var nameB = b.installedInfo?.name ?? b.app.name; var nameB = b.installedInfo?.name ?? b.app.name;
int result = 0; int result = 0;
@ -119,25 +119,30 @@ class AppsPageState extends State<AppsPage> {
result = (a.app.author + nameA).compareTo(b.app.author + nameB); result = (a.app.author + nameA).compareTo(b.app.author + nameB);
} else if (settingsProvider.sortColumn == SortColumnSettings.nameAuthor) { } else if (settingsProvider.sortColumn == SortColumnSettings.nameAuthor) {
result = (nameA + a.app.author).compareTo(nameB + b.app.author); result = (nameA + a.app.author).compareTo(nameB + b.app.author);
} else if (settingsProvider.sortColumn ==
SortColumnSettings.releaseDate) {
result = (a.app.releaseDate)?.compareTo(
b.app.releaseDate ?? DateTime.fromMicrosecondsSinceEpoch(0)) ??
0;
} }
return result; return result;
}); });
if (settingsProvider.sortOrder == SortOrderSettings.descending) { if (settingsProvider.sortOrder == SortOrderSettings.descending) {
sortedApps = sortedApps.reversed.toList(); listedApps = listedApps.reversed.toList();
} }
var existingUpdates = appsProvider.findExistingUpdates(installedOnly: true); var existingUpdates = appsProvider.findExistingUpdates(installedOnly: true);
var existingUpdateIdsAllOrSelected = existingUpdates var existingUpdateIdsAllOrSelected = existingUpdates
.where((element) => selectedApps.isEmpty .where((element) => selectedApps.isEmpty
? sortedApps.where((a) => a.app.id == element).isNotEmpty ? listedApps.where((a) => a.app.id == element).isNotEmpty
: selectedApps.map((e) => e.id).contains(element)) : selectedApps.map((e) => e.id).contains(element))
.toList(); .toList();
var newInstallIdsAllOrSelected = appsProvider var newInstallIdsAllOrSelected = appsProvider
.findExistingUpdates(nonInstalledOnly: true) .findExistingUpdates(nonInstalledOnly: true)
.where((element) => selectedApps.isEmpty .where((element) => selectedApps.isEmpty
? sortedApps.where((a) => a.app.id == element).isNotEmpty ? listedApps.where((a) => a.app.id == element).isNotEmpty
: selectedApps.map((e) => e.id).contains(element)) : selectedApps.map((e) => e.id).contains(element))
.toList(); .toList();
@ -159,26 +164,26 @@ class AppsPageState extends State<AppsPage> {
if (settingsProvider.pinUpdates) { if (settingsProvider.pinUpdates) {
var temp = []; var temp = [];
sortedApps = sortedApps.where((sa) { listedApps = listedApps.where((sa) {
if (existingUpdates.contains(sa.app.id)) { if (existingUpdates.contains(sa.app.id)) {
temp.add(sa); temp.add(sa);
return false; return false;
} }
return true; return true;
}).toList(); }).toList();
sortedApps = [...temp, ...sortedApps]; listedApps = [...temp, ...listedApps];
} }
var tempPinned = []; var tempPinned = [];
var tempNotPinned = []; var tempNotPinned = [];
for (var a in sortedApps) { for (var a in listedApps) {
if (a.app.pinned) { if (a.app.pinned) {
tempPinned.add(a); tempPinned.add(a);
} else { } else {
tempNotPinned.add(a); tempNotPinned.add(a);
} }
} }
sortedApps = [...tempPinned, ...tempNotPinned]; listedApps = [...tempPinned, ...tempNotPinned];
return Scaffold( return Scaffold(
backgroundColor: Theme.of(context).colorScheme.surface, backgroundColor: Theme.of(context).colorScheme.surface,
@ -198,7 +203,7 @@ class AppsPageState extends State<AppsPage> {
}, },
child: CustomScrollView(slivers: <Widget>[ child: CustomScrollView(slivers: <Widget>[
CustomAppBar(title: tr('appsString')), CustomAppBar(title: tr('appsString')),
if (appsProvider.loadingApps || sortedApps.isEmpty) if (appsProvider.loadingApps || listedApps.isEmpty)
SliverFillRemaining( SliverFillRemaining(
child: Center( child: Center(
child: appsProvider.loadingApps child: appsProvider.loadingApps
@ -225,8 +230,8 @@ class AppsPageState extends State<AppsPage> {
delegate: SliverChildBuilderDelegate( delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) { (BuildContext context, int index) {
String? changesUrl = SourceProvider() String? changesUrl = SourceProvider()
.getSource(sortedApps[index].app.url) .getSource(listedApps[index].app.url)
.changeLogPageFromStandardUrl(sortedApps[index].app.url); .changeLogPageFromStandardUrl(listedApps[index].app.url);
var transparent = const Color.fromARGB(0, 0, 0, 0).value; var transparent = const Color.fromARGB(0, 0, 0, 0).value;
return Container( return Container(
decoration: BoxDecoration( decoration: BoxDecoration(
@ -234,52 +239,53 @@ class AppsPageState extends State<AppsPage> {
vertical: BorderSide( vertical: BorderSide(
width: 4, width: 4,
color: Color( color: Color(
sortedApps[index].app.categories.isNotEmpty listedApps[index].app.categories.isNotEmpty
? settingsProvider.categories[ ? settingsProvider.categories[
sortedApps[index] listedApps[index]
.app .app
.categories .categories
.first] ?? .first] ??
transparent transparent
: transparent)))), : transparent)))),
child: ListTile( child: ListTile(
tileColor: sortedApps[index].app.pinned tileColor: listedApps[index].app.pinned
? Colors.grey.withOpacity(0.1) ? Colors.grey.withOpacity(0.1)
: Colors.transparent, : Colors.transparent,
selectedTileColor: Theme.of(context) selectedTileColor: Theme.of(context)
.colorScheme .colorScheme
.primary .primary
.withOpacity(sortedApps[index].app.pinned ? 0.2 : 0.1), .withOpacity(listedApps[index].app.pinned ? 0.2 : 0.1),
selected: selectedApps.contains(sortedApps[index].app), selected: selectedApps.contains(listedApps[index].app),
onLongPress: () { onLongPress: () {
toggleAppSelected(sortedApps[index].app); toggleAppSelected(listedApps[index].app);
}, },
leading: sortedApps[index].installedInfo != null leading: listedApps[index].installedInfo != null
? Image.memory( ? Image.memory(
sortedApps[index].installedInfo!.icon!, listedApps[index].installedInfo!.icon!,
gaplessPlayback: true, gaplessPlayback: true,
) )
: null, : null,
title: Text( title: Text(
sortedApps[index].installedInfo?.name ?? listedApps[index].installedInfo?.name ??
sortedApps[index].app.name, listedApps[index].app.name,
style: TextStyle( style: TextStyle(
fontWeight: sortedApps[index].app.pinned overflow: TextOverflow.ellipsis,
fontWeight: listedApps[index].app.pinned
? FontWeight.bold ? FontWeight.bold
: FontWeight.normal, : FontWeight.normal,
), ),
), ),
subtitle: Text( subtitle: Text(
tr('byX', args: [sortedApps[index].app.author]), tr('byX', args: [listedApps[index].app.author]),
style: TextStyle( style: TextStyle(
fontWeight: sortedApps[index].app.pinned fontWeight: listedApps[index].app.pinned
? FontWeight.bold ? FontWeight.bold
: FontWeight.normal)), : FontWeight.normal)),
trailing: SingleChildScrollView( trailing: SingleChildScrollView(
reverse: true, reverse: true,
child: sortedApps[index].downloadProgress != null child: listedApps[index].downloadProgress != null
? Text(tr('percentProgress', args: [ ? Text(tr('percentProgress', args: [
sortedApps[index] listedApps[index]
.downloadProgress .downloadProgress
?.toInt() ?.toInt()
.toString() ?? .toString() ??
@ -289,60 +295,101 @@ class AppsPageState extends State<AppsPage> {
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.end, crossAxisAlignment: CrossAxisAlignment.end,
children: [ children: [
SizedBox( Row(
width: 100, mainAxisSize: MainAxisSize.min,
children: [
Text(
'${listedApps[index].app.installedVersion ?? tr('notInstalled')}${listedApps[index].app.additionalSettings['trackOnly'] == true ? ' ${tr('estimateInBrackets')}' : ''}',
overflow: TextOverflow.ellipsis,
textAlign: TextAlign.end,
)
]),
GestureDetector(
onTap: changesUrl == null
? null
: () {
launchUrlString(changesUrl,
mode: LaunchMode
.externalApplication);
},
child: Text( child: Text(
'${sortedApps[index].app.installedVersion ?? tr('notInstalled')}${sortedApps[index].app.additionalSettings['trackOnly'] == true ? ' ${tr('estimateInBrackets')}' : ''}', listedApps[index].app.releaseDate ==
overflow: TextOverflow.fade, null
textAlign: TextAlign.end, ? tr('changes')
: DateFormat('yyyy-MM-dd').format(
listedApps[index]
.app
.releaseDate!),
style: const TextStyle(
fontStyle: FontStyle.italic,
decoration:
TextDecoration.underline),
)), )),
sortedApps[index].app.installedVersion != listedApps[index].app.installedVersion !=
null && null &&
sortedApps[index] listedApps[index]
.app .app
.installedVersion != .installedVersion !=
sortedApps[index] listedApps[index]
.app .app
.latestVersion .latestVersion
? GestureDetector( ? appsProvider.areDownloadsRunning()
onTap: changesUrl == null ? Text(tr('pleaseWait'))
? null : Row(
: () { mainAxisSize: MainAxisSize.min,
launchUrlString(changesUrl, mainAxisAlignment:
mode: LaunchMode MainAxisAlignment.end,
.externalApplication); children: [
}, GestureDetector(
child: appsProvider onTap: () {
.areDownloadsRunning() appsProvider
? Text(tr('pleaseWait')) .downloadAndInstallLatestApps(
: Text( [
'${tr('updateAvailable')}${sortedApps[index].app.additionalSettings['trackOnly'] == true ? ' ${tr('estimateInBracketsShort')}' : ''}', listedApps[index]
style: TextStyle( .app
fontStyle: .id
FontStyle.italic, ],
decoration: changesUrl == globalNavigatorKey
null .currentContext).catchError(
? TextDecoration.none (e) {
: TextDecoration showError(e, context);
.underline), });
)) },
: const SizedBox(), child: Text(
listedApps[index]
.app
.additionalSettings[
'trackOnly'] ==
true
? tr('markUpdated')
: tr('update'),
style: TextStyle(
color:
Theme.of(context)
.colorScheme
.primary,
fontWeight:
FontWeight.bold),
)),
],
)
: const SizedBox.shrink(),
], ],
))), ))),
onTap: () { onTap: () {
if (selectedApps.isNotEmpty) { if (selectedApps.isNotEmpty) {
toggleAppSelected(sortedApps[index].app); toggleAppSelected(listedApps[index].app);
} else { } else {
Navigator.push( Navigator.push(
context, context,
MaterialPageRoute( MaterialPageRoute(
builder: (context) => builder: (context) =>
AppPage(appId: sortedApps[index].app.id)), AppPage(appId: listedApps[index].app.id)),
); );
} }
}, },
)); ));
}, childCount: sortedApps.length)) }, childCount: listedApps.length))
])), ])),
persistentFooterButtons: appsProvider.apps.isEmpty persistentFooterButtons: appsProvider.apps.isEmpty
? null ? null
@ -354,20 +401,20 @@ class AppsPageState extends State<AppsPage> {
style: const ButtonStyle( style: const ButtonStyle(
visualDensity: VisualDensity.compact), visualDensity: VisualDensity.compact),
onPressed: () { onPressed: () {
selectThese(sortedApps.map((e) => e.app).toList()); selectThese(listedApps.map((e) => e.app).toList());
}, },
icon: Icon( icon: Icon(
Icons.select_all_outlined, Icons.select_all_outlined,
color: Theme.of(context).colorScheme.primary, color: Theme.of(context).colorScheme.primary,
), ),
label: Text(sortedApps.length.toString())) label: Text(listedApps.length.toString()))
: TextButton.icon( : TextButton.icon(
style: const ButtonStyle( style: const ButtonStyle(
visualDensity: VisualDensity.compact), visualDensity: VisualDensity.compact),
onPressed: () { onPressed: () {
selectedApps.isEmpty selectedApps.isEmpty
? selectThese( ? selectThese(
sortedApps.map((e) => e.app).toList()) listedApps.map((e) => e.app).toList())
: clearSelected(); : clearSelected();
}, },
icon: Icon( icon: Icon(

View File

@ -101,6 +101,10 @@ class _SettingsPageState extends State<SettingsPage> {
DropdownMenuItem( DropdownMenuItem(
value: SortColumnSettings.added, value: SortColumnSettings.added,
child: Text(tr('asAdded')), child: Text(tr('asAdded')),
),
DropdownMenuItem(
value: SortColumnSettings.releaseDate,
child: Text(tr('releaseDate')),
) )
], ],
onChanged: (value) { onChanged: (value) {

View File

@ -182,7 +182,7 @@ class AppsProvider with ChangeNotifier {
// The former case should be handled (give the App its real ID), the latter is a security issue // The former case should be handled (give the App its real ID), the latter is a security issue
var newInfo = await PackageArchiveInfo.fromPath(downloadedFile.path); var newInfo = await PackageArchiveInfo.fromPath(downloadedFile.path);
if (app.id != newInfo.packageName) { if (app.id != newInfo.packageName) {
if (apps[app.id] != null && !SourceProvider().isTempId(app.id)) { if (apps[app.id] != null && !SourceProvider().isTempId(app)) {
throw IDChangedError(); throw IDChangedError();
} }
var originalAppId = app.id; var originalAppId = app.id;

View File

@ -6,7 +6,6 @@ import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:fluttertoast/fluttertoast.dart'; import 'package:fluttertoast/fluttertoast.dart';
import 'package:obtainium/app_sources/github.dart'; import 'package:obtainium/app_sources/github.dart';
import 'package:obtainium/components/generated_form.dart';
import 'package:obtainium/main.dart'; import 'package:obtainium/main.dart';
import 'package:permission_handler/permission_handler.dart'; import 'package:permission_handler/permission_handler.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
@ -18,7 +17,7 @@ enum ThemeSettings { system, light, dark }
enum ColourSettings { basic, materialYou } enum ColourSettings { basic, materialYou }
enum SortColumnSettings { added, nameAuthor, authorName } enum SortColumnSettings { added, nameAuthor, authorName, releaseDate }
enum SortOrderSettings { ascending, descending } enum SortOrderSettings { ascending, descending }

View File

@ -33,8 +33,9 @@ class APKDetails {
late String version; late String version;
late List<String> apkUrls; late List<String> apkUrls;
late AppNames names; late AppNames names;
late DateTime? releaseDate;
APKDetails(this.version, this.apkUrls, this.names); APKDetails(this.version, this.apkUrls, this.names, {this.releaseDate});
} }
class App { class App {
@ -50,6 +51,7 @@ class App {
late DateTime? lastUpdateCheck; late DateTime? lastUpdateCheck;
bool pinned = false; bool pinned = false;
List<String> categories; List<String> categories;
late DateTime? releaseDate;
App( App(
this.id, this.id,
this.url, this.url,
@ -62,7 +64,8 @@ class App {
this.additionalSettings, this.additionalSettings,
this.lastUpdateCheck, this.lastUpdateCheck,
this.pinned, this.pinned,
{this.categories = const []}); {this.categories = const [],
this.releaseDate});
@override @override
String toString() { String toString() {
@ -111,30 +114,34 @@ class App {
preferredApkIndex = 0; preferredApkIndex = 0;
} }
return App( return App(
json['id'] as String, json['id'] as String,
json['url'] as String, json['url'] as String,
json['author'] as String, json['author'] as String,
json['name'] as String, json['name'] as String,
json['installedVersion'] == null json['installedVersion'] == null
? null ? null
: json['installedVersion'] as String, : json['installedVersion'] as String,
json['latestVersion'] as String, json['latestVersion'] as String,
json['apkUrls'] == null json['apkUrls'] == null
? [] ? []
: List<String>.from(jsonDecode(json['apkUrls'])), : List<String>.from(jsonDecode(json['apkUrls'])),
preferredApkIndex, preferredApkIndex,
additionalSettings, additionalSettings,
json['lastUpdateCheck'] == null json['lastUpdateCheck'] == null
? null ? null
: DateTime.fromMicrosecondsSinceEpoch(json['lastUpdateCheck']), : DateTime.fromMicrosecondsSinceEpoch(json['lastUpdateCheck']),
json['pinned'] ?? false, json['pinned'] ?? false,
categories: json['categories'] != null categories: json['categories'] != null
? (json['categories'] as List<dynamic>) ? (json['categories'] as List<dynamic>)
.map((e) => e.toString()) .map((e) => e.toString())
.toList() .toList()
: json['category'] != null : json['category'] != null
? [json['category'] as String] ? [json['category'] as String]
: []); : [],
releaseDate: json['releaseDate'] == null
? null
: DateTime.fromMicrosecondsSinceEpoch(json['releaseDate']),
);
} }
Map<String, dynamic> toJson() => { Map<String, dynamic> toJson() => {
@ -149,7 +156,8 @@ class App {
'additionalSettings': jsonEncode(additionalSettings), 'additionalSettings': jsonEncode(additionalSettings),
'lastUpdateCheck': lastUpdateCheck?.microsecondsSinceEpoch, 'lastUpdateCheck': lastUpdateCheck?.microsecondsSinceEpoch,
'pinned': pinned, 'pinned': pinned,
'categories': categories 'categories': categories,
'releaseDate': releaseDate?.microsecondsSinceEpoch
}; };
} }
@ -225,6 +233,10 @@ class AppSource {
label: tr('trackOnly'), label: tr('trackOnly'),
) )
], ],
[
GeneratedFormSwitch('releaseDateAsVersion',
label: tr('useReleaseDateAsVersion'))
],
[ [
GeneratedFormSwitch('noVersionDetection', label: tr('noVersionDetection')) GeneratedFormSwitch('noVersionDetection', label: tr('noVersionDetection'))
], ],
@ -350,34 +362,28 @@ class SourceProvider {
return false; return false;
} }
String generateTempID(AppNames names, AppSource source) => String generateTempID(
'${names.author.toLowerCase()}_${names.name.toLowerCase()}_${source.host}'; String standardUrl, Map<String, dynamic> additionalSettings) =>
(standardUrl + additionalSettings.toString()).hashCode.toString();
bool isTempId(String id) { bool isTempId(App app) {
List<String> parts = id.split('_'); return app.id == generateTempID(app.url, app.additionalSettings);
if (parts.length < 3) {
return false;
}
for (int i = 0; i < parts.length - 1; i++) {
if (RegExp('.*[A-Z].*').hasMatch(parts[i])) {
// TODO: Look into RegEx for non-Latin characters
return false;
}
}
return true;
} }
Future<App> getApp( Future<App> getApp(
AppSource source, AppSource source, String url, Map<String, dynamic> additionalSettings,
String url, {App? currentApp,
Map<String, dynamic> additionalSettings, { bool trackOnlyOverride = false,
App? currentApp, bool noVersionDetectionOverride = false,
bool trackOnlyOverride = false, bool releaseDateAsVersionOverride = false}) async {
noVersionDetectionOverride = false,
}) async {
if (trackOnlyOverride || source.enforceTrackOnly) { if (trackOnlyOverride || source.enforceTrackOnly) {
additionalSettings['trackOnly'] = true; additionalSettings['trackOnly'] = true;
} }
if (releaseDateAsVersionOverride) {
additionalSettings['releaseDateAsVersion'] = true;
noVersionDetectionOverride =
true; // Rel. date as version means no ver. det.
}
if (noVersionDetectionOverride) { if (noVersionDetectionOverride) {
additionalSettings['noVersionDetection'] = true; additionalSettings['noVersionDetection'] = true;
} }
@ -385,6 +391,10 @@ class SourceProvider {
String standardUrl = source.standardizeURL(preStandardizeUrl(url)); String standardUrl = source.standardizeURL(preStandardizeUrl(url));
APKDetails apk = APKDetails apk =
await source.getLatestAPKDetails(standardUrl, additionalSettings); await source.getLatestAPKDetails(standardUrl, additionalSettings);
if (additionalSettings['releaseDateAsVersion'] == true &&
apk.releaseDate != null) {
apk.version = apk.releaseDate!.microsecondsSinceEpoch.toString();
}
if (additionalSettings['apkFilterRegEx'] != null) { if (additionalSettings['apkFilterRegEx'] != null) {
var reg = RegExp(additionalSettings['apkFilterRegEx']); var reg = RegExp(additionalSettings['apkFilterRegEx']);
apk.apkUrls = apk.apkUrls =
@ -400,7 +410,7 @@ class SourceProvider {
currentApp?.id ?? currentApp?.id ??
source.tryInferringAppId(standardUrl, source.tryInferringAppId(standardUrl,
additionalSettings: additionalSettings) ?? additionalSettings: additionalSettings) ??
generateTempID(apk.names, source), generateTempID(standardUrl, additionalSettings),
standardUrl, standardUrl,
apk.names.author[0].toUpperCase() + apk.names.author.substring(1), apk.names.author[0].toUpperCase() + apk.names.author.substring(1),
name.trim().isNotEmpty name.trim().isNotEmpty
@ -413,7 +423,8 @@ class SourceProvider {
additionalSettings, additionalSettings,
DateTime.now(), DateTime.now(),
currentApp?.pinned ?? false, currentApp?.pinned ?? false,
categories: currentApp?.categories ?? const []); categories: currentApp?.categories ?? const [],
releaseDate: apk.releaseDate);
} }
// Returns errors in [results, errors] instead of throwing them // Returns errors in [results, errors] instead of throwing them

View File

@ -258,10 +258,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: flutter_local_notifications_linux name: flutter_local_notifications_linux
sha256: "8f6c1611e0c4a88a382691a97bb3c3feb24cc0c0b54152b8b5fb7ffb837f7fbf" sha256: ccb08b93703aeedb58856e5637450bf3ffec899adb66dc325630b68994734b89
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.0.0" version: "3.0.0+1"
flutter_local_notifications_platform_interface: flutter_local_notifications_platform_interface:
dependency: transitive dependency: transitive
description: description:

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.10.12+118 # When changing this, update the tag in main() accordingly version: 0.11.0+119 # 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'