diff --git a/lib/app_sources/directAPKLink.dart b/lib/app_sources/directAPKLink.dart index def21dd..f80a711 100644 --- a/lib/app_sources/directAPKLink.dart +++ b/lib/app_sources/directAPKLink.dart @@ -24,6 +24,14 @@ class DirectAPKLink extends AppSource { ]; } + @override + Future?> getRequestHeaders( + Map additionalSettings, + {bool forAPKDownload = false}) { + return html.getRequestHeaders(additionalSettings, + forAPKDownload: forAPKDownload); + } + @override Future getLatestAPKDetails( String standardUrl, diff --git a/lib/app_sources/fdroidrepo.dart b/lib/app_sources/fdroidrepo.dart index 41e92df..1eb0262 100644 --- a/lib/app_sources/fdroidrepo.dart +++ b/lib/app_sources/fdroidrepo.dart @@ -9,7 +9,7 @@ class FDroidRepo extends AppSource { FDroidRepo() { name = tr('fdroidThirdPartyRepo'); canSearch = true; - excludeFromMassSearch = true; + includeAdditionalOptsInMainSearch = true; neverAutoSelect = true; showReleaseDateAsVersionToggle = true; @@ -86,6 +86,27 @@ class FDroidRepo extends AppSource { } } + @override + void runOnAddAppInputChange(String userInput) { + this.additionalSourceAppSpecificSettingFormItems = + this.additionalSourceAppSpecificSettingFormItems.map((row) { + row = row.map((item) { + if (item.key == 'appIdOrName') { + try { + var appId = Uri.parse(userInput).queryParameters['appId']; + if (appId != null && item is GeneratedFormTextField) { + item.required = false; + } + } catch (e) { + // + } + } + return item; + }).toList(); + return row; + }).toList(); + } + @override App endOfGetAppChanges(App app) { var uri = Uri.parse(app.url); @@ -142,6 +163,7 @@ class FDroidRepo extends AppSource { if (appIdOrName == null) { throw NoReleasesError(); } + additionalSettings['appIdOrName'] = appIdOrName; var res = await sourceRequestWithURLVariants(standardUrl, additionalSettings); if (res.statusCode == 200) { diff --git a/lib/app_sources/html.dart b/lib/app_sources/html.dart index b089d6e..3862e12 100644 --- a/lib/app_sources/html.dart +++ b/lib/app_sources/html.dart @@ -332,10 +332,13 @@ class HTML extends AppSource { additionalSettings['versionExtractWholePage'] == true ? versionExtractionWholePageString : relDecoded); - version ??= - additionalSettings['defaultPseudoVersioningMethod'] == 'APKLinkHash' - ? rel.hashCode.toString() - : (await checkPartialDownloadHashDynamic(rel)).toString(); + version ??= additionalSettings['defaultPseudoVersioningMethod'] == + 'APKLinkHash' + ? rel.hashCode.toString() + : (await checkPartialDownloadHashDynamic(rel, + headers: await getRequestHeaders(additionalSettings, + forAPKDownload: true))) + .toString(); return APKDetails(version, [rel].map((e) => MapEntry(e, e)).toList(), AppNames(uri.host, tr('app'))); } diff --git a/lib/components/generated_form.dart b/lib/components/generated_form.dart index 23eb928..e9dcc0b 100644 --- a/lib/components/generated_form.dart +++ b/lib/components/generated_form.dart @@ -5,6 +5,7 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:obtainium/components/generated_form_modal.dart'; import 'package:obtainium/providers/source_provider.dart'; +import 'package:flutter_typeahead/flutter_typeahead.dart'; abstract class GeneratedFormItem { late String key; @@ -28,6 +29,7 @@ class GeneratedFormTextField extends GeneratedFormItem { late String? hint; late bool password; late TextInputType? textInputType; + late List? autoCompleteOptions; GeneratedFormTextField(super.key, {super.label, @@ -39,7 +41,8 @@ class GeneratedFormTextField extends GeneratedFormItem { this.max = 1, this.hint, this.password = false, - this.textInputType}); + this.textInputType, + this.autoCompleteOptions}); @override String ensureType(val) { @@ -274,38 +277,62 @@ class _GeneratedFormState extends State { var formItem = e.value; if (formItem is GeneratedFormTextField) { final formFieldKey = GlobalKey(); - return TextFormField( - keyboardType: formItem.textInputType, - obscureText: formItem.password, - autocorrect: !formItem.password, - enableSuggestions: !formItem.password, - key: formFieldKey, - initialValue: values[formItem.key], - autovalidateMode: AutovalidateMode.onUserInteraction, - onChanged: (value) { + var ctrl = TextEditingController(text: values[formItem.key]); + return TypeAheadField( + controller: ctrl, + builder: (context, controller, focusNode) { + return TextFormField( + controller: ctrl, + focusNode: focusNode, + keyboardType: formItem.textInputType, + obscureText: formItem.password, + autocorrect: !formItem.password, + enableSuggestions: !formItem.password, + key: formFieldKey, + autovalidateMode: AutovalidateMode.onUserInteraction, + onChanged: (value) { + setState(() { + values[formItem.key] = value; + someValueChanged(); + }); + }, + decoration: InputDecoration( + helperText: + formItem.label + (formItem.required ? ' *' : ''), + hintText: formItem.hint), + minLines: formItem.max <= 1 ? null : formItem.max, + maxLines: formItem.max <= 1 ? 1 : formItem.max, + validator: (value) { + if (formItem.required && + (value == null || value.trim().isEmpty)) { + return '${formItem.label} ${tr('requiredInBrackets')}'; + } + for (var validator in formItem.additionalValidators) { + String? result = validator(value); + if (result != null) { + return result; + } + } + return null; + }, + ); + }, + itemBuilder: (context, value) { + return ListTile(title: Text(value)); + }, + onSelected: (value) { + ctrl.text = value; setState(() { values[formItem.key] = value; someValueChanged(); }); }, - decoration: InputDecoration( - helperText: formItem.label + (formItem.required ? ' *' : ''), - hintText: formItem.hint), - minLines: formItem.max <= 1 ? null : formItem.max, - maxLines: formItem.max <= 1 ? 1 : formItem.max, - validator: (value) { - if (formItem.required && - (value == null || value.trim().isEmpty)) { - return '${formItem.label} ${tr('requiredInBrackets')}'; - } - for (var validator in formItem.additionalValidators) { - String? result = validator(value); - if (result != null) { - return result; - } - } - return null; + suggestionsCallback: (search) { + return formItem.autoCompleteOptions + ?.where((t) => t.toLowerCase().contains(search.toLowerCase())) + .toList(); }, + hideOnEmpty: true, ); } else if (formItem is GeneratedFormDropdown) { if (formItem.opts!.isEmpty) { diff --git a/lib/pages/add_app.dart b/lib/pages/add_app.dart index 5dc17b0..faa610c 100644 --- a/lib/pages/add_app.dart +++ b/lib/pages/add_app.dart @@ -51,10 +51,13 @@ class AddAppPageState extends State { } changeUserInput(String input, bool valid, bool isBuilding, - {bool updateUrlInput = false}) { + {bool updateUrlInput = false, String? overrideSource}) { userInput = input; if (!isBuilding) { setState(() { + if (overrideSource != null) { + pickedSourceOverride = overrideSource; + } if (updateUrlInput) { urlInputKey++; } @@ -68,6 +71,7 @@ class AddAppPageState extends State { if (pickedSource.runtimeType != source.runtimeType || (prevHost != null && prevHost != source?.hosts[0])) { pickedSource = source; + pickedSource?.runOnAddAppInputChange(userInput); additionalSettings = source != null ? getDefaultValuesFromFormItems( source.combinedAppSpecificSettingFormItems) @@ -259,9 +263,7 @@ class AddAppPageState extends State { searching = true; }); var sourceStrings = >{}; - sourceProvider.sources - .where((e) => e.canSearch && !e.excludeFromMassSearch) - .forEach((s) { + sourceProvider.sources.where((e) => e.canSearch).forEach((s) { sourceStrings[s.name] = [s.name]; }); try { @@ -282,32 +284,78 @@ class AddAppPageState extends State { settingsProvider.searchDeselected = sourceStrings.keys .where((s) => !searchSources.contains(s)) .toList(); - var results = await Future.wait(sourceProvider.sources - .where((e) => searchSources.contains(e.name)) - .map((e) async { + List>>?> results = + (await Future.wait(sourceProvider.sources + .where((e) => searchSources.contains(e.name)) + .map((e) async { try { - return await e.search(searchQuery); + Map? querySettings = {}; + if (e.includeAdditionalOptsInMainSearch) { + querySettings = await showDialog?>( + context: context, + builder: (BuildContext ctx) { + return GeneratedFormModal( + title: tr('searchX', args: [e.name]), + items: [ + ...e.searchQuerySettingFormItems.map((e) => [e]), + [ + GeneratedFormTextField('url', + label: e.hosts.isNotEmpty + ? tr('overrideSource') + : plural('url', 1).substring(2), + autoCompleteOptions: [ + ...(e.hosts.isNotEmpty ? [e.hosts[0]] : []), + ...appsProvider.apps.values + .where((a) => + sourceProvider + .getSource(a.app.url, + overrideSource: + a.app.overrideSource) + .runtimeType == + e.runtimeType) + .map((a) { + var uri = Uri.parse(a.app.url); + return '${uri.origin}${uri.path}'; + }) + ], + defaultValue: + e.hosts.isNotEmpty ? e.hosts[0] : '', + required: true) + ], + ], + ); + }); + if (querySettings == null) { + return null; + } + } + return MapEntry(e.runtimeType.toString(), + await e.search(searchQuery, querySettings: querySettings)); } catch (err) { if (err is! CredsNeededError) { rethrow; } else { err.unexpected = true; showError(err, context); - return >{}; + return null; } } - })); + }))) + .where((a) => a != null) + .toList(); // Interleave results instead of simple reduce - Map> res = {}; + Map>> res = {}; var si = 0; var done = false; while (!done) { done = true; for (var r in results) { - if (r.length > si) { + var sourceName = r!.key; + if (r.value.length > si) { done = false; - res.addEntries([r.entries.elementAt(si)]); + var singleRes = r.value.entries.elementAt(si); + res[singleRes.key] = MapEntry(sourceName, singleRes.value); } } si++; @@ -322,13 +370,15 @@ class AddAppPageState extends State { context: context, builder: (BuildContext ctx) { return SelectionModal( - entries: res, + entries: res.map((k, v) => MapEntry(k, v.value)), selectedByDefault: false, onlyOneSelectionAllowed: true, ); }); if (selectedUrls != null && selectedUrls.isNotEmpty) { - changeUserInput(selectedUrls[0], true, false, updateUrlInput: true); + var sourceName = res[selectedUrls[0]]?.key; + changeUserInput(selectedUrls[0], true, false, + updateUrlInput: true, overrideSource: sourceName); } } } catch (e) { @@ -349,7 +399,7 @@ class AddAppPageState extends State { [ GeneratedFormDropdown( 'overrideSource', - defaultValue: '', + defaultValue: pickedSourceOverride ?? '', [ MapEntry('', tr('none')), ...sourceProvider.sources.map( diff --git a/lib/pages/app.dart b/lib/pages/app.dart index 4166480..cda1d8d 100644 --- a/lib/pages/app.dart +++ b/lib/pages/app.dart @@ -161,25 +161,46 @@ class _AppPageState extends State { if (app?.app.apkUrls.isNotEmpty == true || app?.app.otherAssetUrls.isNotEmpty == true) GestureDetector( - onTap: app?.app == null || updating - ? null - : () async { - try { - await appsProvider - .downloadAppAssets([app!.app.id], context); - } catch (e) { - showError(e, context); - } - }, - child: Text( - tr('downloadX', args: [tr('releaseAsset').toLowerCase()]), - textAlign: TextAlign.center, - style: Theme.of(context).textTheme.labelSmall!.copyWith( - decoration: TextDecoration.underline, - fontStyle: FontStyle.italic, - ), - ), - ), + onTap: app?.app == null || updating + ? null + : () async { + try { + await appsProvider + .downloadAppAssets([app!.app.id], context); + } catch (e) { + showError(e, context); + } + }, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + color: settingsProvider.highlightTouchTargets + ? (Theme.of(context).brightness == + Brightness.light + ? Theme.of(context).primaryColor + : Theme.of(context).primaryColorLight) + .withAlpha(20) + : null), + padding: settingsProvider.highlightTouchTargets + ? const EdgeInsetsDirectional.fromSTEB(12, 6, 12, 6) + : const EdgeInsetsDirectional.fromSTEB(0, 6, 0, 6), + margin: + const EdgeInsetsDirectional.fromSTEB(0, 6, 0, 0), + child: Text( + tr('downloadX', + args: [tr('releaseAsset').toLowerCase()]), + textAlign: TextAlign.center, + style: + Theme.of(context).textTheme.labelSmall!.copyWith( + decoration: TextDecoration.underline, + fontStyle: FontStyle.italic, + ), + )) + ], + )), const SizedBox( height: 48, ), diff --git a/lib/pages/apps.dart b/lib/pages/apps.dart index 23ffb1c..0cad218 100644 --- a/lib/pages/apps.dart +++ b/lib/pages/apps.dart @@ -466,7 +466,7 @@ class AppsPageState extends State { hasUpdate ? getUpdateButton(index) : const SizedBox.shrink(), hasUpdate ? const SizedBox( - width: 10, + width: 5, ) : const SizedBox.shrink(), GestureDetector( @@ -1105,6 +1105,7 @@ class AppsPageState extends State { interactive: true, controller: scrollController, child: CustomScrollView( + physics: const AlwaysScrollableScrollPhysics(), controller: scrollController, slivers: [ CustomAppBar(title: tr('appsString')), diff --git a/lib/providers/apps_provider.dart b/lib/providers/apps_provider.dart index e1f349c..9c12521 100644 --- a/lib/providers/apps_provider.dart +++ b/lib/providers/apps_provider.dart @@ -717,7 +717,8 @@ class AppsProvider with ChangeNotifier { } Future?> confirmAppFileUrl( - App app, BuildContext? context, bool pickAnyAsset) async { + App app, BuildContext? context, bool pickAnyAsset, + {bool evenIfSingleChoice = false}) async { var urlsToSelectFrom = app.apkUrls; if (pickAnyAsset) { urlsToSelectFrom = [...urlsToSelectFrom, ...app.otherAssetUrls]; @@ -728,7 +729,8 @@ class AppsProvider with ChangeNotifier { // get device supported architecture List archs = (await DeviceInfoPlugin().androidInfo).supportedAbis; - if (urlsToSelectFrom.length > 1 && context != null) { + if ((urlsToSelectFrom.length > 1 || evenIfSingleChoice) && + context != null) { appFileUrl = await showDialog( // ignore: use_build_context_synchronously context: context, @@ -973,7 +975,8 @@ class AppsProvider with ChangeNotifier { if (apps[id]!.app.apkUrls.isNotEmpty || apps[id]!.app.otherAssetUrls.isNotEmpty) { // ignore: use_build_context_synchronously - fileUrl = await confirmAppFileUrl(apps[id]!.app, context, true); + fileUrl = await confirmAppFileUrl(apps[id]!.app, context, true, + evenIfSingleChoice: true); } if (fileUrl != null) { filesToDownload.add(MapEntry(fileUrl, apps[id]!.app)); @@ -1650,7 +1653,9 @@ class _AppFilePickerState extends State { ? tr('selectX', args: [tr('releaseAsset').toLowerCase()]) : tr('pickAnAPK')), content: Column(children: [ - Text(tr('appHasMoreThanOnePackage', args: [widget.app.finalName])), + urlsToSelectFrom.length > 1 + ? Text(tr('appHasMoreThanOnePackage', args: [widget.app.finalName])) + : const SizedBox.shrink(), const SizedBox(height: 16), ...urlsToSelectFrom.map( (u) => RadioListTile( diff --git a/lib/providers/source_provider.dart b/lib/providers/source_provider.dart index 73d9b46..50dd068 100644 --- a/lib/providers/source_provider.dart +++ b/lib/providers/source_provider.dart @@ -354,7 +354,9 @@ preStandardizeUrl(String url) { url.toLowerCase().indexOf('https://') != 0) { url = 'https://$url'; } - var trailingSlash = Uri.tryParse(url)?.path.endsWith('/') ?? false; + var uri = Uri.tryParse(url); + var trailingSlash = (uri?.path.endsWith('/') ?? false) && + (uri?.queryParameters.isEmpty ?? false); url = url .split('/') .where((e) => e.isNotEmpty) @@ -463,6 +465,10 @@ abstract class AppSource { } } + void runOnAddAppInputChange(String inputUrl) { + // + } + String sourceSpecificStandardizeURL(String url) { throw NotImplementedError(); } @@ -617,7 +623,7 @@ abstract class AppSource { } bool canSearch = false; - bool excludeFromMassSearch = false; + bool includeAdditionalOptsInMainSearch = false; List searchQuerySettingFormItems = []; Future>> search(String query, {Map querySettings = const {}}) { diff --git a/pubspec.lock b/pubspec.lock index 1218dfc..656977c 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -47,10 +47,34 @@ packages: dependency: "direct main" description: name: app_links - sha256: "96e677810b83707ff5e10fac11e4839daa0ea4e0123c35864c092699165eb3db" + sha256: a9905d6a60e814503fabc7523a9ed161b812d7ca69c99ad8ceea14279dc4f06b url: "https://pub.dev" source: hosted - version: "6.1.1" + version: "6.1.3" + app_links_linux: + dependency: transitive + description: + name: app_links_linux + sha256: "567139eca3ca9fb113f2082f3aaa75a26f30f0ebdbe5fa7f09a3913c5bebd630" + url: "https://pub.dev" + source: hosted + version: "1.0.2" + app_links_platform_interface: + dependency: transitive + description: + name: app_links_platform_interface + sha256: "58cff6f11df59b0e514dd5e4a61e988348ad5662f0e75d45d4e214ebea55c94c" + url: "https://pub.dev" + source: hosted + version: "2.0.0" + app_links_web: + dependency: transitive + description: + name: app_links_web + sha256: "74586ed5f3c4786341e82a0fa43c39ec3f13108a550f74e80d8bf68aa11349d1" + url: "https://pub.dev" + source: hosted + version: "1.0.3" archive: dependency: transitive description: @@ -279,18 +303,18 @@ packages: dependency: "direct main" description: name: flex_color_picker - sha256: "31b27677d8d8400e4cff5edb3f189f606dd964d608779b6ae1b7ddad37ea48c6" + sha256: "809af4ec82ede3b140ed0219b97d548de99e47aa4b99b14a10f705a2dbbcba5e" url: "https://pub.dev" source: hosted - version: "3.5.0" + version: "3.5.1" flex_seed_scheme: dependency: transitive description: name: flex_seed_scheme - sha256: fb66cdb8ca89084e79efcad2bc2d9deb144666875116f08cdd8d9f8238c8b3ab + sha256: "6c595e545b0678e1fe17e8eec3d1fbca7237482da194fadc20ad8607dc7a7f3d" url: "https://pub.dev" source: hosted - version: "2.0.0" + version: "3.0.0" flutter: dependency: "direct main" description: flutter @@ -312,6 +336,54 @@ packages: url: "https://pub.dev" source: hosted version: "0.3.0" + flutter_keyboard_visibility: + dependency: transitive + description: + name: flutter_keyboard_visibility + sha256: "98664be7be0e3ffca00de50f7f6a287ab62c763fc8c762e0a21584584a3ff4f8" + url: "https://pub.dev" + source: hosted + version: "6.0.0" + flutter_keyboard_visibility_linux: + dependency: transitive + description: + name: flutter_keyboard_visibility_linux + sha256: "6fba7cd9bb033b6ddd8c2beb4c99ad02d728f1e6e6d9b9446667398b2ac39f08" + url: "https://pub.dev" + source: hosted + version: "1.0.0" + flutter_keyboard_visibility_macos: + dependency: transitive + description: + name: flutter_keyboard_visibility_macos + sha256: c5c49b16fff453dfdafdc16f26bdd8fb8d55812a1d50b0ce25fc8d9f2e53d086 + url: "https://pub.dev" + source: hosted + version: "1.0.0" + flutter_keyboard_visibility_platform_interface: + dependency: transitive + description: + name: flutter_keyboard_visibility_platform_interface + sha256: e43a89845873f7be10cb3884345ceb9aebf00a659f479d1c8f4293fcb37022a4 + url: "https://pub.dev" + source: hosted + version: "2.0.0" + flutter_keyboard_visibility_web: + dependency: transitive + description: + name: flutter_keyboard_visibility_web + sha256: d3771a2e752880c79203f8d80658401d0c998e4183edca05a149f5098ce6e3d1 + url: "https://pub.dev" + source: hosted + version: "2.0.0" + flutter_keyboard_visibility_windows: + dependency: transitive + description: + name: flutter_keyboard_visibility_windows + sha256: fc4b0f0b6be9b93ae527f3d527fb56ee2d918cd88bbca438c478af7bcfd0ef73 + url: "https://pub.dev" + source: hosted + version: "1.0.0" flutter_launcher_icons: dependency: "direct dev" description: @@ -332,10 +404,10 @@ packages: dependency: "direct main" description: name: flutter_local_notifications - sha256: "40e6fbd2da7dcc7ed78432c5cdab1559674b4af035fddbfb2f9a8f9c2112fcef" + sha256: ced76d337f54de33d7d9f06092137b4ac2da5079e00cee8a11a1794ffc7c61c6 url: "https://pub.dev" source: hosted - version: "17.1.2" + version: "17.2.1" flutter_local_notifications_linux: dependency: transitive description: @@ -348,10 +420,10 @@ packages: dependency: transitive description: name: flutter_local_notifications_platform_interface - sha256: "340abf67df238f7f0ef58f4a26d2a83e1ab74c77ab03cd2b2d5018ac64db30b7" + sha256: "85f8d07fe708c1bdcf45037f2c0109753b26ae077e9d9e899d55971711a4ea66" url: "https://pub.dev" source: hosted - version: "7.1.0" + version: "7.2.0" flutter_localizations: dependency: transitive description: flutter @@ -361,10 +433,10 @@ packages: dependency: "direct main" description: name: flutter_markdown - sha256: ff76a9300a06ad1f2b394e54c0b4beaaf6a95f95c98540c918b870221499bb10 + sha256: "2e8a801b1ded5ea001a4529c97b1f213dcb11c6b20668e081cafb23468593514" url: "https://pub.dev" source: hosted - version: "0.7.2" + version: "0.7.3" flutter_plugin_android_lifecycle: dependency: transitive description: @@ -378,6 +450,14 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_typeahead: + dependency: "direct main" + description: + name: flutter_typeahead + sha256: d64712c65db240b1057559b952398ebb6e498077baeebf9b0731dade62438a6d + url: "https://pub.dev" + source: hosted + version: "5.2.0" flutter_web_plugins: dependency: transitive description: flutter @@ -571,10 +651,10 @@ packages: dependency: transitive description: name: path_provider_android - sha256: "9c96da072b421e98183f9ea7464898428e764bc0ce5567f27ec8693442e72514" + sha256: bca87b0165ffd7cdb9cad8edd22d18d2201e886d9a9f19b4fb3452ea7df3a72a url: "https://pub.dev" source: hosted - version: "2.2.5" + version: "2.2.6" path_provider_foundation: dependency: transitive description: @@ -679,6 +759,38 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.8" + pointer_interceptor: + dependency: transitive + description: + name: pointer_interceptor + sha256: d0a8e660d1204eaec5bd34b34cc92174690e076d2e4f893d9d68c486a13b07c4 + url: "https://pub.dev" + source: hosted + version: "0.10.1+1" + pointer_interceptor_ios: + dependency: transitive + description: + name: pointer_interceptor_ios + sha256: a6906772b3205b42c44614fcea28f818b1e5fdad73a4ca742a7bd49818d9c917 + url: "https://pub.dev" + source: hosted + version: "0.10.1" + pointer_interceptor_platform_interface: + dependency: transitive + description: + name: pointer_interceptor_platform_interface + sha256: "0597b0560e14354baeb23f8375cd612e8bd4841bf8306ecb71fcd0bb78552506" + url: "https://pub.dev" + source: hosted + version: "0.10.0+1" + pointer_interceptor_web: + dependency: transitive + description: + name: pointer_interceptor_web + sha256: a6237528b46c411d8d55cdfad8fcb3269fc4cbb26060b14bff94879165887d1e + url: "https://pub.dev" + source: hosted + version: "0.10.2" provider: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 3489b98..d3cfec2 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -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: 1.1.11+2268 +version: 1.1.12+2269 environment: sdk: '>=3.0.0 <4.0.0' @@ -80,6 +80,7 @@ dependencies: ref: master markdown: any + flutter_typeahead: ^5.2.0 dev_dependencies: flutter_test: sdk: flutter