mirror of
				https://github.com/ImranR98/Obtainium.git
				synced 2025-11-04 07:13:28 +01:00 
			
		
		
		
	Compare commits
	
		
			22 Commits
		
	
	
		
			v0.11.17-b
			...
			v0.11.24-b
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					361a3e1bc2 | ||
| 
						 | 
					f33a26d4f4 | ||
| 
						 | 
					7aaf56ec8c | ||
| 
						 | 
					ed120016d9 | ||
| 
						 | 
					e8cbac8657 | ||
| 
						 | 
					b66c13d319 | ||
| 
						 | 
					782d055bc3 | ||
| 
						 | 
					d557746965 | ||
| 
						 | 
					e6b05d50b9 | ||
| 
						 | 
					dea635fa6a | ||
| 
						 | 
					682026ed0a | ||
| 
						 | 
					9fe8a200ef | ||
| 
						 | 
					210100da2b | ||
| 
						 | 
					d52660235b | ||
| 
						 | 
					e386b5ab8a | ||
| 
						 | 
					abf7be222d | ||
| 
						 | 
					4c5b9304c0 | ||
| 
						 | 
					4cfe6af044 | ||
| 
						 | 
					3f0c4068dd | ||
| 
						 | 
					7981ca29c5 | ||
| 
						 | 
					187efa8fc5 | ||
| 
						 | 
					cd27ff7f2d | 
@@ -207,6 +207,7 @@
 | 
			
		||||
    "addCategory": "Kategorie hinzufügen",
 | 
			
		||||
    "label": "Bezeichnung",
 | 
			
		||||
    "language": "Sprache",
 | 
			
		||||
    "copiedToClipboard": "Copied to Clipboard",
 | 
			
		||||
    "storagePermissionDenied": "Speicherberechtigung verweigert",
 | 
			
		||||
    "selectedCategorizeWarning": "Dadurch werden alle bestehenden Kategorieeinstellungen für die ausgewählten Apps ersetzt.",
 | 
			
		||||
    "filterAPKsByRegEx": "APKs nach regulärem Ausdruck filtern",
 | 
			
		||||
 
 | 
			
		||||
@@ -207,6 +207,7 @@
 | 
			
		||||
    "addCategory": "Add Category",
 | 
			
		||||
    "label": "Label",
 | 
			
		||||
    "language": "Language",
 | 
			
		||||
    "copiedToClipboard": "Copied to Clipboard",
 | 
			
		||||
    "storagePermissionDenied": "Storage permission denied",
 | 
			
		||||
    "selectedCategorizeWarning": "This will replace any existing category settings for the selected Apps.",
 | 
			
		||||
    "filterAPKsByRegEx": "Filter APKs by Regular Expression",
 | 
			
		||||
 
 | 
			
		||||
@@ -207,6 +207,7 @@
 | 
			
		||||
    "addCategory": "اضافه کردن دسته",
 | 
			
		||||
    "label": "برچسب",
 | 
			
		||||
    "language": "زبان",
 | 
			
		||||
    "copiedToClipboard": "Copied to Clipboard",
 | 
			
		||||
    "storagePermissionDenied": "مجوز ذخیره سازی رد شد",
 | 
			
		||||
    "selectedCategorizeWarning": "این جایگزین تنظیمات دسته بندی موجود برای برنامه های انتخابی می شود.",
 | 
			
		||||
    "filterAPKsByRegEx": "فایلهای APK را با نظم فیلتر کنید",
 | 
			
		||||
 
 | 
			
		||||
@@ -207,6 +207,7 @@
 | 
			
		||||
    "addCategory": "Ajouter une catégorie",
 | 
			
		||||
    "label": "Étiquette",
 | 
			
		||||
    "language": "Langue",
 | 
			
		||||
    "copiedToClipboard": "Copied to Clipboard",
 | 
			
		||||
    "storagePermissionDenied": "Autorisation de stockage refusée",
 | 
			
		||||
    "selectedCategorizeWarning": "Cela remplacera tous les paramètres de catégorie existants pour les applications sélectionnées.",
 | 
			
		||||
    "filterAPKsByRegEx": "Filtrer les APK par expression régulière",
 | 
			
		||||
 
 | 
			
		||||
@@ -206,6 +206,7 @@
 | 
			
		||||
    "addCategory": "Új kategória",
 | 
			
		||||
    "label": "Címke",
 | 
			
		||||
    "language": "Nyelv",
 | 
			
		||||
    "copiedToClipboard": "Copied to Clipboard",
 | 
			
		||||
    "storagePermissionDenied": "Tárhely engedély megtagadva",
 | 
			
		||||
    "selectedCategorizeWarning": "Ez felváltja a kiválasztott alkalmazások meglévő kategória-beállításait.",
 | 
			
		||||
    "filterAPKsByRegEx": "Az APK-k szűrése reguláris kifejezéssel",
 | 
			
		||||
