mirror of
				https://github.com/ImranR98/Obtainium.git
				synced 2025-11-03 23:03:29 +01:00 
			
		
		
		
	Merge pull request #177 from ImranR98/dev
Added very basic categorization support
This commit is contained in:
		@@ -199,6 +199,14 @@
 | 
				
			|||||||
    "downloadNotifDescription": "Notifies the user of the progress in downloading an App",
 | 
					    "downloadNotifDescription": "Notifies the user of the progress in downloading an App",
 | 
				
			||||||
    "noAPKFound": "No APK found",
 | 
					    "noAPKFound": "No APK found",
 | 
				
			||||||
    "noVersionDetection": "No version detection",
 | 
					    "noVersionDetection": "No version detection",
 | 
				
			||||||
 | 
					    "categorize": "Categorize",
 | 
				
			||||||
 | 
					    "categories": "Categories",
 | 
				
			||||||
 | 
					    "category": "Category",
 | 
				
			||||||
 | 
					    "noCategory": "No Category",
 | 
				
			||||||
 | 
					    "deleteCategoryQuestion": "Delete Category?",
 | 
				
			||||||
 | 
					    "categoryDeleteWarning": "All Apps in {} will be set to uncategorized.",
 | 
				
			||||||
 | 
					    "addCategory": "Add Category",
 | 
				
			||||||
 | 
					    "label": "Label",
 | 
				
			||||||
    "tooManyRequestsTryAgainInMinutes": {
 | 
					    "tooManyRequestsTryAgainInMinutes": {
 | 
				
			||||||
        "one": "Zu viele Anfragen (Rate begrenzt) - versuchen Sie es in {} Minute erneut",
 | 
					        "one": "Zu viele Anfragen (Rate begrenzt) - versuchen Sie es in {} Minute erneut",
 | 
				
			||||||
        "other": "Zu viele Anfragen (Rate begrenzt) - versuchen Sie es in {} Minuten erneut"
 | 
					        "other": "Zu viele Anfragen (Rate begrenzt) - versuchen Sie es in {} Minuten erneut"
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -199,6 +199,14 @@
 | 
				
			|||||||
    "downloadNotifDescription": "Notifies the user of the progress in downloading an App",
 | 
					    "downloadNotifDescription": "Notifies the user of the progress in downloading an App",
 | 
				
			||||||
    "noAPKFound": "No APK found",
 | 
					    "noAPKFound": "No APK found",
 | 
				
			||||||
    "noVersionDetection": "No version detection",
 | 
					    "noVersionDetection": "No version detection",
 | 
				
			||||||
 | 
					    "categorize": "Categorize",
 | 
				
			||||||
 | 
					    "categories": "Categories",
 | 
				
			||||||
 | 
					    "category": "Category",
 | 
				
			||||||
 | 
					    "noCategory": "No Category",
 | 
				
			||||||
 | 
					    "deleteCategoryQuestion": "Delete Category?",
 | 
				
			||||||
 | 
					    "categoryDeleteWarning": "All Apps in {} will be set to uncategorized.",
 | 
				
			||||||
 | 
					    "addCategory": "Add Category",
 | 
				
			||||||
 | 
					    "label": "Label",
 | 
				
			||||||
    "tooManyRequestsTryAgainInMinutes": {
 | 
					    "tooManyRequestsTryAgainInMinutes": {
 | 
				
			||||||
        "one": "Too many requests (rate limited) - try again in {} minute",
 | 
					        "one": "Too many requests (rate limited) - try again in {} minute",
 | 
				
			||||||
        "other": "Too many requests (rate limited) - try again in {} minutes"
 | 
					        "other": "Too many requests (rate limited) - try again in {} minutes"
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -199,6 +199,14 @@
 | 
				
			|||||||
    "downloadNotifDescription": "Notifies the user of the progress in downloading an App",
 | 
					    "downloadNotifDescription": "Notifies the user of the progress in downloading an App",
 | 
				
			||||||
    "noAPKFound": "No APK found",
 | 
					    "noAPKFound": "No APK found",
 | 
				
			||||||
    "noVersionDetection": "No version detection",
 | 
					    "noVersionDetection": "No version detection",
 | 
				
			||||||
 | 
					    "categorize": "Categorize",
 | 
				
			||||||
 | 
					    "categories": "Categories",
 | 
				
			||||||
 | 
					    "category": "Category",
 | 
				
			||||||
 | 
					    "noCategory": "No Category",
 | 
				
			||||||
 | 
					    "deleteCategoryQuestion": "Delete Category?",
 | 
				
			||||||
 | 
					    "categoryDeleteWarning": "All Apps in {} will be set to uncategorized.",
 | 
				
			||||||
 | 
					    "addCategory": "Add Category",
 | 
				
			||||||
 | 
					    "label": "Label",
 | 
				
			||||||
    "tooManyRequestsTryAgainInMinutes": {
 | 
					    "tooManyRequestsTryAgainInMinutes": {
 | 
				
			||||||
        "one": "Túl sok kérés (korlátozott arány) – próbálja újra {} perc múlva",
 | 
					        "one": "Túl sok kérés (korlátozott arány) – próbálja újra {} perc múlva",
 | 
				
			||||||
        "other": "Túl sok kérés (korlátozott arány) – próbálja újra {} perc múlva"
 | 
					        "other": "Túl sok kérés (korlátozott arány) – próbálja újra {} perc múlva"
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -199,6 +199,14 @@
 | 
				
			|||||||
    "downloadNotifDescription": "Notifies the user of the progress in downloading an App",
 | 
					    "downloadNotifDescription": "Notifies the user of the progress in downloading an App",
 | 
				
			||||||
    "noAPKFound": "No APK found",
 | 
					    "noAPKFound": "No APK found",
 | 
				
			||||||
    "noVersionDetection": "No version detection",
 | 
					    "noVersionDetection": "No version detection",
 | 
				
			||||||
 | 
					    "categorize": "Categorize",
 | 
				
			||||||
 | 
					    "categories": "Categories",
 | 
				
			||||||
 | 
					    "category": "Category",
 | 
				
			||||||
 | 
					    "noCategory": "No Category",
 | 
				
			||||||
 | 
					    "deleteCategoryQuestion": "Delete Category?",
 | 
				
			||||||
 | 
					    "categoryDeleteWarning": "All Apps in {} will be set to uncategorized.",
 | 
				
			||||||
 | 
					    "addCategory": "Add Category",
 | 
				
			||||||
 | 
					    "label": "Label",
 | 
				
			||||||
    "tooManyRequestsTryAgainInMinutes": {
 | 
					    "tooManyRequestsTryAgainInMinutes": {
 | 
				
			||||||
        "one": "Troppe richieste (traffico limitato) - riprova tra {} minuto",
 | 
					        "one": "Troppe richieste (traffico limitato) - riprova tra {} minuto",
 | 
				
			||||||
        "other": "Troppe richieste (traffico limitato) - riprova tra {} minuti"
 | 
					        "other": "Troppe richieste (traffico limitato) - riprova tra {} minuti"
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -199,6 +199,14 @@
 | 
				
			|||||||
    "downloadNotifDescription": "アプリのダウンロード状況を通知する",
 | 
					    "downloadNotifDescription": "アプリのダウンロード状況を通知する",
 | 
				
			||||||
    "noAPKFound": "APKが見つかりません",
 | 
					    "noAPKFound": "APKが見つかりません",
 | 
				
			||||||
    "noVersionDetection": "バージョン検出を行わない",
 | 
					    "noVersionDetection": "バージョン検出を行わない",
 | 
				
			||||||
 | 
					    "categorize": "Categorize",
 | 
				
			||||||
 | 
					    "categories": "Categories",
 | 
				
			||||||
 | 
					    "category": "Category",
 | 
				
			||||||
 | 
					    "noCategory": "No Category",
 | 
				
			||||||
 | 
					    "deleteCategoryQuestion": "Delete Category?",
 | 
				
			||||||
 | 
					    "categoryDeleteWarning": "All Apps in {} will be set to uncategorized.",
 | 
				
			||||||
 | 
					    "addCategory": "Add Category",
 | 
				
			||||||
 | 
					    "label": "Label",
 | 
				
			||||||
    "tooManyRequestsTryAgainInMinutes": {
 | 
					    "tooManyRequestsTryAgainInMinutes": {
 | 
				
			||||||
        "one": "リクエストが多すぎます(レート制限)- {}分後に再試行してください",
 | 
					        "one": "リクエストが多すぎます(レート制限)- {}分後に再試行してください",
 | 
				
			||||||
        "other": "リクエストが多すぎます(レート制限)- {}分後に再試行してください"
 | 
					        "other": "リクエストが多すぎます(レート制限)- {}分後に再試行してください"
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -199,6 +199,14 @@
 | 
				
			|||||||
    "downloadNotifDescription": "通知用户下载进度",
 | 
					    "downloadNotifDescription": "通知用户下载进度",
 | 
				
			||||||
    "noAPKFound": "未找到安装包",
 | 
					    "noAPKFound": "未找到安装包",
 | 
				
			||||||
    "noVersionDetection": "无版本检测",
 | 
					    "noVersionDetection": "无版本检测",
 | 
				
			||||||
 | 
					    "categorize": "Categorize",
 | 
				
			||||||
 | 
					    "categories": "Categories",
 | 
				
			||||||
 | 
					    "category": "Category",
 | 
				
			||||||
 | 
					    "noCategory": "No Category",
 | 
				
			||||||
 | 
					    "deleteCategoryQuestion": "Delete Category?",
 | 
				
			||||||
 | 
					    "categoryDeleteWarning": "All Apps in {} will be set to uncategorized.",
 | 
				
			||||||
 | 
					    "addCategory": "Add Category",
 | 
				
			||||||
 | 
					    "label": "Label",
 | 
				
			||||||
    "tooManyRequestsTryAgainInMinutes": {
 | 
					    "tooManyRequestsTryAgainInMinutes": {
 | 
				
			||||||
        "one": "请求过多 (API 限制) - 在 {} 分钟后重试",
 | 
					        "one": "请求过多 (API 限制) - 在 {} 分钟后重试",
 | 
				
			||||||
        "other": "请求过多 (API 限制) - 在 {} 分钟后重试"
 | 
					        "other": "请求过多 (API 限制) - 在 {} 分钟后重试"
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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.8.23';
 | 
					const String currentVersion = '0.9.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
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,6 +1,7 @@
 | 
				
			|||||||
import 'package:easy_localization/easy_localization.dart';
 | 
					import 'package:easy_localization/easy_localization.dart';
 | 
				
			||||||
import 'package:flutter/material.dart';
 | 
					import 'package:flutter/material.dart';
 | 
				
			||||||
import 'package:flutter/services.dart';
 | 
					import 'package:flutter/services.dart';
 | 
				
			||||||
 | 
					import 'package:obtainium/components/generated_form.dart';
 | 
				
			||||||
import 'package:obtainium/components/generated_form_modal.dart';
 | 
					import 'package:obtainium/components/generated_form_modal.dart';
 | 
				
			||||||
import 'package:obtainium/custom_errors.dart';
 | 
					import 'package:obtainium/custom_errors.dart';
 | 
				
			||||||
import 'package:obtainium/main.dart';
 | 
					import 'package:obtainium/main.dart';
 | 
				
			||||||
@@ -33,6 +34,7 @@ class _AppPageState extends State<AppPage> {
 | 
				
			|||||||
      });
 | 
					      });
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    var categories = settingsProvider.categories;
 | 
				
			||||||
    var sourceProvider = SourceProvider();
 | 
					    var sourceProvider = SourceProvider();
 | 
				
			||||||
    AppInMemory? app = appsProvider.apps[widget.appId];
 | 
					    AppInMemory? app = appsProvider.apps[widget.appId];
 | 
				
			||||||
    var source = app != null ? sourceProvider.getSource(app.app.url) : null;
 | 
					    var source = app != null ? sourceProvider.getSource(app.app.url) : null;
 | 
				
			||||||
@@ -148,7 +150,51 @@ class _AppPageState extends State<AppPage> {
 | 
				
			|||||||
                          textAlign: TextAlign.center,
 | 
					                          textAlign: TextAlign.center,
 | 
				
			||||||
                          style: const TextStyle(
 | 
					                          style: const TextStyle(
 | 
				
			||||||
                              fontStyle: FontStyle.italic, fontSize: 12),
 | 
					                              fontStyle: FontStyle.italic, fontSize: 12),
 | 
				
			||||||
                        )
 | 
					                        ),
 | 
				
			||||||
 | 
					                        const SizedBox(
 | 
				
			||||||
 | 
					                          height: 32,
 | 
				
			||||||
 | 
					                        ),
 | 
				
			||||||
 | 
					                        app?.app.category != null
 | 
				
			||||||
 | 
					                            ? Chip(
 | 
				
			||||||
 | 
					                                label: Text(app!.app.category!),
 | 
				
			||||||
 | 
					                                backgroundColor:
 | 
				
			||||||
 | 
					                                    Color(categories[app.app.category!] ?? 0x0),
 | 
				
			||||||
 | 
					                                onDeleted: () {
 | 
				
			||||||
 | 
					                                  app.app.category = null;
 | 
				
			||||||
 | 
					                                  appsProvider.saveApps([app.app]);
 | 
				
			||||||
 | 
					                                },
 | 
				
			||||||
 | 
					                                visualDensity: VisualDensity.compact,
 | 
				
			||||||
 | 
					                              )
 | 
				
			||||||
 | 
					                            : Row(
 | 
				
			||||||
 | 
					                                mainAxisAlignment: MainAxisAlignment.center,
 | 
				
			||||||
 | 
					                                children: [
 | 
				
			||||||
 | 
					                                    TextButton(
 | 
				
			||||||
 | 
					                                        onPressed: () {
 | 
				
			||||||
 | 
					                                          showDialog<Map<String, String>?>(
 | 
				
			||||||
 | 
					                                              context: context,
 | 
				
			||||||
 | 
					                                              builder: (BuildContext ctx) {
 | 
				
			||||||
 | 
					                                                return GeneratedFormModal(
 | 
				
			||||||
 | 
					                                                    title: 'Pick a Category',
 | 
				
			||||||
 | 
					                                                    items: [
 | 
				
			||||||
 | 
					                                                      [
 | 
				
			||||||
 | 
					                                                        settingsProvider
 | 
				
			||||||
 | 
					                                                            .getCategoryFormItem()
 | 
				
			||||||
 | 
					                                                      ]
 | 
				
			||||||
 | 
					                                                    ]);
 | 
				
			||||||
 | 
					                                              }).then((value) {
 | 
				
			||||||
 | 
					                                            if (value != null && app != null) {
 | 
				
			||||||
 | 
					                                              String? cat = (value['category']
 | 
				
			||||||
 | 
					                                                          ?.isNotEmpty ??
 | 
				
			||||||
 | 
					                                                      false)
 | 
				
			||||||
 | 
					                                                  ? value['category']
 | 
				
			||||||
 | 
					                                                  : null;
 | 
				
			||||||
 | 
					                                              app.app.category = cat;
 | 
				
			||||||
 | 
					                                              appsProvider.saveApps([app.app]);
 | 
				
			||||||
 | 
					                                            }
 | 
				
			||||||
 | 
					                                          });
 | 
				
			||||||
 | 
					                                        },
 | 
				
			||||||
 | 
					                                        child: Text(tr('categorize')))
 | 
				
			||||||
 | 
					                                  ])
 | 
				
			||||||
                      ],
 | 
					                      ],
 | 
				
			||||||
                    )),
 | 
					                    )),
 | 
				
			||||||
                  ],
 | 
					                  ],
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -80,28 +80,31 @@ class AppsPageState extends State<AppsPage> {
 | 
				
			|||||||
            !(filter!.includeNonInstalled)) {
 | 
					            !(filter!.includeNonInstalled)) {
 | 
				
			||||||
          return false;
 | 
					          return false;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        if (filter!.nameFilter.isEmpty && filter!.authorFilter.isEmpty) {
 | 
					        if (filter!.nameFilter.isNotEmpty || filter!.authorFilter.isNotEmpty) {
 | 
				
			||||||
          return true;
 | 
					          List<String> nameTokens = filter!.nameFilter
 | 
				
			||||||
        }
 | 
					              .split(' ')
 | 
				
			||||||
        List<String> nameTokens = filter!.nameFilter
 | 
					              .where((element) => element.trim().isNotEmpty)
 | 
				
			||||||
            .split(' ')
 | 
					              .toList();
 | 
				
			||||||
            .where((element) => element.trim().isNotEmpty)
 | 
					          List<String> authorTokens = filter!.authorFilter
 | 
				
			||||||
            .toList();
 | 
					              .split(' ')
 | 
				
			||||||
        List<String> authorTokens = filter!.authorFilter
 | 
					              .where((element) => element.trim().isNotEmpty)
 | 
				
			||||||
            .split(' ')
 | 
					              .toList();
 | 
				
			||||||
            .where((element) => element.trim().isNotEmpty)
 | 
					 | 
				
			||||||
            .toList();
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        for (var t in nameTokens) {
 | 
					          for (var t in nameTokens) {
 | 
				
			||||||
          var name = app.installedInfo?.name ?? app.app.name;
 | 
					            var name = app.installedInfo?.name ?? app.app.name;
 | 
				
			||||||
          if (!name.toLowerCase().contains(t.toLowerCase())) {
 | 
					            if (!name.toLowerCase().contains(t.toLowerCase())) {
 | 
				
			||||||
            return false;
 | 
					              return false;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					          for (var t in authorTokens) {
 | 
				
			||||||
 | 
					            if (!app.app.author.toLowerCase().contains(t.toLowerCase())) {
 | 
				
			||||||
 | 
					              return false;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
          }
 | 
					          }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        for (var t in authorTokens) {
 | 
					        if (filter!.categoryFilter.isNotEmpty &&
 | 
				
			||||||
          if (!app.app.author.toLowerCase().contains(t.toLowerCase())) {
 | 
					            filter!.categoryFilter != app.app.category) {
 | 
				
			||||||
            return false;
 | 
					          return false;
 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        return true;
 | 
					        return true;
 | 
				
			||||||
      }).toList();
 | 
					      }).toList();
 | 
				
			||||||
@@ -225,96 +228,114 @@ class AppsPageState extends State<AppsPage> {
 | 
				
			|||||||
              String? changesUrl = SourceProvider()
 | 
					              String? changesUrl = SourceProvider()
 | 
				
			||||||
                  .getSource(sortedApps[index].app.url)
 | 
					                  .getSource(sortedApps[index].app.url)
 | 
				
			||||||
                  .changeLogPageFromStandardUrl(sortedApps[index].app.url);
 | 
					                  .changeLogPageFromStandardUrl(sortedApps[index].app.url);
 | 
				
			||||||
              return ListTile(
 | 
					              return Container(
 | 
				
			||||||
                tileColor: sortedApps[index].app.pinned
 | 
					                  decoration: BoxDecoration(
 | 
				
			||||||
                    ? Colors.grey.withOpacity(0.1)
 | 
					                      border: Border.symmetric(
 | 
				
			||||||
                    : Colors.transparent,
 | 
					                          vertical: BorderSide(
 | 
				
			||||||
                selectedTileColor: Theme.of(context)
 | 
					                              width: 3,
 | 
				
			||||||
                    .colorScheme
 | 
					                              color: Color(settingsProvider.categories[
 | 
				
			||||||
                    .primary
 | 
					                                      sortedApps[index].app.category] ??
 | 
				
			||||||
                    .withOpacity(sortedApps[index].app.pinned ? 0.2 : 0.1),
 | 
					                                  const Color.fromARGB(0, 0, 0, 0).value)))),
 | 
				
			||||||
                selected: selectedApps.contains(sortedApps[index].app),
 | 
					                  child: ListTile(
 | 
				
			||||||
                onLongPress: () {
 | 
					                    tileColor: sortedApps[index].app.pinned
 | 
				
			||||||
                  toggleAppSelected(sortedApps[index].app);
 | 
					                        ? Colors.grey.withOpacity(0.1)
 | 
				
			||||||
                },
 | 
					                        : Colors.transparent,
 | 
				
			||||||
                leading: sortedApps[index].installedInfo != null
 | 
					                    selectedTileColor: Theme.of(context)
 | 
				
			||||||
                    ? Image.memory(
 | 
					                        .colorScheme
 | 
				
			||||||
                        sortedApps[index].installedInfo!.icon!,
 | 
					                        .primary
 | 
				
			||||||
                        gaplessPlayback: true,
 | 
					                        .withOpacity(sortedApps[index].app.pinned ? 0.2 : 0.1),
 | 
				
			||||||
                      )
 | 
					                    selected: selectedApps.contains(sortedApps[index].app),
 | 
				
			||||||
                    : null,
 | 
					                    onLongPress: () {
 | 
				
			||||||
                title: Text(
 | 
					                      toggleAppSelected(sortedApps[index].app);
 | 
				
			||||||
                  sortedApps[index].installedInfo?.name ??
 | 
					                    },
 | 
				
			||||||
                      sortedApps[index].app.name,
 | 
					                    leading: sortedApps[index].installedInfo != null
 | 
				
			||||||
                  style: TextStyle(
 | 
					                        ? Image.memory(
 | 
				
			||||||
                      fontWeight: sortedApps[index].app.pinned
 | 
					                            sortedApps[index].installedInfo!.icon!,
 | 
				
			||||||
                          ? FontWeight.bold
 | 
					                            gaplessPlayback: true,
 | 
				
			||||||
                          : FontWeight.normal),
 | 
					                          )
 | 
				
			||||||
                ),
 | 
					                        : null,
 | 
				
			||||||
                subtitle: Text(tr('byX', args: [sortedApps[index].app.author]),
 | 
					                    title: Text(
 | 
				
			||||||
                    style: TextStyle(
 | 
					                      sortedApps[index].installedInfo?.name ??
 | 
				
			||||||
 | 
					                          sortedApps[index].app.name,
 | 
				
			||||||
 | 
					                      style: TextStyle(
 | 
				
			||||||
                        fontWeight: sortedApps[index].app.pinned
 | 
					                        fontWeight: sortedApps[index].app.pinned
 | 
				
			||||||
                            ? FontWeight.bold
 | 
					                            ? FontWeight.bold
 | 
				
			||||||
                            : FontWeight.normal)),
 | 
					                            : FontWeight.normal,
 | 
				
			||||||
                trailing: SingleChildScrollView(
 | 
					                      ),
 | 
				
			||||||
                    reverse: true,
 | 
					                    ),
 | 
				
			||||||
                    child: sortedApps[index].downloadProgress != null
 | 
					                    subtitle: Text(
 | 
				
			||||||
                        ? Text(tr('percentProgress', args: [
 | 
					                        tr('byX', args: [sortedApps[index].app.author]),
 | 
				
			||||||
                            sortedApps[index]
 | 
					                        style: TextStyle(
 | 
				
			||||||
                                    .downloadProgress
 | 
					                            fontWeight: sortedApps[index].app.pinned
 | 
				
			||||||
                                    ?.toInt()
 | 
					                                ? FontWeight.bold
 | 
				
			||||||
                                    .toString() ??
 | 
					                                : FontWeight.normal)),
 | 
				
			||||||
                                '100'
 | 
					                    trailing: SingleChildScrollView(
 | 
				
			||||||
                          ]))
 | 
					                        reverse: true,
 | 
				
			||||||
                        : (Column(
 | 
					                        child: sortedApps[index].downloadProgress != null
 | 
				
			||||||
                            mainAxisAlignment: MainAxisAlignment.center,
 | 
					                            ? Text(tr('percentProgress', args: [
 | 
				
			||||||
                            crossAxisAlignment: CrossAxisAlignment.end,
 | 
					                                sortedApps[index]
 | 
				
			||||||
                            children: [
 | 
					                                        .downloadProgress
 | 
				
			||||||
                              SizedBox(
 | 
					                                        ?.toInt()
 | 
				
			||||||
                                  width: 100,
 | 
					                                        .toString() ??
 | 
				
			||||||
                                  child: Text(
 | 
					                                    '100'
 | 
				
			||||||
                                    '${sortedApps[index].app.installedVersion ?? tr('notInstalled')}${sortedApps[index].app.additionalSettings['trackOnly'] == 'true' ? ' ${tr('estimateInBrackets')}' : ''}',
 | 
					                              ]))
 | 
				
			||||||
                                    overflow: TextOverflow.fade,
 | 
					                            : (Column(
 | 
				
			||||||
                                    textAlign: TextAlign.end,
 | 
					                                mainAxisAlignment: MainAxisAlignment.center,
 | 
				
			||||||
                                  )),
 | 
					                                crossAxisAlignment: CrossAxisAlignment.end,
 | 
				
			||||||
                              sortedApps[index].app.installedVersion != null &&
 | 
					                                children: [
 | 
				
			||||||
                                      sortedApps[index].app.installedVersion !=
 | 
					                                  SizedBox(
 | 
				
			||||||
                                          sortedApps[index].app.latestVersion
 | 
					                                      width: 100,
 | 
				
			||||||
                                  ? GestureDetector(
 | 
					                                      child: Text(
 | 
				
			||||||
                                      onTap: changesUrl == null
 | 
					                                        '${sortedApps[index].app.installedVersion ?? tr('notInstalled')}${sortedApps[index].app.additionalSettings['trackOnly'] == 'true' ? ' ${tr('estimateInBrackets')}' : ''}',
 | 
				
			||||||
                                          ? null
 | 
					                                        overflow: TextOverflow.fade,
 | 
				
			||||||
                                          : () {
 | 
					                                        textAlign: TextAlign.end,
 | 
				
			||||||
                                              launchUrlString(changesUrl,
 | 
					                                      )),
 | 
				
			||||||
                                                  mode: LaunchMode
 | 
					                                  sortedApps[index].app.installedVersion !=
 | 
				
			||||||
                                                      .externalApplication);
 | 
					                                              null &&
 | 
				
			||||||
                                            },
 | 
					                                          sortedApps[index]
 | 
				
			||||||
                                      child: appsProvider.areDownloadsRunning()
 | 
					                                                  .app
 | 
				
			||||||
                                          ? Text(tr('pleaseWait'))
 | 
					                                                  .installedVersion !=
 | 
				
			||||||
                                          : Text(
 | 
					                                              sortedApps[index]
 | 
				
			||||||
                                              '${tr('updateAvailable')}${sortedApps[index].app.additionalSettings['trackOnly'] == 'true' ? ' ${tr('estimateInBracketsShort')}' : ''}',
 | 
					                                                  .app
 | 
				
			||||||
                                              style: TextStyle(
 | 
					                                                  .latestVersion
 | 
				
			||||||
                                                  fontStyle: FontStyle.italic,
 | 
					                                      ? GestureDetector(
 | 
				
			||||||
                                                  decoration: changesUrl == null
 | 
					                                          onTap: changesUrl == null
 | 
				
			||||||
                                                      ? TextDecoration.none
 | 
					                                              ? null
 | 
				
			||||||
                                                      : TextDecoration
 | 
					                                              : () {
 | 
				
			||||||
                                                          .underline),
 | 
					                                                  launchUrlString(changesUrl,
 | 
				
			||||||
                                            ))
 | 
					                                                      mode: LaunchMode
 | 
				
			||||||
                                  : const SizedBox(),
 | 
					                                                          .externalApplication);
 | 
				
			||||||
                            ],
 | 
					                                                },
 | 
				
			||||||
                          ))),
 | 
					                                          child: appsProvider
 | 
				
			||||||
                onTap: () {
 | 
					                                                  .areDownloadsRunning()
 | 
				
			||||||
                  if (selectedApps.isNotEmpty) {
 | 
					                                              ? Text(tr('pleaseWait'))
 | 
				
			||||||
                    toggleAppSelected(sortedApps[index].app);
 | 
					                                              : Text(
 | 
				
			||||||
                  } else {
 | 
					                                                  '${tr('updateAvailable')}${sortedApps[index].app.additionalSettings['trackOnly'] == 'true' ? ' ${tr('estimateInBracketsShort')}' : ''}',
 | 
				
			||||||
                    Navigator.push(
 | 
					                                                  style: TextStyle(
 | 
				
			||||||
                      context,
 | 
					                                                      fontStyle:
 | 
				
			||||||
                      MaterialPageRoute(
 | 
					                                                          FontStyle.italic,
 | 
				
			||||||
                          builder: (context) =>
 | 
					                                                      decoration: changesUrl ==
 | 
				
			||||||
                              AppPage(appId: sortedApps[index].app.id)),
 | 
					                                                              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))
 | 
					            }, childCount: sortedApps.length))
 | 
				
			||||||
          ])),
 | 
					          ])),
 | 
				
			||||||
      persistentFooterButtons: [
 | 
					      persistentFooterButtons: [
 | 
				
			||||||
@@ -708,6 +729,10 @@ class AppsPageState extends State<AppsPage> {
 | 
				
			|||||||
                                        label: tr('nonInstalledApps'),
 | 
					                                        label: tr('nonInstalledApps'),
 | 
				
			||||||
                                        type: FormItemType.bool,
 | 
					                                        type: FormItemType.bool,
 | 
				
			||||||
                                        defaultValue: vals['nonInstalledApps'])
 | 
					                                        defaultValue: vals['nonInstalledApps'])
 | 
				
			||||||
 | 
					                                  ],
 | 
				
			||||||
 | 
					                                  [
 | 
				
			||||||
 | 
					                                    settingsProvider.getCategoryFormItem(
 | 
				
			||||||
 | 
					                                        initCategory: vals['category'] ?? '')
 | 
				
			||||||
                                  ]
 | 
					                                  ]
 | 
				
			||||||
                                ]);
 | 
					                                ]);
 | 
				
			||||||
                          }).then((values) {
 | 
					                          }).then((values) {
 | 
				
			||||||
@@ -734,19 +759,22 @@ class AppsFilter {
 | 
				
			|||||||
  late String authorFilter;
 | 
					  late String authorFilter;
 | 
				
			||||||
  late bool includeUptodate;
 | 
					  late bool includeUptodate;
 | 
				
			||||||
  late bool includeNonInstalled;
 | 
					  late bool includeNonInstalled;
 | 
				
			||||||
 | 
					  late String categoryFilter;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  AppsFilter(
 | 
					  AppsFilter(
 | 
				
			||||||
      {this.nameFilter = '',
 | 
					      {this.nameFilter = '',
 | 
				
			||||||
      this.authorFilter = '',
 | 
					      this.authorFilter = '',
 | 
				
			||||||
      this.includeUptodate = true,
 | 
					      this.includeUptodate = true,
 | 
				
			||||||
      this.includeNonInstalled = true});
 | 
					      this.includeNonInstalled = true,
 | 
				
			||||||
 | 
					      this.categoryFilter = ''});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  Map<String, String> toValuesMap() {
 | 
					  Map<String, String> toValuesMap() {
 | 
				
			||||||
    return {
 | 
					    return {
 | 
				
			||||||
      'appName': nameFilter,
 | 
					      'appName': nameFilter,
 | 
				
			||||||
      'author': authorFilter,
 | 
					      'author': authorFilter,
 | 
				
			||||||
      'upToDateApps': includeUptodate ? 'true' : '',
 | 
					      'upToDateApps': includeUptodate ? 'true' : '',
 | 
				
			||||||
      'nonInstalledApps': includeNonInstalled ? 'true' : ''
 | 
					      'nonInstalledApps': includeNonInstalled ? 'true' : '',
 | 
				
			||||||
 | 
					      'category': categoryFilter
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -755,11 +783,13 @@ class AppsFilter {
 | 
				
			|||||||
    authorFilter = values['author']!;
 | 
					    authorFilter = values['author']!;
 | 
				
			||||||
    includeUptodate = values['upToDateApps'] == 'true';
 | 
					    includeUptodate = values['upToDateApps'] == 'true';
 | 
				
			||||||
    includeNonInstalled = values['nonInstalledApps'] == 'true';
 | 
					    includeNonInstalled = values['nonInstalledApps'] == 'true';
 | 
				
			||||||
 | 
					    categoryFilter = values['category']!;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  bool isIdenticalTo(AppsFilter other) =>
 | 
					  bool isIdenticalTo(AppsFilter other) =>
 | 
				
			||||||
      authorFilter.trim() == other.authorFilter.trim() &&
 | 
					      authorFilter.trim() == other.authorFilter.trim() &&
 | 
				
			||||||
      nameFilter.trim() == other.nameFilter.trim() &&
 | 
					      nameFilter.trim() == other.nameFilter.trim() &&
 | 
				
			||||||
      includeUptodate == other.includeUptodate &&
 | 
					      includeUptodate == other.includeUptodate &&
 | 
				
			||||||
      includeNonInstalled == other.includeNonInstalled;
 | 
					      includeNonInstalled == other.includeNonInstalled &&
 | 
				
			||||||
 | 
					      categoryFilter.trim() == other.categoryFilter.trim();
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,8 +1,12 @@
 | 
				
			|||||||
 | 
					import 'dart:math';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import 'package:easy_localization/easy_localization.dart';
 | 
					import 'package:easy_localization/easy_localization.dart';
 | 
				
			||||||
import 'package:flutter/material.dart';
 | 
					import 'package:flutter/material.dart';
 | 
				
			||||||
import 'package:obtainium/components/custom_app_bar.dart';
 | 
					import 'package:obtainium/components/custom_app_bar.dart';
 | 
				
			||||||
import 'package:obtainium/components/generated_form.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/custom_errors.dart';
 | 
				
			||||||
 | 
					import 'package:obtainium/providers/apps_provider.dart';
 | 
				
			||||||
import 'package:obtainium/providers/logs_provider.dart';
 | 
					import 'package:obtainium/providers/logs_provider.dart';
 | 
				
			||||||
import 'package:obtainium/providers/settings_provider.dart';
 | 
					import 'package:obtainium/providers/settings_provider.dart';
 | 
				
			||||||
import 'package:obtainium/providers/source_provider.dart';
 | 
					import 'package:obtainium/providers/source_provider.dart';
 | 
				
			||||||
@@ -17,11 +21,27 @@ class SettingsPage extends StatefulWidget {
 | 
				
			|||||||
  State<SettingsPage> createState() => _SettingsPageState();
 | 
					  State<SettingsPage> createState() => _SettingsPageState();
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Generates a random light color
 | 
				
			||||||
 | 
					// Courtesy of ChatGPT 😭 (with a bugfix 🥳)
 | 
				
			||||||
 | 
					Color generateRandomLightColor() {
 | 
				
			||||||
 | 
					  // Create a random number generator
 | 
				
			||||||
 | 
					  final Random random = Random();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Generate random hue, saturation, and value values
 | 
				
			||||||
 | 
					  final double hue = random.nextDouble() * 360;
 | 
				
			||||||
 | 
					  final double saturation = 0.5 + random.nextDouble() * 0.5;
 | 
				
			||||||
 | 
					  final double value = 0.9 + random.nextDouble() * 0.1;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Create a HSV color with the random values
 | 
				
			||||||
 | 
					  return HSVColor.fromAHSV(1.0, hue, saturation, value).toColor();
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class _SettingsPageState extends State<SettingsPage> {
 | 
					class _SettingsPageState extends State<SettingsPage> {
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  Widget build(BuildContext context) {
 | 
					  Widget build(BuildContext context) {
 | 
				
			||||||
    SettingsProvider settingsProvider = context.watch<SettingsProvider>();
 | 
					    SettingsProvider settingsProvider = context.watch<SettingsProvider>();
 | 
				
			||||||
    SourceProvider sourceProvider = SourceProvider();
 | 
					    SourceProvider sourceProvider = SourceProvider();
 | 
				
			||||||
 | 
					    AppsProvider appsProvider = context.read<AppsProvider>();
 | 
				
			||||||
    if (settingsProvider.prefs == null) {
 | 
					    if (settingsProvider.prefs == null) {
 | 
				
			||||||
      settingsProvider.initializeSettings();
 | 
					      settingsProvider.initializeSettings();
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@@ -157,6 +177,8 @@ class _SettingsPageState extends State<SettingsPage> {
 | 
				
			|||||||
      height: 16,
 | 
					      height: 16,
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    var categories = settingsProvider.categories;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return Scaffold(
 | 
					    return Scaffold(
 | 
				
			||||||
        backgroundColor: Theme.of(context).colorScheme.surface,
 | 
					        backgroundColor: Theme.of(context).colorScheme.surface,
 | 
				
			||||||
        body: CustomScrollView(slivers: <Widget>[
 | 
					        body: CustomScrollView(slivers: <Widget>[
 | 
				
			||||||
@@ -232,6 +254,94 @@ class _SettingsPageState extends State<SettingsPage> {
 | 
				
			|||||||
                                  color: Theme.of(context).colorScheme.primary),
 | 
					                                  color: Theme.of(context).colorScheme.primary),
 | 
				
			||||||
                            ),
 | 
					                            ),
 | 
				
			||||||
                            ...sourceSpecificFields,
 | 
					                            ...sourceSpecificFields,
 | 
				
			||||||
 | 
					                            intervalDropdown,
 | 
				
			||||||
 | 
					                            const Divider(
 | 
				
			||||||
 | 
					                              height: 48,
 | 
				
			||||||
 | 
					                            ),
 | 
				
			||||||
 | 
					                            Text(
 | 
				
			||||||
 | 
					                              tr('categories'),
 | 
				
			||||||
 | 
					                              style: TextStyle(
 | 
				
			||||||
 | 
					                                  color: Theme.of(context).colorScheme.primary),
 | 
				
			||||||
 | 
					                            ),
 | 
				
			||||||
 | 
					                            height16,
 | 
				
			||||||
 | 
					                            Wrap(
 | 
				
			||||||
 | 
					                              children: [
 | 
				
			||||||
 | 
					                                ...categories.entries.toList().map((e) {
 | 
				
			||||||
 | 
					                                  return Padding(
 | 
				
			||||||
 | 
					                                      padding: const EdgeInsets.symmetric(
 | 
				
			||||||
 | 
					                                          horizontal: 4),
 | 
				
			||||||
 | 
					                                      child: Chip(
 | 
				
			||||||
 | 
					                                        label: Text(e.key),
 | 
				
			||||||
 | 
					                                        backgroundColor: Color(e.value),
 | 
				
			||||||
 | 
					                                        visualDensity: VisualDensity.compact,
 | 
				
			||||||
 | 
					                                        onDeleted: () {
 | 
				
			||||||
 | 
					                                          showDialog<Map<String, String>?>(
 | 
				
			||||||
 | 
					                                              context: context,
 | 
				
			||||||
 | 
					                                              builder: (BuildContext ctx) {
 | 
				
			||||||
 | 
					                                                return GeneratedFormModal(
 | 
				
			||||||
 | 
					                                                    title: tr(
 | 
				
			||||||
 | 
					                                                        'deleteCategoryQuestion'),
 | 
				
			||||||
 | 
					                                                    message: tr(
 | 
				
			||||||
 | 
					                                                        'categoryDeleteWarning',
 | 
				
			||||||
 | 
					                                                        args: [e.key]),
 | 
				
			||||||
 | 
					                                                    items: []);
 | 
				
			||||||
 | 
					                                              }).then((value) {
 | 
				
			||||||
 | 
					                                            if (value != null) {
 | 
				
			||||||
 | 
					                                              setState(() {
 | 
				
			||||||
 | 
					                                                categories.remove(e.key);
 | 
				
			||||||
 | 
					                                                settingsProvider.categories =
 | 
				
			||||||
 | 
					                                                    categories;
 | 
				
			||||||
 | 
					                                              });
 | 
				
			||||||
 | 
					                                              appsProvider.saveApps(appsProvider
 | 
				
			||||||
 | 
					                                                  .apps.values
 | 
				
			||||||
 | 
					                                                  .where((element) =>
 | 
				
			||||||
 | 
					                                                      element.app.category ==
 | 
				
			||||||
 | 
					                                                      e.key)
 | 
				
			||||||
 | 
					                                                  .map((e) {
 | 
				
			||||||
 | 
					                                                var a = e.app;
 | 
				
			||||||
 | 
					                                                a.category = null;
 | 
				
			||||||
 | 
					                                                return a;
 | 
				
			||||||
 | 
					                                              }).toList());
 | 
				
			||||||
 | 
					                                            }
 | 
				
			||||||
 | 
					                                          });
 | 
				
			||||||
 | 
					                                        },
 | 
				
			||||||
 | 
					                                      ));
 | 
				
			||||||
 | 
					                                }),
 | 
				
			||||||
 | 
					                                Padding(
 | 
				
			||||||
 | 
					                                    padding: const EdgeInsets.symmetric(
 | 
				
			||||||
 | 
					                                        horizontal: 4),
 | 
				
			||||||
 | 
					                                    child: IconButton(
 | 
				
			||||||
 | 
					                                      onPressed: () {
 | 
				
			||||||
 | 
					                                        showDialog<Map<String, String>?>(
 | 
				
			||||||
 | 
					                                            context: context,
 | 
				
			||||||
 | 
					                                            builder: (BuildContext ctx) {
 | 
				
			||||||
 | 
					                                              return GeneratedFormModal(
 | 
				
			||||||
 | 
					                                                  title: tr('addCategory'),
 | 
				
			||||||
 | 
					                                                  items: [
 | 
				
			||||||
 | 
					                                                    [
 | 
				
			||||||
 | 
					                                                      GeneratedFormItem('label',
 | 
				
			||||||
 | 
					                                                          label: tr('label'))
 | 
				
			||||||
 | 
					                                                    ]
 | 
				
			||||||
 | 
					                                                  ]);
 | 
				
			||||||
 | 
					                                            }).then((value) {
 | 
				
			||||||
 | 
					                                          String? label = value?['label'];
 | 
				
			||||||
 | 
					                                          if (label != null) {
 | 
				
			||||||
 | 
					                                            setState(() {
 | 
				
			||||||
 | 
					                                              categories[label] =
 | 
				
			||||||
 | 
					                                                  generateRandomLightColor()
 | 
				
			||||||
 | 
					                                                      .value;
 | 
				
			||||||
 | 
					                                              settingsProvider.categories =
 | 
				
			||||||
 | 
					                                                  categories;
 | 
				
			||||||
 | 
					                                            });
 | 
				
			||||||
 | 
					                                          }
 | 
				
			||||||
 | 
					                                        });
 | 
				
			||||||
 | 
					                                      },
 | 
				
			||||||
 | 
					                                      icon: const Icon(Icons.add),
 | 
				
			||||||
 | 
					                                      visualDensity: VisualDensity.compact,
 | 
				
			||||||
 | 
					                                      tooltip: tr('add'),
 | 
				
			||||||
 | 
					                                    ))
 | 
				
			||||||
 | 
					                              ],
 | 
				
			||||||
 | 
					                            )
 | 
				
			||||||
                          ],
 | 
					                          ],
 | 
				
			||||||
                        ))),
 | 
					                        ))),
 | 
				
			||||||
          SliverToBoxAdapter(
 | 
					          SliverToBoxAdapter(
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,9 +1,12 @@
 | 
				
			|||||||
// Exposes functions used to save/load app settings
 | 
					// Exposes functions used to save/load app settings
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import 'dart:convert';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import 'package:easy_localization/easy_localization.dart';
 | 
					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: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';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -144,4 +147,20 @@ class SettingsProvider with ChangeNotifier {
 | 
				
			|||||||
    prefs?.setString(settingId, value);
 | 
					    prefs?.setString(settingId, value);
 | 
				
			||||||
    notifyListeners();
 | 
					    notifyListeners();
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Map<String, int> get categories =>
 | 
				
			||||||
 | 
					      Map<String, int>.from(jsonDecode(prefs?.getString('categories') ?? '{}'));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  set categories(Map<String, int> cats) {
 | 
				
			||||||
 | 
					    prefs?.setString('categories', jsonEncode(cats));
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  getCategoryFormItem({String initCategory = ''}) =>
 | 
				
			||||||
 | 
					      GeneratedFormItem('category',
 | 
				
			||||||
 | 
					          label: tr('category'),
 | 
				
			||||||
 | 
					          opts: [
 | 
				
			||||||
 | 
					            MapEntry('', tr('noCategory')),
 | 
				
			||||||
 | 
					            ...categories.entries.map((e) => MapEntry(e.key, e.key)).toList()
 | 
				
			||||||
 | 
					          ],
 | 
				
			||||||
 | 
					          defaultValue: initCategory);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -19,6 +19,7 @@ import 'package:obtainium/app_sources/steammobile.dart';
 | 
				
			|||||||
import 'package:obtainium/components/generated_form.dart';
 | 
					import 'package:obtainium/components/generated_form.dart';
 | 
				
			||||||
import 'package:obtainium/custom_errors.dart';
 | 
					import 'package:obtainium/custom_errors.dart';
 | 
				
			||||||
import 'package:obtainium/mass_app_sources/githubstars.dart';
 | 
					import 'package:obtainium/mass_app_sources/githubstars.dart';
 | 
				
			||||||
 | 
					import 'package:obtainium/providers/settings_provider.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class AppNames {
 | 
					class AppNames {
 | 
				
			||||||
  late String author;
 | 
					  late String author;
 | 
				
			||||||
@@ -47,6 +48,7 @@ class App {
 | 
				
			|||||||
  late Map<String, String> additionalSettings;
 | 
					  late Map<String, String> additionalSettings;
 | 
				
			||||||
  late DateTime? lastUpdateCheck;
 | 
					  late DateTime? lastUpdateCheck;
 | 
				
			||||||
  bool pinned = false;
 | 
					  bool pinned = false;
 | 
				
			||||||
 | 
					  String? category;
 | 
				
			||||||
  App(
 | 
					  App(
 | 
				
			||||||
      this.id,
 | 
					      this.id,
 | 
				
			||||||
      this.url,
 | 
					      this.url,
 | 
				
			||||||
@@ -58,7 +60,8 @@ class App {
 | 
				
			|||||||
      this.preferredApkIndex,
 | 
					      this.preferredApkIndex,
 | 
				
			||||||
      this.additionalSettings,
 | 
					      this.additionalSettings,
 | 
				
			||||||
      this.lastUpdateCheck,
 | 
					      this.lastUpdateCheck,
 | 
				
			||||||
      this.pinned);
 | 
					      this.pinned,
 | 
				
			||||||
 | 
					      {this.category});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  String toString() {
 | 
					  String toString() {
 | 
				
			||||||
@@ -107,7 +110,8 @@ class App {
 | 
				
			|||||||
        json['lastUpdateCheck'] == null
 | 
					        json['lastUpdateCheck'] == null
 | 
				
			||||||
            ? null
 | 
					            ? null
 | 
				
			||||||
            : DateTime.fromMicrosecondsSinceEpoch(json['lastUpdateCheck']),
 | 
					            : DateTime.fromMicrosecondsSinceEpoch(json['lastUpdateCheck']),
 | 
				
			||||||
        json['pinned'] ?? false);
 | 
					        json['pinned'] ?? false,
 | 
				
			||||||
 | 
					        category: json['category']);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  Map<String, dynamic> toJson() => {
 | 
					  Map<String, dynamic> toJson() => {
 | 
				
			||||||
@@ -121,7 +125,8 @@ class App {
 | 
				
			|||||||
        'preferredApkIndex': preferredApkIndex,
 | 
					        'preferredApkIndex': preferredApkIndex,
 | 
				
			||||||
        'additionalSettings': jsonEncode(additionalSettings),
 | 
					        'additionalSettings': jsonEncode(additionalSettings),
 | 
				
			||||||
        'lastUpdateCheck': lastUpdateCheck?.microsecondsSinceEpoch,
 | 
					        'lastUpdateCheck': lastUpdateCheck?.microsecondsSinceEpoch,
 | 
				
			||||||
        'pinned': pinned
 | 
					        'pinned': pinned,
 | 
				
			||||||
 | 
					        'category': category
 | 
				
			||||||
      };
 | 
					      };
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -350,7 +355,8 @@ class SourceProvider {
 | 
				
			|||||||
        apk.apkUrls.length - 1,
 | 
					        apk.apkUrls.length - 1,
 | 
				
			||||||
        additionalSettings,
 | 
					        additionalSettings,
 | 
				
			||||||
        DateTime.now(),
 | 
					        DateTime.now(),
 | 
				
			||||||
        currentApp?.pinned ?? false);
 | 
					        currentApp?.pinned ?? false,
 | 
				
			||||||
 | 
					        category: currentApp?.category);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // Returns errors in [results, errors] instead of throwing them
 | 
					  // Returns errors in [results, errors] instead of throwing them
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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.8.23+87 # When changing this, update the tag in main() accordingly
 | 
					version: 0.9.0+88 # 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'
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user