Merge pull request #1696 from ImranR98/dev

- Bugfix: Pull to refresh not working with few apps (#1680)
- Add third-party F-Droid repo search to main search menu (#1681)
- Added autocomplete for F-Droid repos (#1681)
- Bugfix: Missing request headers for direct APK link apps (#1688)
- Release asset download confirmation even for single choice (#1694)
- Add a less obvious touch target to highlights (#1694)
This commit is contained in:
Imran
2024-06-28 23:03:14 -04:00
committed by GitHub
11 changed files with 345 additions and 89 deletions

View File

@@ -24,6 +24,14 @@ class DirectAPKLink extends AppSource {
];
}
@override
Future<Map<String, String>?> getRequestHeaders(
Map<String, dynamic> additionalSettings,
{bool forAPKDownload = false}) {
return html.getRequestHeaders(additionalSettings,
forAPKDownload: forAPKDownload);
}
@override
Future<APKDetails> getLatestAPKDetails(
String standardUrl,

View File

@@ -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) {

View File

@@ -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')));
}

View File

@@ -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<String>? 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<GeneratedForm> {
var formItem = e.value;
if (formItem is GeneratedFormTextField) {
final formFieldKey = GlobalKey<FormFieldState>();
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<String>(
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) {

View File

@@ -51,10 +51,13 @@ class AddAppPageState extends State<AddAppPage> {
}
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<AddAppPage> {
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<AddAppPage> {
searching = true;
});
var sourceStrings = <String, List<String>>{};
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<AddAppPage> {
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<MapEntry<String, Map<String, List<String>>>?> results =
(await Future.wait(sourceProvider.sources
.where((e) => searchSources.contains(e.name))
.map((e) async {
try {
return await e.search(searchQuery);
Map<String, dynamic>? querySettings = {};
if (e.includeAdditionalOptsInMainSearch) {
querySettings = await showDialog<Map<String, dynamic>?>(
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 <String, List<String>>{};
return null;
}
}
}));
})))
.where((a) => a != null)
.toList();
// Interleave results instead of simple reduce
Map<String, List<String>> res = {};
Map<String, MapEntry<String, List<String>>> 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<AddAppPage> {
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<AddAppPage> {
[
GeneratedFormDropdown(
'overrideSource',
defaultValue: '',
defaultValue: pickedSourceOverride ?? '',
[
MapEntry('', tr('none')),
...sourceProvider.sources.map(

View File

@@ -161,25 +161,46 @@ class _AppPageState extends State<AppPage> {
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,
),

View File

@@ -466,7 +466,7 @@ class AppsPageState extends State<AppsPage> {
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<AppsPage> {
interactive: true,
controller: scrollController,
child: CustomScrollView(
physics: const AlwaysScrollableScrollPhysics(),
controller: scrollController,
slivers: <Widget>[
CustomAppBar(title: tr('appsString')),

View File

@@ -717,7 +717,8 @@ class AppsProvider with ChangeNotifier {
}
Future<MapEntry<String, String>?> 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<String> 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<AppFilePicker> {
? 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<String>(

View File

@@ -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<GeneratedFormItem> searchQuerySettingFormItems = [];
Future<Map<String, List<String>>> search(String query,
{Map<String, dynamic> querySettings = const {}}) {

View File

@@ -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:

View File

@@ -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