Compare commits

...

5 Commits

Author SHA1 Message Date
Imran Remtulla
5c4bb8f84c Merge pull request #217 from ImranR98/dev
Bugfixes and UI Tweaks (#213, #215, #216)
2023-01-06 21:11:58 -05:00
Imran Remtulla
1c8e759494 Increment version + updated packages 2023-01-06 21:11:13 -05:00
Imran Remtulla
081c2a07d2 Categories re-added on import (#213) 2023-01-06 21:10:04 -05:00
Imran Remtulla
02751fe8fa Made GitHub PATs hidden (password field) (#215) 2023-01-06 20:57:26 -05:00
Imran Remtulla
95f3362a84 Apps bottom bar tweaks (#216) 2023-01-06 20:47:22 -05:00
7 changed files with 449 additions and 387 deletions

View File

@@ -15,6 +15,7 @@ class GitHub extends AppSource {
additionalSourceSpecificSettingFormItems = [ additionalSourceSpecificSettingFormItems = [
GeneratedFormTextField('github-creds', GeneratedFormTextField('github-creds',
label: tr('githubPATLabel'), label: tr('githubPATLabel'),
password: true,
required: false, required: false,
additionalValidators: [ additionalValidators: [
(value) { (value) {

View File

@@ -3,7 +3,6 @@ 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/generated_form_modal.dart'; import 'package:obtainium/components/generated_form_modal.dart';
import 'package:obtainium/providers/settings_provider.dart';
abstract class GeneratedFormItem { abstract class GeneratedFormItem {
late String key; late String key;
@@ -24,6 +23,7 @@ class GeneratedFormTextField extends GeneratedFormItem {
late bool required; late bool required;
late int max; late int max;
late String? hint; late String? hint;
late bool password;
GeneratedFormTextField(String key, GeneratedFormTextField(String key,
{String label = 'Input', {String label = 'Input',
@@ -32,7 +32,8 @@ class GeneratedFormTextField extends GeneratedFormItem {
List<String? Function(String? value)> additionalValidators = const [], List<String? Function(String? value)> additionalValidators = const [],
this.required = true, this.required = true,
this.max = 1, this.max = 1,
this.hint}) this.hint,
this.password = false})
: super(key, : super(key,
label: label, label: label,
belowWidgets: belowWidgets, belowWidgets: belowWidgets,
@@ -129,6 +130,21 @@ class GeneratedForm extends StatefulWidget {
State<GeneratedForm> createState() => _GeneratedFormState(); State<GeneratedForm> createState() => _GeneratedFormState();
} }
// 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 _GeneratedFormState extends State<GeneratedForm> { class _GeneratedFormState extends State<GeneratedForm> {
final _formKey = GlobalKey<FormState>(); final _formKey = GlobalKey<FormState>();
Map<String, dynamic> values = {}; Map<String, dynamic> values = {};
@@ -153,21 +169,6 @@ class _GeneratedFormState extends State<GeneratedForm> {
widget.onValueChanges(returnValues, valid, isBuilding); widget.onValueChanges(returnValues, valid, isBuilding);
} }
// 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();
}
@override @override
void initState() { void initState() {
super.initState(); super.initState();
@@ -188,6 +189,9 @@ class _GeneratedFormState extends State<GeneratedForm> {
if (formItem is GeneratedFormTextField) { if (formItem is GeneratedFormTextField) {
final formFieldKey = GlobalKey<FormFieldState>(); final formFieldKey = GlobalKey<FormFieldState>();
return TextFormField( return TextFormField(
obscureText: formItem.password,
autocorrect: !formItem.password,
enableSuggestions: !formItem.password,
key: formFieldKey, key: formFieldKey,
initialValue: values[formItem.key], initialValue: values[formItem.key],
autovalidateMode: AutovalidateMode.onUserInteraction, autovalidateMode: AutovalidateMode.onUserInteraction,

View File

@@ -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.9.13'; const String currentVersion = '0.9.14';
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

View File

@@ -348,8 +348,9 @@ class AppsPageState extends State<AppsPage> {
Row( Row(
children: [ children: [
selectedApps.isEmpty selectedApps.isEmpty
? IconButton( ? TextButton.icon(
visualDensity: VisualDensity.compact, style:
const ButtonStyle(visualDensity: VisualDensity.compact),
onPressed: () { onPressed: () {
selectThese(sortedApps.map((e) => e.app).toList()); selectThese(sortedApps.map((e) => e.app).toList());
}, },
@@ -357,7 +358,7 @@ class AppsPageState extends State<AppsPage> {
Icons.select_all_outlined, Icons.select_all_outlined,
color: Theme.of(context).colorScheme.primary, color: Theme.of(context).colorScheme.primary,
), ),
tooltip: tr('selectAll')) label: Text(sortedApps.length.toString()))
: TextButton.icon( : TextButton.icon(
style: style:
const ButtonStyle(visualDensity: VisualDensity.compact), const ButtonStyle(visualDensity: VisualDensity.compact),
@@ -375,31 +376,36 @@ class AppsPageState extends State<AppsPage> {
label: Text(selectedApps.length.toString())), label: Text(selectedApps.length.toString())),
const VerticalDivider(), const VerticalDivider(),
Expanded( Expanded(
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly, mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [ children: [
selectedApps.isEmpty IconButton(
? const SizedBox()
: IconButton(
visualDensity: VisualDensity.compact, visualDensity: VisualDensity.compact,
onPressed: () { onPressed: selectedApps.isEmpty
? null
: () {
showDialog<Map<String, dynamic>?>( showDialog<Map<String, dynamic>?>(
context: context, context: context,
builder: (BuildContext ctx) { builder: (BuildContext ctx) {
return GeneratedFormModal( return GeneratedFormModal(
title: tr('removeSelectedAppsQuestion'), title:
tr('removeSelectedAppsQuestion'),
items: const [], items: const [],
initValid: true, initValid: true,
message: tr( message: tr(
'xWillBeRemovedButRemainInstalled', 'xWillBeRemovedButRemainInstalled',
args: [ args: [
plural('apps', selectedApps.length) plural(
'apps', selectedApps.length)
]), ]),
); );
}).then((values) { }).then((values) {
if (values != null) { if (values != null) {
appsProvider.removeApps( appsProvider.removeApps(selectedApps
selectedApps.map((e) => e.id).toList()); .map((e) => e.id)
.toList());
} }
}); });
}, },
@@ -416,50 +422,71 @@ class AppsPageState extends State<AppsPage> {
: () { : () {
HapticFeedback.heavyImpact(); HapticFeedback.heavyImpact();
List<GeneratedFormItem> formItems = []; List<GeneratedFormItem> formItems = [];
if (existingUpdateIdsAllOrSelected.isNotEmpty) { if (existingUpdateIdsAllOrSelected
formItems.add(GeneratedFormSwitch('updates', .isNotEmpty) {
formItems.add(GeneratedFormSwitch(
'updates',
label: tr('updateX', args: [ label: tr('updateX', args: [
plural('apps', plural(
existingUpdateIdsAllOrSelected.length) 'apps',
existingUpdateIdsAllOrSelected
.length)
]), ]),
defaultValue: true)); defaultValue: true));
} }
if (newInstallIdsAllOrSelected.isNotEmpty) { if (newInstallIdsAllOrSelected.isNotEmpty) {
formItems.add(GeneratedFormSwitch('installs', formItems.add(GeneratedFormSwitch(
'installs',
label: tr('installX', args: [ label: tr('installX', args: [
plural('apps', plural(
newInstallIdsAllOrSelected.length) 'apps',
newInstallIdsAllOrSelected
.length)
]), ]),
defaultValue: existingUpdateIdsAllOrSelected defaultValue:
existingUpdateIdsAllOrSelected
.isNotEmpty)); .isNotEmpty));
} }
if (trackOnlyUpdateIdsAllOrSelected.isNotEmpty) { if (trackOnlyUpdateIdsAllOrSelected
formItems.add(GeneratedFormSwitch('trackonlies', .isNotEmpty) {
label: tr('markXTrackOnlyAsUpdated', args: [ formItems.add(GeneratedFormSwitch(
plural('apps', 'trackonlies',
trackOnlyUpdateIdsAllOrSelected.length) label: tr('markXTrackOnlyAsUpdated',
args: [
plural(
'apps',
trackOnlyUpdateIdsAllOrSelected
.length)
]), ]),
defaultValue: existingUpdateIdsAllOrSelected defaultValue:
existingUpdateIdsAllOrSelected
.isNotEmpty || .isNotEmpty ||
newInstallIdsAllOrSelected.isNotEmpty)); newInstallIdsAllOrSelected
.isNotEmpty));
} }
showDialog<Map<String, dynamic>?>( showDialog<Map<String, dynamic>?>(
context: context, context: context,
builder: (BuildContext ctx) { builder: (BuildContext ctx) {
var totalApps = existingUpdateIdsAllOrSelected var totalApps =
existingUpdateIdsAllOrSelected.length +
newInstallIdsAllOrSelected
.length + .length +
newInstallIdsAllOrSelected.length + trackOnlyUpdateIdsAllOrSelected
trackOnlyUpdateIdsAllOrSelected.length; .length;
return GeneratedFormModal( return GeneratedFormModal(
title: tr('changeX', title: tr('changeX', args: [
args: [plural('apps', totalApps)]), plural('apps', totalApps)
items: formItems.map((e) => [e]).toList(), ]),
items: formItems
.map((e) => [e])
.toList(),
initValid: true, initValid: true,
); );
}).then((values) { }).then((values) {
if (values != null) { if (values != null) {
if (values.isEmpty) { if (values.isEmpty) {
values = getDefaultValuesFromFormItems( values =
getDefaultValuesFromFormItems(
[formItems]); [formItems]);
} }
bool shouldInstallUpdates = bool shouldInstallUpdates =
@@ -478,20 +505,22 @@ class AppsPageState extends State<AppsPage> {
.then((_) { .then((_) {
List<String> toInstall = []; List<String> toInstall = [];
if (shouldInstallUpdates) { if (shouldInstallUpdates) {
toInstall toInstall.addAll(
.addAll(existingUpdateIdsAllOrSelected); existingUpdateIdsAllOrSelected);
} }
if (shouldInstallNew) { if (shouldInstallNew) {
toInstall toInstall.addAll(
.addAll(newInstallIdsAllOrSelected); newInstallIdsAllOrSelected);
} }
if (shouldMarkTrackOnlies) { if (shouldMarkTrackOnlies) {
toInstall.addAll( toInstall.addAll(
trackOnlyUpdateIdsAllOrSelected); trackOnlyUpdateIdsAllOrSelected);
} }
appsProvider appsProvider
.downloadAndInstallLatestApps(toInstall, .downloadAndInstallLatestApps(
globalNavigatorKey.currentContext) toInstall,
globalNavigatorKey
.currentContext)
.catchError((e) { .catchError((e) {
showError(e, context); showError(e, context);
}); });
@@ -505,16 +534,17 @@ class AppsPageState extends State<AppsPage> {
icon: const Icon( icon: const Icon(
Icons.file_download_outlined, Icons.file_download_outlined,
)), )),
selectedApps.isEmpty IconButton(
? const SizedBox()
: IconButton(
visualDensity: VisualDensity.compact, visualDensity: VisualDensity.compact,
onPressed: () async { onPressed: selectedApps.isEmpty
? null
: () async {
try { try {
Set<String>? preselected; Set<String>? preselected;
var showPrompt = false; var showPrompt = false;
for (var element in selectedApps) { for (var element in selectedApps) {
var currentCats = element.categories.toSet(); var currentCats =
element.categories.toSet();
if (preselected == null) { if (preselected == null) {
preselected = currentCats; preselected = currentCats;
} else { } else {
@@ -527,15 +557,16 @@ class AppsPageState extends State<AppsPage> {
} }
var cont = true; var cont = true;
if (showPrompt) { if (showPrompt) {
cont = await showDialog<Map<String, dynamic>?>( cont = await showDialog<
Map<String, dynamic>?>(
context: context, context: context,
builder: (BuildContext ctx) { builder: (BuildContext ctx) {
return GeneratedFormModal( return GeneratedFormModal(
title: tr('categorize'), title: tr('categorize'),
items: const [], items: const [],
initValid: true, initValid: true,
message: message: tr(
tr('selectedCategorizeWarning'), 'selectedCategorizeWarning'),
); );
}) != }) !=
null; null;
@@ -548,7 +579,8 @@ class AppsPageState extends State<AppsPage> {
title: tr('categorize'), title: tr('categorize'),
items: const [], items: const [],
initValid: true, initValid: true,
singleNullReturnButton: tr('continue'), singleNullReturnButton:
tr('continue'),
additionalWidgets: [ additionalWidgets: [
CategoryEditorSelector( CategoryEditorSelector(
preselected: !showPrompt preselected: !showPrompt
@@ -556,8 +588,8 @@ class AppsPageState extends State<AppsPage> {
: {}, : {},
showLabelWhenNotEmpty: false, showLabelWhenNotEmpty: false,
onSelected: (categories) { onSelected: (categories) {
appsProvider appsProvider.saveApps(
.saveApps(selectedApps.map((e) { selectedApps.map((e) {
e.categories = categories; e.categories = categories;
return e; return e;
}).toList()); }).toList());
@@ -574,30 +606,32 @@ class AppsPageState extends State<AppsPage> {
tooltip: tr('categorize'), tooltip: tr('categorize'),
icon: const Icon(Icons.category_outlined), icon: const Icon(Icons.category_outlined),
), ),
selectedApps.isEmpty IconButton(
? const SizedBox()
: IconButton(
visualDensity: VisualDensity.compact, visualDensity: VisualDensity.compact,
onPressed: () { onPressed: selectedApps.isEmpty
? null
: () {
showDialog( showDialog(
context: context, context: context,
builder: (BuildContext ctx) { builder: (BuildContext ctx) {
return AlertDialog( return AlertDialog(
scrollable: true, scrollable: true,
content: Padding( content: Padding(
padding: const EdgeInsets.only(top: 6), padding:
const EdgeInsets.only(top: 6),
child: Row( child: Row(
mainAxisAlignment: mainAxisAlignment:
MainAxisAlignment.spaceAround, MainAxisAlignment
.spaceAround,
children: [ children: [
IconButton( IconButton(
onPressed: onPressed: appsProvider
appsProvider
.areDownloadsRunning() .areDownloadsRunning()
? null ? null
: () { : () {
showDialog( showDialog(
context: context, context:
context,
builder: builder:
(BuildContext (BuildContext
ctx) { ctx) {
@@ -605,47 +639,39 @@ class AppsPageState extends State<AppsPage> {
title: Text(tr( title: Text(tr(
'markXSelectedAppsAsUpdated', 'markXSelectedAppsAsUpdated',
args: [ args: [
selectedApps selectedApps.length.toString()
.length
.toString()
])), ])),
content: Text( content:
Text(
tr('onlyWorksWithNonEVDApps'), tr('onlyWorksWithNonEVDApps'),
style: const TextStyle( style: const TextStyle(
fontWeight: fontWeight:
FontWeight FontWeight.bold,
.bold, fontStyle: FontStyle.italic),
fontStyle:
FontStyle.italic),
), ),
actions: [ actions: [
TextButton( TextButton(
onPressed: onPressed:
() { () {
Navigator.of(context) Navigator.of(context).pop();
.pop();
}, },
child: Text( child:
tr('no'))), Text(tr('no'))),
TextButton( TextButton(
onPressed: onPressed:
() { () {
HapticFeedback HapticFeedback.selectionClick();
.selectionClick(); appsProvider.saveApps(selectedApps.map((a) {
appsProvider if (a.installedVersion != null) {
.saveApps(selectedApps.map((a) {
if (a.installedVersion !=
null) {
a.installedVersion = a.latestVersion; a.installedVersion = a.latestVersion;
} }
return a; return a;
}).toList()); }).toList());
Navigator.of(context) Navigator.of(context).pop();
.pop();
}, },
child: Text( child:
tr('yes'))) Text(tr('yes')))
], ],
); );
}).whenComplete(() { }).whenComplete(() {
@@ -654,21 +680,25 @@ class AppsPageState extends State<AppsPage> {
.pop(); .pop();
}); });
}, },
tooltip: tooltip: tr(
tr('markSelectedAppsUpdated'), 'markSelectedAppsUpdated'),
icon: const Icon(Icons.done)), icon: const Icon(
Icons.done)),
IconButton( IconButton(
onPressed: () { onPressed: () {
var pinStatus = selectedApps var pinStatus =
selectedApps
.where((element) => .where((element) =>
element.pinned) element
.pinned)
.isEmpty; .isEmpty;
appsProvider.saveApps( appsProvider.saveApps(
selectedApps.map((e) { selectedApps.map((e) {
e.pinned = pinStatus; e.pinned = pinStatus;
return e; return e;
}).toList()); }).toList());
Navigator.of(context).pop(); Navigator.of(context)
.pop();
}, },
tooltip: selectedApps tooltip: selectedApps
.where((element) => .where((element) =>
@@ -680,14 +710,16 @@ class AppsPageState extends State<AppsPage> {
.where((element) => .where((element) =>
element.pinned) element.pinned)
.isEmpty .isEmpty
? Icons.bookmark_outline_rounded ? Icons
.bookmark_outline_rounded
: Icons : Icons
.bookmark_remove_outlined), .bookmark_remove_outlined),
), ),
IconButton( IconButton(
onPressed: () { onPressed: () {
String urls = ''; String urls = '';
for (var a in selectedApps) { for (var a
in selectedApps) {
urls += '${a.url}\n'; urls += '${a.url}\n';
} }
urls = urls.substring( urls = urls.substring(
@@ -695,16 +727,20 @@ class AppsPageState extends State<AppsPage> {
Share.share(urls, Share.share(urls,
subject: tr( subject: tr(
'selectedAppURLsFromObtainium')); 'selectedAppURLsFromObtainium'));
Navigator.of(context).pop(); Navigator.of(context)
.pop();
}, },
tooltip: tr('shareSelectedAppURLs'), tooltip: tr(
icon: const Icon(Icons.share), 'shareSelectedAppURLs'),
icon:
const Icon(Icons.share),
), ),
IconButton( IconButton(
onPressed: () { onPressed: () {
showDialog( showDialog(
context: context, context: context,
builder: (BuildContext ctx) { builder: (BuildContext
ctx) {
return GeneratedFormModal( return GeneratedFormModal(
title: tr( title: tr(
'resetInstallStatusForSelectedAppsQuestion'), 'resetInstallStatusForSelectedAppsQuestion'),
@@ -722,18 +758,22 @@ class AppsPageState extends State<AppsPage> {
}).then((values) { }).then((values) {
if (values != null) { if (values != null) {
appsProvider.saveApps( appsProvider.saveApps(
selectedApps.map((e) { selectedApps
e.installedVersion = null; .map((e) {
e.installedVersion =
null;
return e; return e;
}).toList()); }).toList());
} }
}).whenComplete(() { }).whenComplete(() {
Navigator.of(context).pop(); Navigator.of(context)
.pop();
}); });
}, },
tooltip: tr('resetInstallStatus'), tooltip: tr(
icon: const Icon( 'resetInstallStatus'),
Icons.restore_page_outlined), icon: const Icon(Icons
.restore_page_outlined),
), ),
]), ]),
), ),
@@ -744,7 +784,7 @@ class AppsPageState extends State<AppsPage> {
icon: const Icon(Icons.more_horiz), icon: const Icon(Icons.more_horiz),
), ),
], ],
)), ))),
const VerticalDivider(), const VerticalDivider(),
IconButton( IconButton(
visualDensity: VisualDensity.compact, visualDensity: VisualDensity.compact,

View File

@@ -9,6 +9,7 @@ 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/providers/apps_provider.dart'; import 'package:obtainium/providers/apps_provider.dart';
import 'package:obtainium/providers/settings_provider.dart';
import 'package:obtainium/providers/source_provider.dart'; import 'package:obtainium/providers/source_provider.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:file_picker/file_picker.dart'; import 'package:file_picker/file_picker.dart';
@@ -28,6 +29,7 @@ class _ImportExportPageState extends State<ImportExportPage> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
SourceProvider sourceProvider = SourceProvider(); SourceProvider sourceProvider = SourceProvider();
var appsProvider = context.read<AppsProvider>(); var appsProvider = context.read<AppsProvider>();
var settingsProvider = context.read<SettingsProvider>();
var outlineButtonStyle = ButtonStyle( var outlineButtonStyle = ButtonStyle(
shape: MaterialStateProperty.all( shape: MaterialStateProperty.all(
StadiumBorder( StadiumBorder(
@@ -100,6 +102,21 @@ class _ImportExportPageState extends State<ImportExportPage> {
appsProvider appsProvider
.importApps(data) .importApps(data)
.then((value) { .then((value) {
var cats =
settingsProvider.categories;
appsProvider.apps
.forEach((key, value) {
for (var c
in value.app.categories) {
if (!cats.containsKey(c)) {
cats[c] =
generateRandomLightColor()
.value;
}
}
});
settingsProvider.categories =
cats;
showError( showError(
tr('importedX', args: [ tr('importedX', args: [
plural('apps', value) plural('apps', value)

View File

@@ -56,7 +56,7 @@ packages:
name: checked_yaml name: checked_yaml
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.0.1" version: "2.0.2"
cli_util: cli_util:
dependency: transitive dependency: transitive
description: description:
@@ -286,7 +286,7 @@ packages:
name: image name: image
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "3.2.2" version: "3.3.0"
install_plugin_v2: install_plugin_v2:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -559,7 +559,7 @@ packages:
name: shared_preferences_macos name: shared_preferences_macos
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.0.4" version: "2.0.5"
shared_preferences_platform_interface: shared_preferences_platform_interface:
dependency: transitive dependency: transitive
description: description:
@@ -655,7 +655,7 @@ packages:
name: timezone name: timezone
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.9.0" version: "0.9.1"
typed_data: typed_data:
dependency: transitive dependency: transitive
description: 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 # 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.9.13+103 # When changing this, update the tag in main() accordingly version: 0.9.14+104 # 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'