@@ -219,7 +220,7 @@
 | 
			
		||||
    "importFromURLsInFile": "Importálás fájlban található URL-ből (mint pl. OPML)",
 | 
			
		||||
    "versionDetection": "Verzió érzékelés",
 | 
			
		||||
    "standardVersionDetection": "Alapért. verzió érzékelés",
 | 
			
		||||
    "groupByCategory": "Group by Category",
 | 
			
		||||
    "groupByCategory": "Csoportosítás Kategória alapján",
 | 
			
		||||
    "removeAppQuestion": {
 | 
			
		||||
        "one": "Eltávolítja az alkalmazást?",
 | 
			
		||||
        "other": "Eltávolítja az alkalmazást?"
 | 
			
		||||
 
 | 
			
		||||
@@ -207,6 +207,7 @@
 | 
			
		||||
    "addCategory": "Aggiungi categoria",
 | 
			
		||||
    "label": "Etichetta",
 | 
			
		||||
    "language": "Lingua",
 | 
			
		||||
    "copiedToClipboard": "Copied to Clipboard",
 | 
			
		||||
    "storagePermissionDenied": "Accesso ai file non autorizzato",
 | 
			
		||||
    "selectedCategorizeWarning": "Ciò sostituirà le impostazioni di categoria esistenti per le App selezionate.",
 | 
			
		||||
    "filterAPKsByRegEx": "Filtra file APK con espressioni regolari",
 | 
			
		||||
 
 | 
			
		||||
@@ -207,6 +207,7 @@
 | 
			
		||||
    "addCategory": "カテゴリを追加",
 | 
			
		||||
    "label": "ラベル",
 | 
			
		||||
    "language": "言語",
 | 
			
		||||
    "copiedToClipboard": "クリップボードにコピーしました",
 | 
			
		||||
    "storagePermissionDenied": "ストレージ権限が拒否されました",
 | 
			
		||||
    "selectedCategorizeWarning": "これにより、選択したアプリの既存のカテゴリ設定がすべて置き換えられます。",
 | 
			
		||||
    "filterAPKsByRegEx": "正規表現でAPKを絞り込む",
 | 
			
		||||
 
 | 
			
		||||
@@ -208,6 +208,7 @@
 | 
			
		||||
    "addCategory": "添加类别",
 | 
			
		||||
    "label": "标签",
 | 
			
		||||
    "language": "语言",
 | 
			
		||||
    "copiedToClipboard": "Copied to Clipboard",
 | 
			
		||||
    "storagePermissionDenied": "存储权限已被拒绝",
 | 
			
		||||
    "selectedCategorizeWarning": "这将取代所选应用程序的任何现有类别",
 | 
			
		||||
    "filterAPKsByRegEx": "Filter APKs by Regular Expression",
 | 
			
		||||
 
 | 
			
		||||
@@ -14,12 +14,14 @@ class FDroid extends AppSource {
 | 
			
		||||
  @override
 | 
			
		||||
  String standardizeURL(String url) {
 | 
			
		||||
    RegExp standardUrlRegExB =
 | 
			
		||||
        RegExp('^https?://$host/+[^/]+/+packages/+[^/]+');
 | 
			
		||||
        RegExp('^https?://(cloudflare\\.)?$host/+[^/]+/+packages/+[^/]+');
 | 
			
		||||
    RegExpMatch? match = standardUrlRegExB.firstMatch(url.toLowerCase());
 | 
			
		||||
    if (match != null) {
 | 
			
		||||
      url = 'https://$host/packages/${Uri.parse(url).pathSegments.last}';
 | 
			
		||||
      url =
 | 
			
		||||
          'https://${Uri.parse(url.substring(0, match.end)).host}/packages/${Uri.parse(url).pathSegments.last}';
 | 
			
		||||
    }
 | 
			
		||||
    RegExp standardUrlRegExA = RegExp('^https?://$host/+packages/+[^/]+');
 | 
			
		||||
    RegExp standardUrlRegExA =
 | 
			
		||||
        RegExp('^https?://(cloudflare\\.)?$host/+packages/+[^/]+');
 | 
			
		||||
    match = standardUrlRegExA.firstMatch(url.toLowerCase());
 | 
			
		||||
    if (match == null) {
 | 
			
		||||
      throw InvalidURLError(name);
 | 
			
		||||
@@ -61,9 +63,10 @@ class FDroid extends AppSource {
 | 
			
		||||
    Map<String, dynamic> additionalSettings,
 | 
			
		||||
  ) async {
 | 
			
		||||
    String? appId = tryInferringAppId(standardUrl);
 | 
			
		||||
    String host = Uri.parse(standardUrl).host;
 | 
			
		||||
    return getAPKUrlsFromFDroidPackagesAPIResponse(
 | 
			
		||||
        await get(Uri.parse('https://f-droid.org/api/v1/packages/$appId')),
 | 
			
		||||
        'https://f-droid.org/repo/$appId',
 | 
			
		||||
        await get(Uri.parse('https://$host/api/v1/packages/$appId')),
 | 
			
		||||
        'https://$host/repo/$appId',
 | 
			
		||||
        standardUrl);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -185,9 +185,11 @@ class GitHub extends AppSource {
 | 
			
		||||
      Map<String, String> urlsWithDescriptions = {};
 | 
			
		||||
      for (var e in (jsonDecode(res.body)['items'] as List<dynamic>)) {
 | 
			
		||||
        urlsWithDescriptions.addAll({
 | 
			
		||||
          e['html_url'] as String: e['description'] != null
 | 
			
		||||
          e['html_url'] as String:
 | 
			
		||||
              ((e['archived'] == true ? '[ARCHIVED] ' : '') +
 | 
			
		||||
                  (e['description'] != null
 | 
			
		||||
                      ? e['description'] as String
 | 
			
		||||
              : tr('noDescription')
 | 
			
		||||
                      : tr('noDescription')))
 | 
			
		||||
        });
 | 
			
		||||
      }
 | 
			
		||||
      return urlsWithDescriptions;
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,6 @@
 | 
			
		||||
import 'package:html/parser.dart';
 | 
			
		||||
import 'package:http/http.dart';
 | 
			
		||||
import 'package:obtainium/app_sources/github.dart';
 | 
			
		||||
import 'package:obtainium/app_sources/html.dart';
 | 
			
		||||
import 'package:obtainium/custom_errors.dart';
 | 
			
		||||
import 'package:obtainium/providers/source_provider.dart';
 | 
			
		||||
 | 
			
		||||
@@ -29,24 +28,41 @@ class Mullvad extends AppSource {
 | 
			
		||||
    String standardUrl,
 | 
			
		||||
    Map<String, dynamic> additionalSettings,
 | 
			
		||||
  ) async {
 | 
			
		||||
    var details = await HTML().getLatestAPKDetails(
 | 
			
		||||
        '$standardUrl/en/download/android', additionalSettings);
 | 
			
		||||
    var fileName = details.apkUrls[0].split('/').last;
 | 
			
		||||
    var versionMatch = RegExp('[0-9]+(\\.[0-9]+)+').firstMatch(fileName);
 | 
			
		||||
    if (versionMatch == null) {
 | 
			
		||||
    Response res = await get(Uri.parse('$standardUrl/en/download/android'));
 | 
			
		||||
    if (res.statusCode == 200) {
 | 
			
		||||
      var versions = parse(res.body)
 | 
			
		||||
          .querySelectorAll('p')
 | 
			
		||||
          .map((e) => e.innerHtml)
 | 
			
		||||
          .where((p) => p.contains('Latest version: '))
 | 
			
		||||
          .map((e) {
 | 
			
		||||
            var match = RegExp('[0-9]+(\\.[0-9]+)*').firstMatch(e);
 | 
			
		||||
            if (match == null) {
 | 
			
		||||
              return '';
 | 
			
		||||
            } else {
 | 
			
		||||
              return e.substring(match.start, match.end);
 | 
			
		||||
            }
 | 
			
		||||
          })
 | 
			
		||||
          .where((element) => element.isNotEmpty)
 | 
			
		||||
          .toList();
 | 
			
		||||
      if (versions.isEmpty) {
 | 
			
		||||
        throw NoVersionError();
 | 
			
		||||
      }
 | 
			
		||||
    details.version = fileName.substring(versionMatch.start, versionMatch.end);
 | 
			
		||||
    details.names = AppNames(name, 'Mullvad-VPN');
 | 
			
		||||
      String? changeLog;
 | 
			
		||||
      try {
 | 
			
		||||
      details.changeLog = (await GitHub().getLatestAPKDetails(
 | 
			
		||||
        changeLog = (await GitHub().getLatestAPKDetails(
 | 
			
		||||
                'https://github.com/mullvad/mullvadvpn-app',
 | 
			
		||||
                {'fallbackToOlderReleases': true}))
 | 
			
		||||
            .changeLog;
 | 
			
		||||
      } catch (e) {
 | 
			
		||||
      print(e);
 | 
			
		||||
        // Ignore
 | 
			
		||||
      }
 | 
			
		||||
    return details;
 | 
			
		||||
      return APKDetails(
 | 
			
		||||
          versions[0],
 | 
			
		||||
          ['https://mullvad.net/download/app/apk/latest'],
 | 
			
		||||
          AppNames(name, 'Mullvad-VPN'),
 | 
			
		||||
          changeLog: changeLog);
 | 
			
		||||
    } else {
 | 
			
		||||
      throw getObtainiumHttpError(res);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -21,7 +21,7 @@ import 'package:easy_localization/src/easy_localization_controller.dart';
 | 
			
		||||
// ignore: implementation_imports
 | 
			
		||||
import 'package:easy_localization/src/localization.dart';
 | 
			
		||||
 | 
			
		||||
const String currentVersion = '0.11.17';
 | 
			
		||||
const String currentVersion = '0.11.24';
 | 
			
		||||
const String currentReleaseTag =
 | 
			
		||||
    'v$currentVersion-beta'; // KEEP THIS IN SYNC WITH GITHUB RELEASES
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -334,8 +334,7 @@ class _AddAppPageState extends State<AddAppPage> {
 | 
			
		||||
          ],
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
    Widget getSourcesListWidget() => Expanded(
 | 
			
		||||
            child: Column(
 | 
			
		||||
    Widget getSourcesListWidget() => Column(
 | 
			
		||||
            crossAxisAlignment: CrossAxisAlignment.center,
 | 
			
		||||
            mainAxisAlignment: MainAxisAlignment.center,
 | 
			
		||||
            children: [
 | 
			
		||||
@@ -365,16 +364,17 @@ class _AddAppPageState extends State<AddAppPage> {
 | 
			
		||||
                            fontStyle: FontStyle.italic),
 | 
			
		||||
                      )))
 | 
			
		||||
                  .toList()
 | 
			
		||||
            ]));
 | 
			
		||||
            ]);
 | 
			
		||||
 | 
			
		||||
    return Scaffold(
 | 
			
		||||
        backgroundColor: Theme.of(context).colorScheme.surface,
 | 
			
		||||
        body: CustomScrollView(slivers: <Widget>[
 | 
			
		||||
        body: CustomScrollView(shrinkWrap: true, slivers: <Widget>[
 | 
			
		||||
          CustomAppBar(title: tr('addApp')),
 | 
			
		||||
          SliverFillRemaining(
 | 
			
		||||
          SliverToBoxAdapter(
 | 
			
		||||
            child: Padding(
 | 
			
		||||
                padding: const EdgeInsets.all(16),
 | 
			
		||||
                child: Column(
 | 
			
		||||
                    mainAxisSize: MainAxisSize.min,
 | 
			
		||||
                    crossAxisAlignment: CrossAxisAlignment.stretch,
 | 
			
		||||
                    children: [
 | 
			
		||||
                      getUrlInputRow(),
 | 
			
		||||
 
 | 
			
		||||
@@ -61,6 +61,12 @@ class _AppPageState extends State<AppPage> {
 | 
			
		||||
                        mode: LaunchMode.externalApplication);
 | 
			
		||||
                  }
 | 
			
		||||
                },
 | 
			
		||||
                onLongPress: () {
 | 
			
		||||
                  Clipboard.setData(ClipboardData(text: app?.app.url ?? ''));
 | 
			
		||||
                  ScaffoldMessenger.of(context).showSnackBar(SnackBar(
 | 
			
		||||
                    content: Text(tr('copiedToClipboard')),
 | 
			
		||||
                  ));
 | 
			
		||||
                },
 | 
			
		||||
                child: Text(
 | 
			
		||||
                  app?.app.url ?? '',
 | 
			
		||||
                  textAlign: TextAlign.center,
 | 
			
		||||
@@ -147,7 +153,7 @@ class _AppPageState extends State<AppPage> {
 | 
			
		||||
              height: 25,
 | 
			
		||||
            ),
 | 
			
		||||
            Text(
 | 
			
		||||
              app?.installedInfo?.name ?? app?.app.name ?? tr('app'),
 | 
			
		||||
              app?.app.name ?? tr('app'),
 | 
			
		||||
              textAlign: TextAlign.center,
 | 
			
		||||
              style: Theme.of(context).textTheme.displayLarge,
 | 
			
		||||
            ),
 | 
			
		||||
 
 | 
			
		||||
@@ -56,6 +56,7 @@ class AppsPageState extends State<AppsPage> {
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    var appsProvider = context.watch<AppsProvider>();
 | 
			
		||||
    var settingsProvider = context.watch<SettingsProvider>();
 | 
			
		||||
    var sourceProvider = SourceProvider();
 | 
			
		||||
    var listedApps = appsProvider.apps.values.toList();
 | 
			
		||||
    var currentFilterIsUpdatesOnly =
 | 
			
		||||
        filter.isIdenticalTo(updatesOnlyFilter, settingsProvider);
 | 
			
		||||
@@ -110,6 +111,11 @@ class AppsPageState extends State<AppsPage> {
 | 
			
		||||
              .isEmpty) {
 | 
			
		||||
        return false;
 | 
			
		||||
      }
 | 
			
		||||
      if (filter.sourceFilter.isNotEmpty &&
 | 
			
		||||
          sourceProvider.getSource(app.app.url).runtimeType.toString() !=
 | 
			
		||||
              filter.sourceFilter) {
 | 
			
		||||
        return false;
 | 
			
		||||
      }
 | 
			
		||||
      return true;
 | 
			
		||||
    }).toList();
 | 
			
		||||
 | 
			
		||||
@@ -448,7 +454,8 @@ class AppsPageState extends State<AppsPage> {
 | 
			
		||||
                    .app
 | 
			
		||||
                    .categories
 | 
			
		||||
                    .map((e) =>
 | 
			
		||||
                        Color(settingsProvider.categories[e]!).withAlpha(255))
 | 
			
		||||
                        Color(settingsProvider.categories[e] ?? transparent)
 | 
			
		||||
                            .withAlpha(255))
 | 
			
		||||
                    .toList(),
 | 
			
		||||
                Color(transparent)
 | 
			
		||||
              ])),
 | 
			
		||||
@@ -734,14 +741,12 @@ class AppsPageState extends State<AppsPage> {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pinSelectedApps() {
 | 
			
		||||
      () {
 | 
			
		||||
      var pinStatus = selectedApps.where((element) => element.pinned).isEmpty;
 | 
			
		||||
      appsProvider.saveApps(selectedApps.map((e) {
 | 
			
		||||
        e.pinned = pinStatus;
 | 
			
		||||
        return e;
 | 
			
		||||
      }).toList());
 | 
			
		||||
      Navigator.of(context).pop();
 | 
			
		||||
      };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    resetSelectedAppsInstallStatuses() {
 | 
			
		||||
@@ -893,6 +898,19 @@ class AppsPageState extends State<AppsPage> {
 | 
			
		||||
                  GeneratedFormSwitch('nonInstalledApps',
 | 
			
		||||
                      label: tr('nonInstalledApps'),
 | 
			
		||||
                      defaultValue: vals['nonInstalledApps'])
 | 
			
		||||
                ],
 | 
			
		||||
                [
 | 
			
		||||
                  GeneratedFormDropdown(
 | 
			
		||||
                      'sourceFilter',
 | 
			
		||||
                      label: tr('appSource'),
 | 
			
		||||
                      defaultValue: filter.sourceFilter,
 | 
			
		||||
                      [
 | 
			
		||||
                        MapEntry('', tr('none')),
 | 
			
		||||
                        ...sourceProvider.sources
 | 
			
		||||
                            .map((e) =>
 | 
			
		||||
                                MapEntry(e.runtimeType.toString(), e.name))
 | 
			
		||||
                            .toList()
 | 
			
		||||
                      ])
 | 
			
		||||
                ]
 | 
			
		||||
              ],
 | 
			
		||||
              additionalWidgets: [
 | 
			
		||||
@@ -1016,20 +1034,23 @@ class AppsFilter {
 | 
			
		||||
  late bool includeUptodate;
 | 
			
		||||
  late bool includeNonInstalled;
 | 
			
		||||
  late Set<String> categoryFilter;
 | 
			
		||||
  late String sourceFilter;
 | 
			
		||||
 | 
			
		||||
  AppsFilter(
 | 
			
		||||
      {this.nameFilter = '',
 | 
			
		||||
      this.authorFilter = '',
 | 
			
		||||
      this.includeUptodate = true,
 | 
			
		||||
      this.includeNonInstalled = true,
 | 
			
		||||
      this.categoryFilter = const {}});
 | 
			
		||||
      this.categoryFilter = const {},
 | 
			
		||||
      this.sourceFilter = ''});
 | 
			
		||||
 | 
			
		||||
  Map<String, dynamic> toFormValuesMap() {
 | 
			
		||||
    return {
 | 
			
		||||
      'appName': nameFilter,
 | 
			
		||||
      'author': authorFilter,
 | 
			
		||||
      'upToDateApps': includeUptodate,
 | 
			
		||||
      'nonInstalledApps': includeNonInstalled
 | 
			
		||||
      'nonInstalledApps': includeNonInstalled,
 | 
			
		||||
      'sourceFilter': sourceFilter
 | 
			
		||||
    };
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@@ -1038,6 +1059,7 @@ class AppsFilter {
 | 
			
		||||
    authorFilter = values['author']!;
 | 
			
		||||
    includeUptodate = values['upToDateApps'];
 | 
			
		||||
    includeNonInstalled = values['nonInstalledApps'];
 | 
			
		||||
    sourceFilter = values['sourceFilter'];
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  bool isIdenticalTo(AppsFilter other, SettingsProvider settingsProvider) =>
 | 
			
		||||
@@ -1045,5 +1067,6 @@ class AppsFilter {
 | 
			
		||||
      nameFilter.trim() == other.nameFilter.trim() &&
 | 
			
		||||
      includeUptodate == other.includeUptodate &&
 | 
			
		||||
      includeNonInstalled == other.includeNonInstalled &&
 | 
			
		||||
      settingsProvider.setEqual(categoryFilter, other.categoryFilter);
 | 
			
		||||
      settingsProvider.setEqual(categoryFilter, other.categoryFilter) &&
 | 
			
		||||
      sourceFilter.trim() == other.sourceFilter.trim();
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -133,7 +133,7 @@ class _ImportExportPageState extends State<ImportExportPage> {
 | 
			
		||||
                }
 | 
			
		||||
              }
 | 
			
		||||
            });
 | 
			
		||||
            settingsProvider.categories = cats;
 | 
			
		||||
            appsProvider.addMissingCategories(settingsProvider);
 | 
			
		||||
            showError(tr('importedX', args: [plural('apps', value)]), context);
 | 
			
		||||
          });
 | 
			
		||||
        } else {
 | 
			
		||||
 
 | 
			
		||||
@@ -6,6 +6,7 @@ import 'package:obtainium/components/custom_app_bar.dart';
 | 
			
		||||
import 'package:obtainium/components/generated_form.dart';
 | 
			
		||||
import 'package:obtainium/custom_errors.dart';
 | 
			
		||||
import 'package:obtainium/main.dart';
 | 
			
		||||
import 'package:obtainium/providers/apps_provider.dart';
 | 
			
		||||
import 'package:obtainium/providers/logs_provider.dart';
 | 
			
		||||
import 'package:obtainium/providers/settings_provider.dart';
 | 
			
		||||
import 'package:obtainium/providers/source_provider.dart';
 | 
			
		||||
@@ -444,6 +445,7 @@ class _CategoryEditorSelectorState extends State<CategoryEditorSelector> {
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    var settingsProvider = context.watch<SettingsProvider>();
 | 
			
		||||
    var appsProvider = context.watch<AppsProvider>();
 | 
			
		||||
    storedValues = settingsProvider.categories.map((key, value) => MapEntry(
 | 
			
		||||
        key,
 | 
			
		||||
        MapEntry(value,
 | 
			
		||||
@@ -467,8 +469,9 @@ class _CategoryEditorSelectorState extends State<CategoryEditorSelector> {
 | 
			
		||||
          if (!isBuilding) {
 | 
			
		||||
            storedValues =
 | 
			
		||||
                values['categories'] as Map<String, MapEntry<int, bool>>;
 | 
			
		||||
            settingsProvider.categories =
 | 
			
		||||
                storedValues.map((key, value) => MapEntry(key, value.key));
 | 
			
		||||
            settingsProvider.setCategories(
 | 
			
		||||
                storedValues.map((key, value) => MapEntry(key, value.key)),
 | 
			
		||||
                appsProvider: appsProvider);
 | 
			
		||||
            if (widget.onSelected != null) {
 | 
			
		||||
              widget.onSelected!(storedValues.keys
 | 
			
		||||
                  .where((k) => storedValues[k]!.value)
 | 
			
		||||
 
 | 
			
		||||
@@ -670,6 +670,9 @@ class AppsProvider with ChangeNotifier {
 | 
			
		||||
    for (var app in apps) {
 | 
			
		||||
      AppInfo? info = await getInstalledInfo(app.id);
 | 
			
		||||
      app.name = info?.name ?? app.name;
 | 
			
		||||
      if (app.additionalSettings['appName']?.toString().isNotEmpty == true) {
 | 
			
		||||
        app.name = app.additionalSettings['appName'].toString().trim();
 | 
			
		||||
      }
 | 
			
		||||
      if (attemptToCorrectInstallStatus) {
 | 
			
		||||
        app = getCorrectedInstallStatusAppIfPossible(app, info) ?? app;
 | 
			
		||||
      }
 | 
			
		||||
@@ -757,6 +760,18 @@ class AppsProvider with ChangeNotifier {
 | 
			
		||||
    await intent.launch();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  addMissingCategories(SettingsProvider settingsProvider) {
 | 
			
		||||
    var cats = settingsProvider.categories;
 | 
			
		||||
    apps.forEach((key, value) {
 | 
			
		||||
      for (var c in value.app.categories) {
 | 
			
		||||
        if (!cats.containsKey(c)) {
 | 
			
		||||
          cats[c] = generateRandomLightColor().value;
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
    settingsProvider.setCategories(cats, appsProvider: this);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Future<App?> checkUpdate(String appId) async {
 | 
			
		||||
    App? currentApp = apps[appId]!.app;
 | 
			
		||||
    SourceProvider sourceProvider = SourceProvider();
 | 
			
		||||
@@ -836,12 +851,6 @@ class AppsProvider with ChangeNotifier {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Future<String> exportApps() async {
 | 
			
		||||
    Directory? exportDir = Directory('/storage/emulated/0/Download');
 | 
			
		||||
    String path = 'Downloads'; // TODO: See if hardcoding this can be avoided
 | 
			
		||||
    if (!exportDir.existsSync()) {
 | 
			
		||||
      exportDir = await getExternalStorageDirectory();
 | 
			
		||||
      path = exportDir!.path;
 | 
			
		||||
    }
 | 
			
		||||
    if ((await DeviceInfoPlugin().androidInfo).version.sdkInt <= 29) {
 | 
			
		||||
      if (await Permission.storage.isDenied) {
 | 
			
		||||
        await Permission.storage.request();
 | 
			
		||||
@@ -850,6 +859,18 @@ class AppsProvider with ChangeNotifier {
 | 
			
		||||
        throw ObtainiumError(tr('storagePermissionDenied'));
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    Directory? exportDir = Directory('/storage/emulated/0/Download');
 | 
			
		||||
    String path = 'Downloads'; // TODO: See if hardcoding this can be avoided
 | 
			
		||||
    var downloadsAccessible = false;
 | 
			
		||||
    try {
 | 
			
		||||
      downloadsAccessible = exportDir.existsSync();
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      logs.add('Error accessing Downloads (will use fallback): $e');
 | 
			
		||||
    }
 | 
			
		||||
    if (!downloadsAccessible) {
 | 
			
		||||
      exportDir = await getExternalStorageDirectory();
 | 
			
		||||
      path = exportDir!.path;
 | 
			
		||||
    }
 | 
			
		||||
    File export = File(
 | 
			
		||||
        '${exportDir.path}/${tr('obtainiumExportHyphenatedLowercase')}-${DateTime.now().millisecondsSinceEpoch}.json');
 | 
			
		||||
    export.writeAsStringSync(
 | 
			
		||||
 
 | 
			
		||||
@@ -7,6 +7,8 @@ import 'package:flutter/material.dart';
 | 
			
		||||
import 'package:fluttertoast/fluttertoast.dart';
 | 
			
		||||
import 'package:obtainium/app_sources/github.dart';
 | 
			
		||||
import 'package:obtainium/main.dart';
 | 
			
		||||
import 'package:obtainium/providers/apps_provider.dart';
 | 
			
		||||
import 'package:obtainium/providers/source_provider.dart';
 | 
			
		||||
import 'package:permission_handler/permission_handler.dart';
 | 
			
		||||
import 'package:shared_preferences/shared_preferences.dart';
 | 
			
		||||
 | 
			
		||||
@@ -160,7 +162,22 @@ class SettingsProvider with ChangeNotifier {
 | 
			
		||||
  Map<String, int> get categories =>
 | 
			
		||||
      Map<String, int>.from(jsonDecode(prefs?.getString('categories') ?? '{}'));
 | 
			
		||||
 | 
			
		||||
  set categories(Map<String, int> cats) {
 | 
			
		||||
  void setCategories(Map<String, int> cats, {AppsProvider? appsProvider}) {
 | 
			
		||||
    if (appsProvider != null) {
 | 
			
		||||
      List<App> changedApps = appsProvider.apps.values
 | 
			
		||||
          .map((a) {
 | 
			
		||||
            var n1 = a.app.categories.length;
 | 
			
		||||
            a.app.categories.removeWhere((c) => !cats.keys.contains(c));
 | 
			
		||||
            return n1 > a.app.categories.length ? a.app : null;
 | 
			
		||||
          })
 | 
			
		||||
          .where((element) => element != null)
 | 
			
		||||
          .map((e) => e as App)
 | 
			
		||||
          .toList();
 | 
			
		||||
      if (changedApps.isNotEmpty) {
 | 
			
		||||
        appsProvider.saveApps(changedApps,
 | 
			
		||||
            attemptToCorrectInstallStatus: false);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    prefs?.setString('categories', jsonEncode(cats));
 | 
			
		||||
    notifyListeners();
 | 
			
		||||
  }
 | 
			
		||||
 
 | 
			
		||||
@@ -278,7 +278,8 @@ class AppSource {
 | 
			
		||||
              return regExValidator(value);
 | 
			
		||||
            }
 | 
			
		||||
          ])
 | 
			
		||||
    ]
 | 
			
		||||
    ],
 | 
			
		||||
    [GeneratedFormTextField('appName', label: tr('appName'), required: false)]
 | 
			
		||||
  ];
 | 
			
		||||
 | 
			
		||||
  // Previous 2 variables combined into one at runtime for convenient usage
 | 
			
		||||
@@ -362,7 +363,7 @@ class SourceProvider {
 | 
			
		||||
    url = preStandardizeUrl(url);
 | 
			
		||||
    AppSource? source;
 | 
			
		||||
    for (var s in sources.where((element) => element.host != null)) {
 | 
			
		||||
      if (url.contains('://${s.host}')) {
 | 
			
		||||
      if (RegExp('://(.+\\.)?${s.host}').hasMatch(url)) {
 | 
			
		||||
        source = s;
 | 
			
		||||
        break;
 | 
			
		||||
      }
 | 
			
		||||
@@ -427,8 +428,10 @@ class SourceProvider {
 | 
			
		||||
      throw NoAPKError();
 | 
			
		||||
    }
 | 
			
		||||
    String apkVersion = apk.version.replaceAll('/', '-');
 | 
			
		||||
    var name = currentApp?.name.trim() ??
 | 
			
		||||
        apk.names.name[0].toUpperCase() + apk.names.name.substring(1);
 | 
			
		||||
    var name = currentApp != null ? currentApp.name.trim() : '';
 | 
			
		||||
    name = name.isNotEmpty
 | 
			
		||||
        ? name
 | 
			
		||||
        : apk.names.name[0].toUpperCase() + apk.names.name.substring(1);
 | 
			
		||||
    return App(
 | 
			
		||||
        currentApp?.id ??
 | 
			
		||||
            source.tryInferringAppId(standardUrl,
 | 
			
		||||
@@ -436,9 +439,7 @@ class SourceProvider {
 | 
			
		||||
            generateTempID(standardUrl, additionalSettings),
 | 
			
		||||
        standardUrl,
 | 
			
		||||
        apk.names.author[0].toUpperCase() + apk.names.author.substring(1),
 | 
			
		||||
        name.trim().isNotEmpty
 | 
			
		||||
            ? name
 | 
			
		||||
            : apk.names.name[0].toUpperCase() + apk.names.name.substring(1),
 | 
			
		||||
        name,
 | 
			
		||||
        currentApp?.installedVersion,
 | 
			
		||||
        apkVersion,
 | 
			
		||||
        apk.apkUrls,
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										34
									
								
								pubspec.lock
									
									
									
									
									
								
							
							
						
						
									
										34
									
								
								pubspec.lock
									
									
									
									
									
								
							@@ -337,10 +337,10 @@ packages:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
      name: markdown
 | 
			
		||||
      sha256: b3c60dee8c2af50ad0e6e90cceba98e47718a6ee0a7a6772c77846a0cc21f78b
 | 
			
		||||
      sha256: d95a9d12954aafc97f984ca29baaa7690ed4d9ec4140a23ad40580bcdb6c87f5
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "7.0.1"
 | 
			
		||||
    version: "7.0.2"
 | 
			
		||||
  matcher:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
@@ -553,34 +553,34 @@ packages:
 | 
			
		||||
    dependency: "direct main"
 | 
			
		||||
    description:
 | 
			
		||||
      name: shared_preferences
 | 
			
		||||
      sha256: "78528fd87d0d08ffd3e69551173c026e8eacc7b7079c82eb6a77413957b7e394"
 | 
			
		||||
      sha256: "858aaa72d8f61637d64e776aca82e1c67e6d9ee07979123c5d17115031c1b13b"
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "2.0.20"
 | 
			
		||||
    version: "2.1.0"
 | 
			
		||||
  shared_preferences_android:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
      name: shared_preferences_android
 | 
			
		||||
      sha256: ad423a80fe7b4e48b50d6111b3ea1027af0e959e49d485712e134863d9c1c521
 | 
			
		||||
      sha256: "8304d8a1f7d21a429f91dee552792249362b68a331ac5c3c1caf370f658873f6"
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "2.0.17"
 | 
			
		||||
    version: "2.1.0"
 | 
			
		||||
  shared_preferences_foundation:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
      name: shared_preferences_foundation
 | 
			
		||||
      sha256: "1e755f8583229f185cfca61b1d80fb2344c9d660e1c69ede5450d8f478fa5310"
 | 
			
		||||
      sha256: cf2a42fb20148502022861f71698db12d937c7459345a1bdaa88fc91a91b3603
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "2.1.5"
 | 
			
		||||
    version: "2.2.0"
 | 
			
		||||
  shared_preferences_linux:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
      name: shared_preferences_linux
 | 
			
		||||
      sha256: "3a59ed10890a8409ad0faad7bb2957dab4b92b8fbe553257b05d30ed8af2c707"
 | 
			
		||||
      sha256: "9d387433ca65717bbf1be88f4d5bb18f10508917a8fa2fb02e0fd0d7479a9afa"
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "2.1.5"
 | 
			
		||||
    version: "2.2.0"
 | 
			
		||||
  shared_preferences_platform_interface:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
@@ -593,18 +593,18 @@ packages:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
      name: shared_preferences_web
 | 
			
		||||
      sha256: "0dc2633f215a3d4aa3184c9b2c5766f4711e4e5a6b256e62aafee41f89f1bfb8"
 | 
			
		||||
      sha256: "74083203a8eae241e0de4a0d597dbedab3b8fef5563f33cf3c12d7e93c655ca5"
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "2.0.6"
 | 
			
		||||
    version: "2.1.0"
 | 
			
		||||
  shared_preferences_windows:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
      name: shared_preferences_windows
 | 
			
		||||
      sha256: "71bcd669bb9cdb6b39f22c4a7728b6d49e934f6cba73157ffa5a54f1eed67436"
 | 
			
		||||
      sha256: "5e588e2efef56916a3b229c3bfe81e6a525665a454519ca51dbcc4236a274173"
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "2.1.5"
 | 
			
		||||
    version: "2.2.0"
 | 
			
		||||
  sky_engine:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description: flutter
 | 
			
		||||
@@ -790,10 +790,10 @@ packages:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
      name: webview_flutter_android
 | 
			
		||||
      sha256: "34f83c2f0f64c75ad75c77a2ccfc8d2e531afbe8ad41af1fd787d6d33336aa90"
 | 
			
		||||
      sha256: "9e223788e1954087dac30d813dc151f8e12f09f1139f116ce20b5658893f3627"
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "3.4.3"
 | 
			
		||||
    version: "3.4.4"
 | 
			
		||||
  webview_flutter_platform_interface:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
@@ -835,5 +835,5 @@ packages:
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "6.2.2"
 | 
			
		||||
sdks:
 | 
			
		||||
  dart: ">=2.18.2 <3.0.0"
 | 
			
		||||
  dart: ">=2.19.0 <3.0.0"
 | 
			
		||||
  flutter: ">=3.4.0-17.0.pre"
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
# 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.
 | 
			
		||||
version: 0.11.17+139 # When changing this, update the tag in main() accordingly
 | 
			
		||||
version: 0.11.24+146 # When changing this, update the tag in main() accordingly
 | 
			
		||||
 | 
			
		||||
environment:
 | 
			
		||||
  sdk: '>=2.18.2 <3.0.0'
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user