mirror of
				https://github.com/ImranR98/Obtainium.git
				synced 2025-11-03 23:03:29 +01:00 
			
		
		
		
	
		
			
				
	
	
		
			946 lines
		
	
	
		
			51 KiB
		
	
	
	
		
			Dart
		
	
	
	
	
	
			
		
		
	
	
			946 lines
		
	
	
		
			51 KiB
		
	
	
	
		
			Dart
		
	
	
	
	
	
import 'package:easy_localization/easy_localization.dart';
 | 
						|
import 'package:flutter/material.dart';
 | 
						|
import 'package:flutter/services.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';
 | 
						|
 | 
						|
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<App> selectedApps = {};
 | 
						|
  DateTime? refreshingSince;
 | 
						|
 | 
						|
  clearSelected() {
 | 
						|
    if (selectedApps.isNotEmpty) {
 | 
						|
      setState(() {
 | 
						|
        selectedApps.clear();
 | 
						|
      });
 | 
						|
      return true;
 | 
						|
    }
 | 
						|
    return false;
 | 
						|
  }
 | 
						|
 | 
						|
  selectThese(List<App> apps) {
 | 
						|
    if (selectedApps.isEmpty) {
 | 
						|
      setState(() {
 | 
						|
        for (var a in apps) {
 | 
						|
          selectedApps.add(a);
 | 
						|
        }
 | 
						|
      });
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  @override
 | 
						|
  Widget build(BuildContext context) {
 | 
						|
    var appsProvider = context.watch<AppsProvider>();
 | 
						|
    var settingsProvider = context.watch<SettingsProvider>();
 | 
						|
    var sortedApps = appsProvider.apps.values.toList();
 | 
						|
    var currentFilterIsUpdatesOnly =
 | 
						|
        filter.isIdenticalTo(updatesOnlyFilter, settingsProvider);
 | 
						|
 | 
						|
    selectedApps = selectedApps
 | 
						|
        .where((element) => sortedApps.map((e) => e.app).contains(element))
 | 
						|
        .toSet();
 | 
						|
 | 
						|
    toggleAppSelected(App app) {
 | 
						|
      setState(() {
 | 
						|
        if (selectedApps.contains(app)) {
 | 
						|
          selectedApps.remove(app);
 | 
						|
        } else {
 | 
						|
          selectedApps.add(app);
 | 
						|
        }
 | 
						|
      });
 | 
						|
    }
 | 
						|
 | 
						|
    sortedApps = sortedApps.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) {
 | 
						|
          var name = app.installedInfo?.name ?? app.app.name;
 | 
						|
          if (!name.toLowerCase().contains(t.toLowerCase())) {
 | 
						|
            return false;
 | 
						|
          }
 | 
						|
        }
 | 
						|
        for (var t in authorTokens) {
 | 
						|
          if (!app.app.author.toLowerCase().contains(t.toLowerCase())) {
 | 
						|
            return false;
 | 
						|
          }
 | 
						|
        }
 | 
						|
      }
 | 
						|
      if (filter.categoryFilter.isNotEmpty &&
 | 
						|
          filter.categoryFilter
 | 
						|
              .intersection(app.app.categories.toSet())
 | 
						|
              .isEmpty) {
 | 
						|
        return false;
 | 
						|
      }
 | 
						|
      return true;
 | 
						|
    }).toList();
 | 
						|
 | 
						|
    sortedApps.sort((a, b) {
 | 
						|
      var nameA = a.installedInfo?.name ?? a.app.name;
 | 
						|
      var nameB = b.installedInfo?.name ?? b.app.name;
 | 
						|
      int result = 0;
 | 
						|
      if (settingsProvider.sortColumn == SortColumnSettings.authorName) {
 | 
						|
        result = (a.app.author + nameA).compareTo(b.app.author + nameB);
 | 
						|
      } else if (settingsProvider.sortColumn == SortColumnSettings.nameAuthor) {
 | 
						|
        result = (nameA + a.app.author).compareTo(nameB + b.app.author);
 | 
						|
      }
 | 
						|
      return result;
 | 
						|
    });
 | 
						|
 | 
						|
    if (settingsProvider.sortOrder == SortOrderSettings.descending) {
 | 
						|
      sortedApps = sortedApps.reversed.toList();
 | 
						|
    }
 | 
						|
 | 
						|
    var existingUpdates = appsProvider.findExistingUpdates(installedOnly: true);
 | 
						|
 | 
						|
    var existingUpdateIdsAllOrSelected = existingUpdates
 | 
						|
        .where((element) => selectedApps.isEmpty
 | 
						|
            ? sortedApps.where((a) => a.app.id == element).isNotEmpty
 | 
						|
            : selectedApps.map((e) => e.id).contains(element))
 | 
						|
        .toList();
 | 
						|
    var newInstallIdsAllOrSelected = appsProvider
 | 
						|
        .findExistingUpdates(nonInstalledOnly: true)
 | 
						|
        .where((element) => selectedApps.isEmpty
 | 
						|
            ? sortedApps.where((a) => a.app.id == element).isNotEmpty
 | 
						|
            : selectedApps.map((e) => e.id).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 = [];
 | 
						|
      sortedApps = sortedApps.where((sa) {
 | 
						|
        if (existingUpdates.contains(sa.app.id)) {
 | 
						|
          temp.add(sa);
 | 
						|
          return false;
 | 
						|
        }
 | 
						|
        return true;
 | 
						|
      }).toList();
 | 
						|
      sortedApps = [...temp, ...sortedApps];
 | 
						|
    }
 | 
						|
 | 
						|
    var tempPinned = [];
 | 
						|
    var tempNotPinned = [];
 | 
						|
    for (var a in sortedApps) {
 | 
						|
      if (a.app.pinned) {
 | 
						|
        tempPinned.add(a);
 | 
						|
      } else {
 | 
						|
        tempNotPinned.add(a);
 | 
						|
      }
 | 
						|
    }
 | 
						|
    sortedApps = [...tempPinned, ...tempNotPinned];
 | 
						|
 | 
						|
    return Scaffold(
 | 
						|
      backgroundColor: Theme.of(context).colorScheme.surface,
 | 
						|
      body: RefreshIndicator(
 | 
						|
          onRefresh: () {
 | 
						|
            HapticFeedback.lightImpact();
 | 
						|
            setState(() {
 | 
						|
              refreshingSince = DateTime.now();
 | 
						|
            });
 | 
						|
            return appsProvider.checkUpdates().catchError((e) {
 | 
						|
              showError(e, context);
 | 
						|
            }).whenComplete(() {
 | 
						|
              setState(() {
 | 
						|
                refreshingSince = null;
 | 
						|
              });
 | 
						|
            });
 | 
						|
          },
 | 
						|
          child: CustomScrollView(slivers: <Widget>[
 | 
						|
            CustomAppBar(title: tr('appsString')),
 | 
						|
            if (appsProvider.loadingApps || sortedApps.isEmpty)
 | 
						|
              SliverFillRemaining(
 | 
						|
                  child: Center(
 | 
						|
                      child: appsProvider.loadingApps
 | 
						|
                          ? const CircularProgressIndicator()
 | 
						|
                          : Text(
 | 
						|
                              appsProvider.apps.isEmpty
 | 
						|
                                  ? tr('noApps')
 | 
						|
                                  : tr('noAppsForFilter'),
 | 
						|
                              style: Theme.of(context).textTheme.headlineMedium,
 | 
						|
                              textAlign: TextAlign.center,
 | 
						|
                            ))),
 | 
						|
            if (refreshingSince != null)
 | 
						|
              SliverToBoxAdapter(
 | 
						|
                child: LinearProgressIndicator(
 | 
						|
                  value: appsProvider.apps.values
 | 
						|
                          .where((element) => !(element.app.lastUpdateCheck
 | 
						|
                                  ?.isBefore(refreshingSince!) ??
 | 
						|
                              true))
 | 
						|
                          .length /
 | 
						|
                      appsProvider.apps.length,
 | 
						|
                ),
 | 
						|
              ),
 | 
						|
            SliverList(
 | 
						|
                delegate: SliverChildBuilderDelegate(
 | 
						|
                    (BuildContext context, int index) {
 | 
						|
              String? changesUrl = SourceProvider()
 | 
						|
                  .getSource(sortedApps[index].app.url)
 | 
						|
                  .changeLogPageFromStandardUrl(sortedApps[index].app.url);
 | 
						|
              var transparent = const Color.fromARGB(0, 0, 0, 0).value;
 | 
						|
              return Container(
 | 
						|
                  decoration: BoxDecoration(
 | 
						|
                      border: Border.symmetric(
 | 
						|
                          vertical: BorderSide(
 | 
						|
                              width: 4,
 | 
						|
                              color: Color(
 | 
						|
                                  sortedApps[index].app.categories.isNotEmpty
 | 
						|
                                      ? settingsProvider.categories[
 | 
						|
                                              sortedApps[index]
 | 
						|
                                                  .app
 | 
						|
                                                  .categories
 | 
						|
                                                  .first] ??
 | 
						|
                                          transparent
 | 
						|
                                      : transparent)))),
 | 
						|
                  child: ListTile(
 | 
						|
                    tileColor: sortedApps[index].app.pinned
 | 
						|
                        ? Colors.grey.withOpacity(0.1)
 | 
						|
                        : Colors.transparent,
 | 
						|
                    selectedTileColor: Theme.of(context)
 | 
						|
                        .colorScheme
 | 
						|
                        .primary
 | 
						|
                        .withOpacity(sortedApps[index].app.pinned ? 0.2 : 0.1),
 | 
						|
                    selected: selectedApps.contains(sortedApps[index].app),
 | 
						|
                    onLongPress: () {
 | 
						|
                      toggleAppSelected(sortedApps[index].app);
 | 
						|
                    },
 | 
						|
                    leading: sortedApps[index].installedInfo != null
 | 
						|
                        ? Image.memory(
 | 
						|
                            sortedApps[index].installedInfo!.icon!,
 | 
						|
                            gaplessPlayback: true,
 | 
						|
                          )
 | 
						|
                        : null,
 | 
						|
                    title: Text(
 | 
						|
                      sortedApps[index].installedInfo?.name ??
 | 
						|
                          sortedApps[index].app.name,
 | 
						|
                      style: TextStyle(
 | 
						|
                        fontWeight: sortedApps[index].app.pinned
 | 
						|
                            ? FontWeight.bold
 | 
						|
                            : FontWeight.normal,
 | 
						|
                      ),
 | 
						|
                    ),
 | 
						|
                    subtitle: Text(
 | 
						|
                        tr('byX', args: [sortedApps[index].app.author]),
 | 
						|
                        style: TextStyle(
 | 
						|
                            fontWeight: sortedApps[index].app.pinned
 | 
						|
                                ? FontWeight.bold
 | 
						|
                                : FontWeight.normal)),
 | 
						|
                    trailing: SingleChildScrollView(
 | 
						|
                        reverse: true,
 | 
						|
                        child: sortedApps[index].downloadProgress != null
 | 
						|
                            ? Text(tr('percentProgress', args: [
 | 
						|
                                sortedApps[index]
 | 
						|
                                        .downloadProgress
 | 
						|
                                        ?.toInt()
 | 
						|
                                        .toString() ??
 | 
						|
                                    '100'
 | 
						|
                              ]))
 | 
						|
                            : (Column(
 | 
						|
                                mainAxisAlignment: MainAxisAlignment.center,
 | 
						|
                                crossAxisAlignment: CrossAxisAlignment.end,
 | 
						|
                                children: [
 | 
						|
                                  SizedBox(
 | 
						|
                                      width: 100,
 | 
						|
                                      child: Text(
 | 
						|
                                        '${sortedApps[index].app.installedVersion ?? tr('notInstalled')}${sortedApps[index].app.additionalSettings['trackOnly'] == true ? ' ${tr('estimateInBrackets')}' : ''}',
 | 
						|
                                        overflow: TextOverflow.fade,
 | 
						|
                                        textAlign: TextAlign.end,
 | 
						|
                                      )),
 | 
						|
                                  sortedApps[index].app.installedVersion !=
 | 
						|
                                              null &&
 | 
						|
                                          sortedApps[index]
 | 
						|
                                                  .app
 | 
						|
                                                  .installedVersion !=
 | 
						|
                                              sortedApps[index]
 | 
						|
                                                  .app
 | 
						|
                                                  .latestVersion
 | 
						|
                                      ? GestureDetector(
 | 
						|
                                          onTap: changesUrl == null
 | 
						|
                                              ? null
 | 
						|
                                              : () {
 | 
						|
                                                  launchUrlString(changesUrl,
 | 
						|
                                                      mode: LaunchMode
 | 
						|
                                                          .externalApplication);
 | 
						|
                                                },
 | 
						|
                                          child: appsProvider
 | 
						|
                                                  .areDownloadsRunning()
 | 
						|
                                              ? Text(tr('pleaseWait'))
 | 
						|
                                              : Text(
 | 
						|
                                                  '${tr('updateAvailable')}${sortedApps[index].app.additionalSettings['trackOnly'] == true ? ' ${tr('estimateInBracketsShort')}' : ''}',
 | 
						|
                                                  style: TextStyle(
 | 
						|
                                                      fontStyle:
 | 
						|
                                                          FontStyle.italic,
 | 
						|
                                                      decoration: changesUrl ==
 | 
						|
                                                              null
 | 
						|
                                                          ? TextDecoration.none
 | 
						|
                                                          : TextDecoration
 | 
						|
                                                              .underline),
 | 
						|
                                                ))
 | 
						|
                                      : const SizedBox(),
 | 
						|
                                ],
 | 
						|
                              ))),
 | 
						|
                    onTap: () {
 | 
						|
                      if (selectedApps.isNotEmpty) {
 | 
						|
                        toggleAppSelected(sortedApps[index].app);
 | 
						|
                      } else {
 | 
						|
                        Navigator.push(
 | 
						|
                          context,
 | 
						|
                          MaterialPageRoute(
 | 
						|
                              builder: (context) =>
 | 
						|
                                  AppPage(appId: sortedApps[index].app.id)),
 | 
						|
                        );
 | 
						|
                      }
 | 
						|
                    },
 | 
						|
                  ));
 | 
						|
            }, childCount: sortedApps.length))
 | 
						|
          ])),
 | 
						|
      persistentFooterButtons: appsProvider.apps.isEmpty
 | 
						|
          ? null
 | 
						|
          : [
 | 
						|
              Row(
 | 
						|
                children: [
 | 
						|
                  selectedApps.isEmpty
 | 
						|
                      ? TextButton.icon(
 | 
						|
                          style: const ButtonStyle(
 | 
						|
                              visualDensity: VisualDensity.compact),
 | 
						|
                          onPressed: () {
 | 
						|
                            selectThese(sortedApps.map((e) => e.app).toList());
 | 
						|
                          },
 | 
						|
                          icon: Icon(
 | 
						|
                            Icons.select_all_outlined,
 | 
						|
                            color: Theme.of(context).colorScheme.primary,
 | 
						|
                          ),
 | 
						|
                          label: Text(sortedApps.length.toString()))
 | 
						|
                      : TextButton.icon(
 | 
						|
                          style: const ButtonStyle(
 | 
						|
                              visualDensity: VisualDensity.compact),
 | 
						|
                          onPressed: () {
 | 
						|
                            selectedApps.isEmpty
 | 
						|
                                ? selectThese(
 | 
						|
                                    sortedApps.map((e) => e.app).toList())
 | 
						|
                                : clearSelected();
 | 
						|
                          },
 | 
						|
                          icon: Icon(
 | 
						|
                            selectedApps.isEmpty
 | 
						|
                                ? Icons.select_all_outlined
 | 
						|
                                : Icons.deselect_outlined,
 | 
						|
                            color: Theme.of(context).colorScheme.primary,
 | 
						|
                          ),
 | 
						|
                          label: Text(selectedApps.length.toString())),
 | 
						|
                  const VerticalDivider(),
 | 
						|
                  Expanded(
 | 
						|
                      child: SingleChildScrollView(
 | 
						|
                          scrollDirection: Axis.horizontal,
 | 
						|
                          child: Row(
 | 
						|
                            mainAxisAlignment: MainAxisAlignment.spaceEvenly,
 | 
						|
                            children: [
 | 
						|
                              IconButton(
 | 
						|
                                visualDensity: VisualDensity.compact,
 | 
						|
                                onPressed: selectedApps.isEmpty
 | 
						|
                                    ? null
 | 
						|
                                    : () {
 | 
						|
                                        showDialog<Map<String, dynamic>?>(
 | 
						|
                                            context: context,
 | 
						|
                                            builder: (BuildContext ctx) {
 | 
						|
                                              return GeneratedFormModal(
 | 
						|
                                                title: tr(
 | 
						|
                                                    'removeSelectedAppsQuestion'),
 | 
						|
                                                items: const [],
 | 
						|
                                                initValid: true,
 | 
						|
                                                message: tr(
 | 
						|
                                                    'xWillBeRemovedButRemainInstalled',
 | 
						|
                                                    args: [
 | 
						|
                                                      plural('apps',
 | 
						|
                                                          selectedApps.length)
 | 
						|
                                                    ]),
 | 
						|
                                              );
 | 
						|
                                            }).then((values) {
 | 
						|
                                          if (values != null) {
 | 
						|
                                            appsProvider.removeApps(selectedApps
 | 
						|
                                                .map((e) => e.id)
 | 
						|
                                                .toList());
 | 
						|
                                          }
 | 
						|
                                        });
 | 
						|
                                      },
 | 
						|
                                tooltip: tr('removeSelectedApps'),
 | 
						|
                                icon: const Icon(Icons.delete_outline_outlined),
 | 
						|
                              ),
 | 
						|
                              IconButton(
 | 
						|
                                  visualDensity: VisualDensity.compact,
 | 
						|
                                  onPressed: 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
 | 
						|
                                                        .isNotEmpty));
 | 
						|
                                          }
 | 
						|
                                          if (trackOnlyUpdateIdsAllOrSelected
 | 
						|
                                              .isNotEmpty) {
 | 
						|
                                            formItems.add(GeneratedFormSwitch(
 | 
						|
                                                'trackonlies',
 | 
						|
                                                label: tr(
 | 
						|
                                                    'markXTrackOnlyAsUpdated',
 | 
						|
                                                    args: [
 | 
						|
                                                      plural(
 | 
						|
                                                          'apps',
 | 
						|
                                                          trackOnlyUpdateIdsAllOrSelected
 | 
						|
                                                              .length)
 | 
						|
                                                    ]),
 | 
						|
                                                defaultValue:
 | 
						|
                                                    existingUpdateIdsAllOrSelected
 | 
						|
                                                            .isNotEmpty ||
 | 
						|
                                                        newInstallIdsAllOrSelected
 | 
						|
                                                            .isNotEmpty));
 | 
						|
                                          }
 | 
						|
                                          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) {
 | 
						|
                                            if (values != null) {
 | 
						|
                                              if (values.isEmpty) {
 | 
						|
                                                values =
 | 
						|
                                                    getDefaultValuesFromFormItems(
 | 
						|
                                                        [formItems]);
 | 
						|
                                              }
 | 
						|
                                              bool shouldInstallUpdates =
 | 
						|
                                                  values['updates'] == true;
 | 
						|
                                              bool shouldInstallNew =
 | 
						|
                                                  values['installs'] == true;
 | 
						|
                                              bool shouldMarkTrackOnlies =
 | 
						|
                                                  values['trackonlies'] == true;
 | 
						|
                                              (() async {
 | 
						|
                                                if (shouldInstallNew ||
 | 
						|
                                                    shouldInstallUpdates) {
 | 
						|
                                                  await settingsProvider
 | 
						|
                                                      .getInstallPermission();
 | 
						|
                                                }
 | 
						|
                                              })()
 | 
						|
                                                  .then((_) {
 | 
						|
                                                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);
 | 
						|
                                                });
 | 
						|
                                              });
 | 
						|
                                            }
 | 
						|
                                          });
 | 
						|
                                        },
 | 
						|
                                  tooltip: selectedApps.isEmpty
 | 
						|
                                      ? tr('installUpdateApps')
 | 
						|
                                      : tr('installUpdateSelectedApps'),
 | 
						|
                                  icon: const Icon(
 | 
						|
                                    Icons.file_download_outlined,
 | 
						|
                                  )),
 | 
						|
                              IconButton(
 | 
						|
                                visualDensity: VisualDensity.compact,
 | 
						|
                                onPressed: selectedApps.isEmpty
 | 
						|
                                    ? null
 | 
						|
                                    : () 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);
 | 
						|
                                        }
 | 
						|
                                      },
 | 
						|
                                tooltip: tr('categorize'),
 | 
						|
                                icon: const Icon(Icons.category_outlined),
 | 
						|
                              ),
 | 
						|
                              IconButton(
 | 
						|
                                visualDensity: VisualDensity.compact,
 | 
						|
                                onPressed: selectedApps.isEmpty
 | 
						|
                                    ? null
 | 
						|
                                    : () {
 | 
						|
                                        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
 | 
						|
                                                                : () {
 | 
						|
                                                                    showDialog(
 | 
						|
                                                                        context:
 | 
						|
                                                                            context,
 | 
						|
                                                                        builder:
 | 
						|
                                                                            (BuildContext
 | 
						|
                                                                                ctx) {
 | 
						|
                                                                          return AlertDialog(
 | 
						|
                                                                            title:
 | 
						|
                                                                                Text(tr('markXSelectedAppsAsUpdated', args: [
 | 
						|
                                                                              selectedApps.length.toString()
 | 
						|
                                                                            ])),
 | 
						|
                                                                            content:
 | 
						|
                                                                                Text(
 | 
						|
                                                                              tr('onlyWorksWithNonEVDApps'),
 | 
						|
                                                                              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) {
 | 
						|
                                                                                        a.installedVersion = a.latestVersion;
 | 
						|
                                                                                      }
 | 
						|
                                                                                      return a;
 | 
						|
                                                                                    }).toList());
 | 
						|
 | 
						|
                                                                                    Navigator.of(context).pop();
 | 
						|
                                                                                  },
 | 
						|
                                                                                  child: Text(tr('yes')))
 | 
						|
                                                                            ],
 | 
						|
                                                                          );
 | 
						|
                                                                        }).whenComplete(() {
 | 
						|
                                                                      Navigator.of(
 | 
						|
                                                                              context)
 | 
						|
                                                                          .pop();
 | 
						|
                                                                    });
 | 
						|
                                                                  },
 | 
						|
                                                            tooltip: tr(
 | 
						|
                                                                'markSelectedAppsUpdated'),
 | 
						|
                                                            icon: const Icon(
 | 
						|
                                                                Icons.done)),
 | 
						|
                                                        IconButton(
 | 
						|
                                                          onPressed: () {
 | 
						|
                                                            var pinStatus = selectedApps
 | 
						|
                                                                .where((element) =>
 | 
						|
                                                                    element
 | 
						|
                                                                        .pinned)
 | 
						|
                                                                .isEmpty;
 | 
						|
                                                            appsProvider
 | 
						|
                                                                .saveApps(
 | 
						|
                                                                    selectedApps
 | 
						|
                                                                        .map(
 | 
						|
                                                                            (e) {
 | 
						|
                                                              e.pinned =
 | 
						|
                                                                  pinStatus;
 | 
						|
                                                              return e;
 | 
						|
                                                            }).toList());
 | 
						|
                                                            Navigator.of(
 | 
						|
                                                                    context)
 | 
						|
                                                                .pop();
 | 
						|
                                                          },
 | 
						|
                                                          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(
 | 
						|
                                                                    'selectedAppURLsFromObtainium'));
 | 
						|
                                                            Navigator.of(
 | 
						|
                                                                    context)
 | 
						|
                                                                .pop();
 | 
						|
                                                          },
 | 
						|
                                                          tooltip: tr(
 | 
						|
                                                              'shareSelectedAppURLs'),
 | 
						|
                                                          icon: const Icon(
 | 
						|
                                                              Icons.share),
 | 
						|
                                                        ),
 | 
						|
                                                        IconButton(
 | 
						|
                                                          onPressed: () {
 | 
						|
                                                            showDialog(
 | 
						|
                                                                context:
 | 
						|
                                                                    context,
 | 
						|
                                                                builder:
 | 
						|
                                                                    (BuildContext
 | 
						|
                                                                        ctx) {
 | 
						|
                                                                  return GeneratedFormModal(
 | 
						|
                                                                    title: tr(
 | 
						|
                                                                        'resetInstallStatusForSelectedAppsQuestion'),
 | 
						|
                                                                    items: const [],
 | 
						|
                                                                    initValid:
 | 
						|
                                                                        true,
 | 
						|
                                                                    message: tr(
 | 
						|
                                                                        'installStatusOfXWillBeResetExplanation',
 | 
						|
                                                                        args: [
 | 
						|
                                                                          plural(
 | 
						|
                                                                              'app',
 | 
						|
                                                                              selectedApps.length)
 | 
						|
                                                                        ]),
 | 
						|
                                                                  );
 | 
						|
                                                                }).then((values) {
 | 
						|
                                                              if (values !=
 | 
						|
                                                                  null) {
 | 
						|
                                                                appsProvider.saveApps(
 | 
						|
                                                                    selectedApps
 | 
						|
                                                                        .map(
 | 
						|
                                                                            (e) {
 | 
						|
                                                                  e.installedVersion =
 | 
						|
                                                                      null;
 | 
						|
                                                                  return e;
 | 
						|
                                                                }).toList());
 | 
						|
                                                              }
 | 
						|
                                                            }).whenComplete(() {
 | 
						|
                                                              Navigator.of(
 | 
						|
                                                                      context)
 | 
						|
                                                                  .pop();
 | 
						|
                                                            });
 | 
						|
                                                          },
 | 
						|
                                                          tooltip: tr(
 | 
						|
                                                              'resetInstallStatus'),
 | 
						|
                                                          icon: const Icon(Icons
 | 
						|
                                                              .restore_page_outlined),
 | 
						|
                                                        ),
 | 
						|
                                                      ]),
 | 
						|
                                                ),
 | 
						|
                                              );
 | 
						|
                                            });
 | 
						|
                                      },
 | 
						|
                                tooltip: tr('more'),
 | 
						|
                                icon: const Icon(Icons.more_horiz),
 | 
						|
                              ),
 | 
						|
                            ],
 | 
						|
                          ))),
 | 
						|
                  const VerticalDivider(),
 | 
						|
                  IconButton(
 | 
						|
                    visualDensity: VisualDensity.compact,
 | 
						|
                    onPressed: () {
 | 
						|
                      setState(() {
 | 
						|
                        if (currentFilterIsUpdatesOnly) {
 | 
						|
                          filter = AppsFilter();
 | 
						|
                        } else {
 | 
						|
                          filter = updatesOnlyFilter;
 | 
						|
                        }
 | 
						|
                      });
 | 
						|
                    },
 | 
						|
                    tooltip: currentFilterIsUpdatesOnly
 | 
						|
                        ? tr('removeOutdatedFilter')
 | 
						|
                        : tr('showOutdatedOnly'),
 | 
						|
                    icon: Icon(
 | 
						|
                      currentFilterIsUpdatesOnly
 | 
						|
                          ? Icons.update_disabled_rounded
 | 
						|
                          : Icons.update_rounded,
 | 
						|
                      color: Theme.of(context).colorScheme.primary,
 | 
						|
                    ),
 | 
						|
                  ),
 | 
						|
                  TextButton.icon(
 | 
						|
                      style: const ButtonStyle(
 | 
						|
                          visualDensity: VisualDensity.compact),
 | 
						|
                      label: Text(
 | 
						|
                        filter.isIdenticalTo(neutralFilter, settingsProvider)
 | 
						|
                            ? tr('filter')
 | 
						|
                            : tr('filterActive'),
 | 
						|
                        style: TextStyle(
 | 
						|
                            fontWeight: filter.isIdenticalTo(
 | 
						|
                                    neutralFilter, settingsProvider)
 | 
						|
                                ? FontWeight.normal
 | 
						|
                                : FontWeight.bold),
 | 
						|
                      ),
 | 
						|
                      onPressed: () {
 | 
						|
                        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'])
 | 
						|
                                  ],
 | 
						|
                                  [
 | 
						|
                                    GeneratedFormSwitch('upToDateApps',
 | 
						|
                                        label: tr('upToDateApps'),
 | 
						|
                                        defaultValue: vals['upToDateApps'])
 | 
						|
                                  ],
 | 
						|
                                  [
 | 
						|
                                    GeneratedFormSwitch('nonInstalledApps',
 | 
						|
                                        label: tr('nonInstalledApps'),
 | 
						|
                                        defaultValue: vals['nonInstalledApps'])
 | 
						|
                                  ]
 | 
						|
                                ],
 | 
						|
                                additionalWidgets: [
 | 
						|
                                  const SizedBox(
 | 
						|
                                    height: 16,
 | 
						|
                                  ),
 | 
						|
                                  CategoryEditorSelector(
 | 
						|
                                    preselected: filter.categoryFilter,
 | 
						|
                                    onSelected: (categories) {
 | 
						|
                                      filter.categoryFilter =
 | 
						|
                                          categories.toSet();
 | 
						|
                                    },
 | 
						|
                                  )
 | 
						|
                                ],
 | 
						|
                              );
 | 
						|
                            }).then((values) {
 | 
						|
                          if (values != null) {
 | 
						|
                            setState(() {
 | 
						|
                              filter.setFormValuesFromMap(values);
 | 
						|
                            });
 | 
						|
                          }
 | 
						|
                        });
 | 
						|
                      },
 | 
						|
                      icon: const Icon(Icons.filter_list_rounded))
 | 
						|
                ],
 | 
						|
              ),
 | 
						|
            ],
 | 
						|
    );
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
class AppsFilter {
 | 
						|
  late String nameFilter;
 | 
						|
  late String authorFilter;
 | 
						|
  late bool includeUptodate;
 | 
						|
  late bool includeNonInstalled;
 | 
						|
  late Set<String> categoryFilter;
 | 
						|
 | 
						|
  AppsFilter(
 | 
						|
      {this.nameFilter = '',
 | 
						|
      this.authorFilter = '',
 | 
						|
      this.includeUptodate = true,
 | 
						|
      this.includeNonInstalled = true,
 | 
						|
      this.categoryFilter = const {}});
 | 
						|
 | 
						|
  Map<String, dynamic> toFormValuesMap() {
 | 
						|
    return {
 | 
						|
      'appName': nameFilter,
 | 
						|
      'author': authorFilter,
 | 
						|
      'upToDateApps': includeUptodate,
 | 
						|
      'nonInstalledApps': includeNonInstalled
 | 
						|
    };
 | 
						|
  }
 | 
						|
 | 
						|
  setFormValuesFromMap(Map<String, dynamic> values) {
 | 
						|
    nameFilter = values['appName']!;
 | 
						|
    authorFilter = values['author']!;
 | 
						|
    includeUptodate = values['upToDateApps'];
 | 
						|
    includeNonInstalled = values['nonInstalledApps'];
 | 
						|
  }
 | 
						|
 | 
						|
  bool isIdenticalTo(AppsFilter other, SettingsProvider settingsProvider) =>
 | 
						|
      authorFilter.trim() == other.authorFilter.trim() &&
 | 
						|
      nameFilter.trim() == other.nameFilter.trim() &&
 | 
						|
      includeUptodate == other.includeUptodate &&
 | 
						|
      includeNonInstalled == other.includeNonInstalled &&
 | 
						|
      settingsProvider.setEqual(categoryFilter, other.categoryFilter);
 | 
						|
}
 |