Files
Obtainium/lib/pages/apps.dart

1139 lines
42 KiB
Dart

import 'dart:convert';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_markdown/flutter_markdown.dart';
import 'package:obtainium/components/custom_app_bar.dart';
import 'package:obtainium/components/generated_form.dart';
import 'package:obtainium/components/generated_form_modal.dart';
import 'package:obtainium/custom_errors.dart';
import 'package:obtainium/main.dart';
import 'package:obtainium/pages/app.dart';
import 'package:obtainium/pages/settings.dart';
import 'package:obtainium/providers/apps_provider.dart';
import 'package:obtainium/providers/settings_provider.dart';
import 'package:obtainium/providers/source_provider.dart';
import 'package:provider/provider.dart';
import 'package:share_plus/share_plus.dart';
import 'package:url_launcher/url_launcher_string.dart';
import 'package:markdown/markdown.dart' as md;
class AppsPage extends StatefulWidget {
const AppsPage({super.key});
@override
State<AppsPage> createState() => AppsPageState();
}
class AppsPageState extends State<AppsPage> {
AppsFilter filter = AppsFilter();
final AppsFilter neutralFilter = AppsFilter();
var updatesOnlyFilter =
AppsFilter(includeUptodate: false, includeNonInstalled: false);
Set<String> selectedAppIds = {};
DateTime? refreshingSince;
clearSelected() {
if (selectedAppIds.isNotEmpty) {
setState(() {
selectedAppIds.clear();
});
return true;
}
return false;
}
selectThese(List<App> apps) {
if (selectedAppIds.isEmpty) {
setState(() {
for (var a in apps) {
selectedAppIds.add(a.id);
}
});
}
}
final GlobalKey<RefreshIndicatorState> _refreshIndicatorKey =
GlobalKey<RefreshIndicatorState>();
@override
Widget build(BuildContext context) {
var appsProvider = context.watch<AppsProvider>();
var settingsProvider = context.watch<SettingsProvider>();
var sourceProvider = SourceProvider();
var listedApps = appsProvider.getAppValues().toList();
refresh() {
HapticFeedback.lightImpact();
setState(() {
refreshingSince = DateTime.now();
});
return appsProvider.checkUpdates().catchError((e) {
showError(e is Map ? e['errors'] : e, context);
return <App>[];
}).whenComplete(() {
setState(() {
refreshingSince = null;
});
});
}
if (!appsProvider.loadingApps &&
appsProvider.apps.isNotEmpty &&
settingsProvider.checkJustStarted() &&
settingsProvider.checkOnStart) {
_refreshIndicatorKey.currentState?.show();
}
selectedAppIds = selectedAppIds
.where((element) => listedApps.map((e) => e.app.id).contains(element))
.toSet();
toggleAppSelected(App app) {
setState(() {
if (selectedAppIds.map((e) => e).contains(app.id)) {
selectedAppIds.removeWhere((a) => a == app.id);
} else {
selectedAppIds.add(app.id);
}
});
}
listedApps = listedApps.where((app) {
if (app.app.installedVersion == app.app.latestVersion &&
!(filter.includeUptodate)) {
return false;
}
if (app.app.installedVersion == null && !(filter.includeNonInstalled)) {
return false;
}
if (filter.nameFilter.isNotEmpty || filter.authorFilter.isNotEmpty) {
List<String> nameTokens = filter.nameFilter
.split(' ')
.where((element) => element.trim().isNotEmpty)
.toList();
List<String> authorTokens = filter.authorFilter
.split(' ')
.where((element) => element.trim().isNotEmpty)
.toList();
for (var t in nameTokens) {
if (!app.name.toLowerCase().contains(t.toLowerCase())) {
return false;
}
}
for (var t in authorTokens) {
if (!app.app.author.toLowerCase().contains(t.toLowerCase())) {
return false;
}
}
}
if (filter.idFilter.isNotEmpty) {
if (!app.app.id.contains(filter.idFilter)) {
return false;
}
}
if (filter.categoryFilter.isNotEmpty &&
filter.categoryFilter
.intersection(app.app.categories.toSet())
.isEmpty) {
return false;
}
if (filter.sourceFilter.isNotEmpty &&
sourceProvider
.getSource(app.app.url,
overrideSource: app.app.overrideSource)
.runtimeType
.toString() !=
filter.sourceFilter) {
return false;
}
return true;
}).toList();
listedApps.sort((a, b) {
int result = 0;
if (settingsProvider.sortColumn == SortColumnSettings.authorName) {
result = ((a.app.author + a.name).toLowerCase())
.compareTo((b.app.author + b.name).toLowerCase());
} else if (settingsProvider.sortColumn == SortColumnSettings.nameAuthor) {
result = ((a.name + a.app.author).toLowerCase())
.compareTo((b.name + b.app.author).toLowerCase());
} else if (settingsProvider.sortColumn ==
SortColumnSettings.releaseDate) {
result = (a.app.releaseDate)?.compareTo(
b.app.releaseDate ?? DateTime.fromMicrosecondsSinceEpoch(0)) ??
0;
}
return result;
});
if (settingsProvider.sortOrder == SortOrderSettings.descending) {
listedApps = listedApps.reversed.toList();
}
var existingUpdates = appsProvider.findExistingUpdates(installedOnly: true);
var existingUpdateIdsAllOrSelected = existingUpdates
.where((element) => selectedAppIds.isEmpty
? listedApps.where((a) => a.app.id == element).isNotEmpty
: selectedAppIds.map((e) => e).contains(element))
.toList();
var newInstallIdsAllOrSelected = appsProvider
.findExistingUpdates(nonInstalledOnly: true)
.where((element) => selectedAppIds.isEmpty
? listedApps.where((a) => a.app.id == element).isNotEmpty
: selectedAppIds.map((e) => e).contains(element))
.toList();
List<String> trackOnlyUpdateIdsAllOrSelected = [];
existingUpdateIdsAllOrSelected = existingUpdateIdsAllOrSelected.where((id) {
if (appsProvider.apps[id]!.app.additionalSettings['trackOnly'] == true) {
trackOnlyUpdateIdsAllOrSelected.add(id);
return false;
}
return true;
}).toList();
newInstallIdsAllOrSelected = newInstallIdsAllOrSelected.where((id) {
if (appsProvider.apps[id]!.app.additionalSettings['trackOnly'] == true) {
trackOnlyUpdateIdsAllOrSelected.add(id);
return false;
}
return true;
}).toList();
if (settingsProvider.pinUpdates) {
var temp = [];
listedApps = listedApps.where((sa) {
if (existingUpdates.contains(sa.app.id)) {
temp.add(sa);
return false;
}
return true;
}).toList();
listedApps = [...temp, ...listedApps];
}
if (settingsProvider.buryNonInstalled) {
var temp = [];
listedApps = listedApps.where((sa) {
if (sa.app.installedVersion == null) {
temp.add(sa);
return false;
}
return true;
}).toList();
listedApps = [...listedApps, ...temp];
}
var tempPinned = [];
var tempNotPinned = [];
for (var a in listedApps) {
if (a.app.pinned) {
tempPinned.add(a);
} else {
tempNotPinned.add(a);
}
}
listedApps = [...tempPinned, ...tempNotPinned];
List<String?> getListedCategories() {
var temp = listedApps
.map((e) => e.app.categories.isNotEmpty ? e.app.categories : [null]);
return temp.isNotEmpty
? {
...temp.reduce((v, e) => [...v, ...e])
}.toList()
: [];
}
var listedCategories = getListedCategories();
listedCategories.sort((a, b) {
return a != null && b != null
? a.toLowerCase().compareTo(b.toLowerCase())
: a == null
? 1
: -1;
});
Set<App> selectedApps = listedApps
.map((e) => e.app)
.where((a) => selectedAppIds.contains(a.id))
.toSet();
showChangeLogDialog(
String? changesUrl, AppSource appSource, String changeLog, int index) {
showDialog(
context: context,
builder: (BuildContext context) {
return GeneratedFormModal(
title: tr('changes'),
items: const [],
message: listedApps[index].app.latestVersion,
additionalWidgets: [
changesUrl != null
? GestureDetector(
child: Text(
changesUrl,
style: const TextStyle(
decoration: TextDecoration.underline,
fontStyle: FontStyle.italic),
),
onTap: () {
launchUrlString(changesUrl,
mode: LaunchMode.externalApplication);
},
)
: const SizedBox.shrink(),
changesUrl != null
? const SizedBox(
height: 16,
)
: const SizedBox.shrink(),
appSource.changeLogIfAnyIsMarkDown
? SizedBox(
width: MediaQuery.of(context).size.width,
height: MediaQuery.of(context).size.height - 350,
child: Markdown(
data: changeLog,
onTapLink: (text, href, title) {
if (href != null) {
launchUrlString(
href.startsWith('http://') ||
href.startsWith('https://')
? href
: '${Uri.parse(listedApps[index].app.url).origin}/$href',
mode: LaunchMode.externalApplication);
}
},
extensionSet: md.ExtensionSet(
md.ExtensionSet.gitHubFlavored.blockSyntaxes,
[
md.EmojiSyntax(),
...md.ExtensionSet.gitHubFlavored.inlineSyntaxes
],
),
))
: Text(changeLog),
],
singleNullReturnButton: tr('ok'),
);
});
}
getLoadingWidgets() {
return [
if (listedApps.isEmpty)
SliverFillRemaining(
child: Center(
child: Text(
appsProvider.apps.isEmpty ? tr('noApps') : tr('noAppsForFilter'),
style: Theme.of(context).textTheme.headlineMedium,
textAlign: TextAlign.center,
))),
if (refreshingSince != null || appsProvider.loadingApps)
SliverToBoxAdapter(
child: LinearProgressIndicator(
value: appsProvider.loadingApps
? null
: appsProvider
.getAppValues()
.where((element) => !(element.app.lastUpdateCheck
?.isBefore(refreshingSince!) ??
true))
.length /
(appsProvider.apps.isNotEmpty
? appsProvider.apps.length
: 1),
),
)
];
}
getChangeLogFn(int appIndex) {
AppSource appSource = SourceProvider().getSource(
listedApps[appIndex].app.url,
overrideSource: listedApps[appIndex].app.overrideSource);
String? changesUrl =
appSource.changeLogPageFromStandardUrl(listedApps[appIndex].app.url);
String? changeLog = listedApps[appIndex].app.changeLog;
return (changeLog == null && changesUrl == null)
? null
: () {
if (changeLog != null) {
showChangeLogDialog(changesUrl, appSource, changeLog, appIndex);
} else {
launchUrlString(changesUrl!,
mode: LaunchMode.externalApplication);
}
};
}
getUpdateButton(int appIndex) {
return IconButton(
visualDensity: VisualDensity.compact,
color: Theme.of(context).colorScheme.primary,
tooltip:
listedApps[appIndex].app.additionalSettings['trackOnly'] == true
? tr('markUpdated')
: tr('update'),
onPressed: appsProvider.areDownloadsRunning()
? null
: () {
appsProvider.downloadAndInstallLatestApps(
[listedApps[appIndex].app.id],
globalNavigatorKey.currentContext).catchError((e) {
showError(e, context);
return <String>[];
});
},
icon: Icon(
listedApps[appIndex].app.additionalSettings['trackOnly'] == true
? Icons.check_circle_outline
: Icons.install_mobile));
}
getAppIcon(int appIndex) {
return listedApps[appIndex].icon != null
? Image.memory(
listedApps[appIndex].icon!,
gaplessPlayback: true,
)
: Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Transform(
alignment: Alignment.center,
transform: Matrix4.rotationZ(0.31),
child: Padding(
padding: const EdgeInsets.all(15),
child: Image(
image: const AssetImage(
'assets/graphics/icon_small.png'),
color: Colors.white.withOpacity(0.3),
colorBlendMode: BlendMode.modulate,
gaplessPlayback: true,
),
)),
]);
}
getVersionText(int appIndex) {
return '${listedApps[appIndex].app.installedVersion ?? tr('notInstalled')}${listedApps[appIndex].app.additionalSettings['trackOnly'] == true ? ' ${tr('estimateInBrackets')}' : ''}';
}
getChangesButtonString(int appIndex, bool hasChangeLogFn) {
return listedApps[appIndex].app.releaseDate == null
? hasChangeLogFn
? tr('changes')
: ''
: DateFormat('yyyy-MM-dd')
.format(listedApps[appIndex].app.releaseDate!);
}
getSingleAppHorizTile(int index) {
var showChangesFn = getChangeLogFn(index);
var hasUpdate = listedApps[index].app.installedVersion != null &&
listedApps[index].app.installedVersion !=
listedApps[index].app.latestVersion;
Widget trailingRow = Row(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
hasUpdate ? getUpdateButton(index) : const SizedBox.shrink(),
hasUpdate
? const SizedBox(
width: 10,
)
: const SizedBox.shrink(),
GestureDetector(
onTap: showChangesFn,
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
color: settingsProvider.highlightTouchTargets &&
showChangesFn != null
? (Theme.of(context).brightness == Brightness.light
? Theme.of(context).primaryColor
: Theme.of(context).primaryColorLight)
.withAlpha(20)
: null),
padding: settingsProvider.highlightTouchTargets
? const EdgeInsetsDirectional.fromSTEB(12, 0, 12, 0)
: const EdgeInsetsDirectional.fromSTEB(24, 0, 0, 0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Row(mainAxisSize: MainAxisSize.min, children: [
Container(
constraints: BoxConstraints(
maxWidth:
MediaQuery.of(context).size.width / 4),
child: Text(getVersionText(index),
overflow: TextOverflow.ellipsis,
textAlign: TextAlign.end)),
]),
Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(
getChangesButtonString(
index, showChangesFn != null),
style: TextStyle(
fontStyle: FontStyle.italic,
decoration: showChangesFn != null
? TextDecoration.underline
: TextDecoration.none),
)
],
),
],
)))
],
);
var transparent =
Theme.of(context).colorScheme.background.withAlpha(0).value;
List<double> stops = [
...listedApps[index].app.categories.asMap().entries.map(
(e) => ((e.key / (listedApps[index].app.categories.length - 1)))),
1
];
if (stops.length == 2) {
stops[0] = 1;
}
return Container(
decoration: BoxDecoration(
gradient: LinearGradient(
stops: stops,
begin: const Alignment(-1, 0),
end: const Alignment(-0.97, 0),
colors: [
...listedApps[index].app.categories.map((e) =>
Color(settingsProvider.categories[e] ?? transparent)
.withAlpha(255)),
Color(transparent)
])),
child: ListTile(
tileColor: listedApps[index].app.pinned
? Colors.grey.withOpacity(0.1)
: Colors.transparent,
selectedTileColor: Theme.of(context)
.colorScheme
.primary
.withOpacity(listedApps[index].app.pinned ? 0.2 : 0.1),
selected:
selectedAppIds.map((e) => e).contains(listedApps[index].app.id),
onLongPress: () {
toggleAppSelected(listedApps[index].app);
},
leading: getAppIcon(index),
title: Text(
maxLines: 1,
listedApps[index].name,
style: TextStyle(
overflow: TextOverflow.ellipsis,
fontWeight: listedApps[index].app.pinned
? FontWeight.bold
: FontWeight.normal,
),
),
subtitle: Text(tr('byX', args: [listedApps[index].app.author]),
maxLines: 1,
style: TextStyle(
overflow: TextOverflow.ellipsis,
fontWeight: listedApps[index].app.pinned
? FontWeight.bold
: FontWeight.normal)),
trailing: listedApps[index].downloadProgress != null
? SizedBox(
child: Text(
listedApps[index].downloadProgress! >= 0
? tr('percentProgress', args: [
listedApps[index]
.downloadProgress!
.toInt()
.toString()
])
: tr('installing'),
textAlign: (listedApps[index].downloadProgress! >= 0)
? TextAlign.start
: TextAlign.end,
))
: trailingRow,
onTap: () {
if (selectedAppIds.isNotEmpty) {
toggleAppSelected(listedApps[index].app);
} else {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) =>
AppPage(appId: listedApps[index].app.id)),
);
}
},
));
}
getCategoryCollapsibleTile(int index) {
var tiles = listedApps
.asMap()
.entries
.where((e) =>
e.value.app.categories.contains(listedCategories[index]) ||
e.value.app.categories.isEmpty && listedCategories[index] == null)
.map((e) => getSingleAppHorizTile(e.key))
.toList();
capFirstChar(String str) => str[0].toUpperCase() + str.substring(1);
return ExpansionTile(
initiallyExpanded: true,
title: Text(
capFirstChar(listedCategories[index] ?? tr('noCategory')),
style: const TextStyle(fontWeight: FontWeight.bold),
),
controlAffinity: ListTileControlAffinity.leading,
trailing: Text(tiles.length.toString()),
children: tiles);
}
getSelectAllButton() {
return selectedAppIds.isEmpty
? TextButton.icon(
style: const ButtonStyle(visualDensity: VisualDensity.compact),
onPressed: () {
selectThese(listedApps.map((e) => e.app).toList());
},
icon: Icon(
Icons.select_all_outlined,
color: Theme.of(context).colorScheme.primary,
),
label: Text(listedApps.length.toString()))
: TextButton.icon(
style: const ButtonStyle(visualDensity: VisualDensity.compact),
onPressed: () {
selectedAppIds.isEmpty
? selectThese(listedApps.map((e) => e.app).toList())
: clearSelected();
},
icon: Icon(
selectedAppIds.isEmpty
? Icons.select_all_outlined
: Icons.deselect_outlined,
color: Theme.of(context).colorScheme.primary,
),
label: Text(selectedAppIds.length.toString()));
}
getMassObtainFunction() {
return appsProvider.areDownloadsRunning() ||
(existingUpdateIdsAllOrSelected.isEmpty &&
newInstallIdsAllOrSelected.isEmpty &&
trackOnlyUpdateIdsAllOrSelected.isEmpty)
? null
: () {
HapticFeedback.heavyImpact();
List<GeneratedFormItem> formItems = [];
if (existingUpdateIdsAllOrSelected.isNotEmpty) {
formItems.add(GeneratedFormSwitch('updates',
label: tr('updateX', args: [
plural('apps', existingUpdateIdsAllOrSelected.length)
]),
defaultValue: true));
}
if (newInstallIdsAllOrSelected.isNotEmpty) {
formItems.add(GeneratedFormSwitch('installs',
label: tr('installX', args: [
plural('apps', newInstallIdsAllOrSelected.length)
]),
defaultValue: existingUpdateIdsAllOrSelected.isEmpty));
}
if (trackOnlyUpdateIdsAllOrSelected.isNotEmpty) {
formItems.add(GeneratedFormSwitch('trackonlies',
label: tr('markXTrackOnlyAsUpdated', args: [
plural('apps', trackOnlyUpdateIdsAllOrSelected.length)
]),
defaultValue: existingUpdateIdsAllOrSelected.isEmpty &&
newInstallIdsAllOrSelected.isEmpty));
}
showDialog<Map<String, dynamic>?>(
context: context,
builder: (BuildContext ctx) {
var totalApps = existingUpdateIdsAllOrSelected.length +
newInstallIdsAllOrSelected.length +
trackOnlyUpdateIdsAllOrSelected.length;
return GeneratedFormModal(
title: tr('changeX', args: [plural('apps', totalApps)]),
items: formItems.map((e) => [e]).toList(),
initValid: true,
);
}).then((values) async {
if (values != null) {
if (values.isEmpty) {
values = getDefaultValuesFromFormItems([formItems]);
}
bool shouldInstallUpdates = values['updates'] == true;
bool shouldInstallNew = values['installs'] == true;
bool shouldMarkTrackOnlies = values['trackonlies'] == true;
List<String> toInstall = [];
if (shouldInstallUpdates) {
toInstall.addAll(existingUpdateIdsAllOrSelected);
}
if (shouldInstallNew) {
toInstall.addAll(newInstallIdsAllOrSelected);
}
if (shouldMarkTrackOnlies) {
toInstall.addAll(trackOnlyUpdateIdsAllOrSelected);
}
appsProvider
.downloadAndInstallLatestApps(
toInstall, globalNavigatorKey.currentContext)
.catchError((e) {
showError(e, context);
return <String>[];
}).then((value) {
if (shouldInstallUpdates) {
showMessage(tr('appsUpdated'), context);
}
});
}
});
};
}
launchCategorizeDialog() {
return () async {
try {
Set<String>? preselected;
var showPrompt = false;
for (var element in selectedApps) {
var currentCats = element.categories.toSet();
if (preselected == null) {
preselected = currentCats;
} else {
if (!settingsProvider.setEqual(currentCats, preselected)) {
showPrompt = true;
break;
}
}
}
var cont = true;
if (showPrompt) {
cont = await showDialog<Map<String, dynamic>?>(
context: context,
builder: (BuildContext ctx) {
return GeneratedFormModal(
title: tr('categorize'),
items: const [],
initValid: true,
message: tr('selectedCategorizeWarning'),
);
}) !=
null;
}
if (cont) {
// ignore: use_build_context_synchronously
await showDialog<Map<String, dynamic>?>(
context: context,
builder: (BuildContext ctx) {
return GeneratedFormModal(
title: tr('categorize'),
items: const [],
initValid: true,
singleNullReturnButton: tr('continue'),
additionalWidgets: [
CategoryEditorSelector(
preselected: !showPrompt ? preselected ?? {} : {},
showLabelWhenNotEmpty: false,
onSelected: (categories) {
appsProvider.saveApps(selectedApps.map((e) {
e.categories = categories;
return e;
}).toList());
},
)
],
);
});
}
} catch (err) {
showError(err, context);
}
};
}
showMassMarkDialog() {
return showDialog(
context: context,
builder: (BuildContext ctx) {
return AlertDialog(
title: Text(tr('markXSelectedAppsAsUpdated',
args: [selectedAppIds.length.toString()])),
content: Text(
tr('onlyWorksWithNonVersionDetectApps'),
style: const TextStyle(
fontWeight: FontWeight.bold, fontStyle: FontStyle.italic),
),
actions: [
TextButton(
onPressed: () {
Navigator.of(context).pop();
},
child: Text(tr('no'))),
TextButton(
onPressed: () {
HapticFeedback.selectionClick();
appsProvider.saveApps(selectedApps.map((a) {
if (a.installedVersion != null &&
!appsProvider.isVersionDetectionPossible(
appsProvider.apps[a.id])) {
a.installedVersion = a.latestVersion;
}
return a;
}).toList());
Navigator.of(context).pop();
},
child: Text(tr('yes')))
],
);
}).whenComplete(() {
Navigator.of(context).pop();
});
}
pinSelectedApps() {
var pinStatus = selectedApps.where((element) => element.pinned).isEmpty;
appsProvider.saveApps(selectedApps.map((e) {
e.pinned = pinStatus;
return e;
}).toList());
Navigator.of(context).pop();
}
resetSelectedAppsInstallStatuses() async {
try {
var values = await showDialog(
context: context,
builder: (BuildContext ctx) {
return GeneratedFormModal(
title: tr('resetInstallStatusForSelectedAppsQuestion'),
items: const [],
initValid: true,
message: tr('installStatusOfXWillBeResetExplanation',
args: [plural('apps', selectedAppIds.length)]),
);
});
if (values != null) {
appsProvider.saveApps(selectedApps.map((e) {
e.installedVersion = null;
return e;
}).toList());
}
} finally {
Navigator.of(context).pop();
}
}
showMoreOptionsDialog() {
return showDialog(
context: context,
builder: (BuildContext ctx) {
return AlertDialog(
scrollable: true,
content: Padding(
padding: const EdgeInsets.only(top: 6),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
IconButton(
onPressed: appsProvider.areDownloadsRunning()
? null
: showMassMarkDialog,
tooltip: tr('markSelectedAppsUpdated'),
icon: const Icon(Icons.done)),
IconButton(
onPressed: pinSelectedApps,
tooltip: selectedApps
.where((element) => element.pinned)
.isEmpty
? tr('pinToTop')
: tr('unpinFromTop'),
icon: Icon(selectedApps
.where((element) => element.pinned)
.isEmpty
? Icons.bookmark_outline_rounded
: Icons.bookmark_remove_outlined),
),
IconButton(
onPressed: () {
String urls = '';
for (var a in selectedApps) {
urls += '${a.url}\n';
}
urls = urls.substring(0, urls.length - 1);
Share.share(urls,
subject:
'${tr('obtainium')} - ${tr('appsString')}');
Navigator.of(context).pop();
},
tooltip: tr('shareSelectedAppURLs'),
icon: const Icon(Icons.share_rounded),
),
IconButton(
onPressed: selectedAppIds.isEmpty
? null
: () {
String urls =
'<p>${tr('customLinkMessage')}:</p>\n\n<ul>\n';
for (var a in selectedApps) {
urls +=
' <li><a href="obtainium://app/${Uri.encodeComponent(jsonEncode({
'id': a.id,
'url': a.url,
'author': a.author,
'name': a.name,
'preferredApkIndex':
a.preferredApkIndex,
'additionalSettings':
jsonEncode(a.additionalSettings)
}))}">${a.name}</a></li>\n';
}
urls +=
'</ul>\n\n<p><a href="${obtainiumApp.url}">${tr('about')}</a></p>';
Share.share(urls,
subject:
'${tr('obtainium')} - ${tr('appsString')}');
},
tooltip: tr('shareAppConfigLinks'),
icon: const Icon(Icons.ios_share),
),
]),
),
);
});
}
getMainBottomButtons() {
return [
IconButton(
visualDensity: VisualDensity.compact,
onPressed: getMassObtainFunction(),
tooltip: selectedAppIds.isEmpty
? tr('installUpdateApps')
: tr('installUpdateSelectedApps'),
icon: const Icon(
Icons.file_download_outlined,
)),
IconButton(
visualDensity: VisualDensity.compact,
onPressed: selectedAppIds.isEmpty
? null
: () {
appsProvider.removeAppsWithModal(
context, selectedApps.toList());
},
tooltip: tr('removeSelectedApps'),
icon: const Icon(Icons.delete_outline_outlined),
),
IconButton(
visualDensity: VisualDensity.compact,
onPressed: selectedAppIds.isEmpty ? null : launchCategorizeDialog(),
tooltip: tr('categorize'),
icon: const Icon(Icons.category_outlined),
),
IconButton(
visualDensity: VisualDensity.compact,
onPressed: selectedAppIds.isEmpty ? null : showMoreOptionsDialog,
tooltip: tr('more'),
icon: const Icon(Icons.more_horiz),
),
];
}
showFilterDialog() async {
var values = await showDialog<Map<String, dynamic>?>(
context: context,
builder: (BuildContext ctx) {
var vals = filter.toFormValuesMap();
return GeneratedFormModal(
initValid: true,
title: tr('filterApps'),
items: [
[
GeneratedFormTextField('appName',
label: tr('appName'),
required: false,
defaultValue: vals['appName']),
GeneratedFormTextField('author',
label: tr('author'),
required: false,
defaultValue: vals['author'])
],
[
GeneratedFormTextField('appId',
label: tr('appId'),
required: false,
defaultValue: vals['appId'])
],
[
GeneratedFormSwitch('upToDateApps',
label: tr('upToDateApps'),
defaultValue: vals['upToDateApps'])
],
[
GeneratedFormSwitch('nonInstalledApps',
label: tr('nonInstalledApps'),
defaultValue: vals['nonInstalledApps'])
],
[
GeneratedFormDropdown(
'sourceFilter',
label: tr('appSource'),
defaultValue: filter.sourceFilter,
[
MapEntry('', tr('none')),
...sourceProvider.sources.map(
(e) => MapEntry(e.runtimeType.toString(), e.name))
])
]
],
additionalWidgets: [
const SizedBox(
height: 16,
),
CategoryEditorSelector(
preselected: filter.categoryFilter,
onSelected: (categories) {
filter.categoryFilter = categories.toSet();
},
)
],
);
});
if (values != null) {
setState(() {
filter.setFormValuesFromMap(values);
});
}
}
getFilterButtonsRow() {
var isFilterOff = filter.isIdenticalTo(neutralFilter, settingsProvider);
return Row(
children: [
getSelectAllButton(),
IconButton(
color: Theme.of(context).colorScheme.primary,
style: const ButtonStyle(visualDensity: VisualDensity.compact),
tooltip: isFilterOff ? tr('filter') : tr('filterActive'),
onPressed: isFilterOff
? showFilterDialog
: () {
setState(() {
filter = AppsFilter();
});
},
icon: Icon(isFilterOff
? Icons.filter_list_rounded
: Icons.filter_list_off_rounded)),
const SizedBox(
width: 10,
),
const VerticalDivider(),
Expanded(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: getMainBottomButtons(),
)),
],
);
}
getDisplayedList() {
return settingsProvider.groupByCategory &&
!(listedCategories.isEmpty ||
(listedCategories.length == 1 && listedCategories[0] == null))
? SliverList(
delegate:
SliverChildBuilderDelegate((BuildContext context, int index) {
return getCategoryCollapsibleTile(index);
}, childCount: listedCategories.length))
: SliverList(
delegate:
SliverChildBuilderDelegate((BuildContext context, int index) {
return getSingleAppHorizTile(index);
}, childCount: listedApps.length));
}
return Scaffold(
backgroundColor: Theme.of(context).colorScheme.surface,
body: RefreshIndicator(
key: _refreshIndicatorKey,
onRefresh: refresh,
child: CustomScrollView(slivers: <Widget>[
CustomAppBar(title: tr('appsString')),
...getLoadingWidgets(),
getDisplayedList()
])),
persistentFooterButtons: appsProvider.apps.isEmpty
? null
: [
getFilterButtonsRow(),
],
);
}
}
class AppsFilter {
late String nameFilter;
late String authorFilter;
late String idFilter;
late bool includeUptodate;
late bool includeNonInstalled;
late Set<String> categoryFilter;
late String sourceFilter;
AppsFilter(
{this.nameFilter = '',
this.authorFilter = '',
this.idFilter = '',
this.includeUptodate = true,
this.includeNonInstalled = true,
this.categoryFilter = const {},
this.sourceFilter = ''});
Map<String, dynamic> toFormValuesMap() {
return {
'appName': nameFilter,
'author': authorFilter,
'appId': idFilter,
'upToDateApps': includeUptodate,
'nonInstalledApps': includeNonInstalled,
'sourceFilter': sourceFilter
};
}
setFormValuesFromMap(Map<String, dynamic> values) {
nameFilter = values['appName']!;
authorFilter = values['author']!;
idFilter = values['appId']!;
includeUptodate = values['upToDateApps'];
includeNonInstalled = values['nonInstalledApps'];
sourceFilter = values['sourceFilter'];
}
bool isIdenticalTo(AppsFilter other, SettingsProvider settingsProvider) =>
authorFilter.trim() == other.authorFilter.trim() &&
nameFilter.trim() == other.nameFilter.trim() &&
idFilter.trim() == other.idFilter.trim() &&
includeUptodate == other.includeUptodate &&
includeNonInstalled == other.includeNonInstalled &&
settingsProvider.setEqual(categoryFilter, other.categoryFilter) &&
sourceFilter.trim() == other.sourceFilter.trim();
}