Merge pull request #484 from 12people/flatpak

Flatpak updates
This commit is contained in:
Roland Geider
2024-01-10 19:01:16 +01:00
committed by GitHub
15 changed files with 320 additions and 267 deletions

View File

@@ -12,10 +12,9 @@ jobs:
uses: actions/checkout@v4
- name: Setup Java
uses: actions/setup-java@v4
uses: actions/setup-java@v3
with:
java-version: '17'
distribution: 'oracle'
java-version: 17.x
- name: Setup Ruby
uses: ruby/setup-ruby@v1
@@ -64,7 +63,7 @@ jobs:
flutter build linux --release
cd flatpak/scripts
dart pub get
dart flatpak_packager.dart --meta ../flatpak_meta.json --github
dart flatpak_packager.dart --meta ../flatpak_meta.json --github --addTodaysVersion ${{ env.VERSION }}
- name: Build AAB
run: flutter build appbundle --release

View File

@@ -3,14 +3,8 @@
"lowercaseAppName": "wger",
"githubReleaseOrganization": "wger-project",
"githubReleaseProject": "flutter",
"localReleases": [
],
"localReleaseAssets": [
],
"localLinuxBuildDir": "../build/linux",
"appDataPath": "de.wger.flutter.appdata.xml",
"appStreamPath": "de.wger.flutter.metainfo.xml",
"desktopPath": "de.wger.flutter.desktop",
"icons": {
"64x64": "logo64.png",

View File

@@ -1,17 +1,27 @@
// ignore_for_file: avoid_print
import 'dart:io';
import 'flatpak_shared.dart';
/// Creates an archive containing all the sources for the Flatpak package for a
/// specific architecture.
///
/// arguments:
/// --meta [file]
/// Required argument for providing the metadata file for this script.
/// --github
/// Use this option to pull release info from Github rather than the metadata file.
/// --addTodaysVersion [version]
/// If pulling data from Github, this provides a way to specify the release to be released today.
void main(List<String> arguments) async {
if (Platform.isWindows) {
throw Exception('Must be run under a UNIX-like operating system.');
if (!Platform.isLinux) {
throw Exception('Must be run under Linux');
}
// PARSE ARGUMENTS
final metaIndex = arguments.indexOf('--meta');
if (metaIndex == -1) {
throw Exception(
@@ -22,123 +32,136 @@ void main(List<String> arguments) async {
}
final metaFile = File(arguments[metaIndex + 1]);
if (!metaFile.existsSync()) {
if (!(await metaFile.exists())) {
throw Exception('The provided metadata file does not exist.');
}
final meta = FlatpakMeta.fromJson(metaFile);
final fetchFromGithub = arguments.contains('--github');
final addTodaysVersionIndex = arguments.indexOf('--addTodaysVersion');
if (addTodaysVersionIndex != -1 && arguments.length == addTodaysVersionIndex + 1) {
throw Exception('The --addTodaysVersion flag must be followed by the version name.');
}
final addedTodaysVersion =
addTodaysVersionIndex != -1 ? arguments[addTodaysVersionIndex + 1] : null;
// GENERATE PACKAGE
final meta = FlatpakMeta.fromJson(metaFile, skipLocalReleases: fetchFromGithub);
final outputDir = Directory('${Directory.current.path}/flatpak_generator_exports');
outputDir.createSync();
await outputDir.create();
final packageGenerator = PackageGenerator(inputDir: metaFile.parent, meta: meta);
final packageGenerator = PackageGenerator(
inputDir: metaFile.parent, meta: meta, addedTodaysVersion: addedTodaysVersion);
packageGenerator.generatePackage(
outputDir,
PackageGenerator.runningOnARM() ? CPUArchitecture.aarch64 : CPUArchitecture.x86_64,
fetchFromGithub,
);
await packageGenerator.generatePackage(
outputDir,
await PackageGenerator.runningOnARM() ? CPUArchitecture.aarch64 : CPUArchitecture.x86_64,
fetchFromGithub);
}
class PackageGenerator {
final Directory inputDir;
final FlatpakMeta meta;
final Map<CPUArchitecture, String> shaByArch = {};
final String? addedTodaysVersion;
PackageGenerator({required this.inputDir, required this.meta});
PackageGenerator({required this.inputDir, required this.meta, required this.addedTodaysVersion});
Future<void> generatePackage(
Directory outputDir, CPUArchitecture arch, bool fetchReleasesFromGithub) async {
final tempDir = outputDir.createTempSync('flutter_generator_temp');
final tempDir = await outputDir.createTemp('flutter_generator_temp');
final appId = meta.appId;
// desktop file
final desktopFile = File('${inputDir.path}/${meta.desktopPath}');
if (!desktopFile.existsSync()) {
if (!(await desktopFile.exists())) {
throw Exception(
'The desktop file does not exist under the specified path: ${desktopFile.path}');
}
desktopFile.copySync('${tempDir.path}/$appId.desktop');
await desktopFile.copy('${tempDir.path}/$appId.desktop');
// icons
final iconTempDir = Directory('${tempDir.path}/icons');
for (final icon in meta.icons) {
final iconFile = File('${inputDir.path}/${icon.path}');
if (!iconFile.existsSync()) {
if (!(await iconFile.exists())) {
throw Exception('The icon file ${iconFile.path} does not exist.');
}
final iconSubdir = Directory('${iconTempDir.path}/${icon.type}');
iconSubdir.createSync(recursive: true);
iconFile.copySync('${iconSubdir.path}/${icon.getFilename(appId)}');
await iconSubdir.create(recursive: true);
await iconFile.copy('${iconSubdir.path}/${icon.getFilename(appId)}');
}
// appdata file
final origAppDataFile = File('${inputDir.path}/${meta.appDataPath}');
if (!origAppDataFile.existsSync()) {
// AppStream metainfo file
final origAppStreamFile = File('${inputDir.path}/${meta.appStreamPath}');
if (!(await origAppStreamFile.exists())) {
throw Exception(
'The app data file does not exist under the specified path: ${origAppDataFile.path}');
'The app data file does not exist under the specified path: ${origAppStreamFile.path}');
}
final editedAppDataContent = AppDataModifier.replaceVersions(
origAppDataFile.readAsStringSync(), await meta.getReleases(fetchReleasesFromGithub));
final editedAppStreamContent = AppStreamModifier.replaceVersions(
await origAppStreamFile.readAsString(),
await meta.getReleases(fetchReleasesFromGithub, addedTodaysVersion));
final editedAppDataFile = File('${tempDir.path}/$appId.appdata.xml');
editedAppDataFile.writeAsStringSync(editedAppDataContent);
final editedAppStreamFile = File('${tempDir.path}/$appId.metainfo.xml');
await editedAppStreamFile.writeAsString(editedAppStreamContent);
// build files
final bundlePath =
'${inputDir.path}/${meta.localLinuxBuildDir}/${arch.flutterDirName}/release/bundle';
final buildDir = Directory(bundlePath);
if (!buildDir.existsSync()) {
if (!(await buildDir.exists())) {
throw Exception(
'The linux build directory does not exist under the specified path: ${buildDir.path}');
}
final destDir = Directory('${tempDir.path}/bin');
destDir.createSync();
await destDir.create();
final baseFileName = '${meta.lowercaseAppName}-linux-${arch.flatpakArchCode}';
final baseFilename = '${meta.lowercaseAppName}-linux-${arch.flatpakArchCode}';
final packagePath = '${outputDir.absolute.path}/$baseFilename.tar.gz';
final shaPath = '${outputDir.absolute.path}/$baseFilename.sha256';
await Process.run('cp', ['-r', '${buildDir.absolute.path}/.', destDir.absolute.path]);
await Process.run('tar', ['-czvf', packagePath, '.'], workingDirectory: tempDir.absolute.path);
final packagePath = '${outputDir.absolute.path}/$baseFileName.tar.gz';
Process.runSync('cp', ['-r', '${buildDir.absolute.path}/.', destDir.absolute.path]);
Process.runSync('tar', ['-czvf', packagePath, '.'], workingDirectory: tempDir.absolute.path);
print('Generated $packagePath');
final preShasum = Process.runSync('shasum', ['-a', '256', packagePath]);
final sha256 = preShasum.stdout.toString().split(' ').first;
final preShasum = await Process.run('shasum', ['-a', '256', packagePath]);
final shasum = preShasum.stdout.toString().split(' ').first;
final shaFile = await File('${outputDir.path}/$baseFileName.sha256').writeAsString(sha256);
print('Generated ${shaFile.path}');
await File(shaPath).writeAsString(shasum);
shaByArch.putIfAbsent(arch, () => sha256);
print('Generated $shaPath');
tempDir.deleteSync(recursive: true);
await tempDir.delete(recursive: true);
}
static bool runningOnARM() {
final unameRes = Process.runSync('uname', ['-m']);
static Future<bool> runningOnARM() async {
final unameRes = await Process.run('uname', ['-m']);
final unameString = unameRes.stdout.toString().trimLeft();
return unameString.startsWith('arm') || unameString.startsWith('aarch');
}
}
// updates releases in ${appName}.appdata.xml
class AppDataModifier {
static String replaceVersions(String origAppDataContent, List<Release> versions) {
// updates releases in ${appName}.metainfo.xml
class AppStreamModifier {
static String replaceVersions(String origAppStreamContent, List<Release> versions) {
final joinedReleases =
versions.map((v) => '\t\t<release version="${v.version}" date="${v.date}" />').join('\n');
final releasesSection = '<releases>\n$joinedReleases\n\t</releases>'; //todo check this
if (origAppDataContent.contains('<releases')) {
return origAppDataContent
final releasesSection = '<releases>\n$joinedReleases\n\t</releases>'; //TODO check this
if (origAppStreamContent.contains('<releases')) {
return origAppStreamContent
.replaceAll('\n', '<~>')
.replaceFirst(RegExp('<releases.*</releases>'), releasesSection)
.replaceAll('<~>', '\n');
} else {
return origAppDataContent.replaceFirst('</component>', '\n\t$releasesSection\n</component>');
return origAppStreamContent.replaceFirst(
'</component>', '\n\t$releasesSection\n</component>');
}
}
}

View File

@@ -3,20 +3,21 @@ import 'dart:io';
import 'package:http/http.dart' as http;
/// Shared files for the two Flatpak-related scripts.
class Release {
final String version;
final String date; //todo add resources
final String date; //TODO add resources
Release({required this.version, required this.date});
}
enum CPUArchitecture {
x86_64('x86_64', 'x64'),
aarch64('aarch64', 'aarch64');
aarch64('aarch64', 'arm64');
final String flatpakArchCode;
final String flutterDirName;
const CPUArchitecture(this.flatpakArchCode, this.flutterDirName);
}
@@ -43,8 +44,9 @@ class Icon {
_fileExtension = path.split('.').last;
}
String getFilename(String appId) =>
(type == _symbolicType) ? '$appId-symbolic.$_fileExtension' : '$appId.$_fileExtension';
String getFilename(String appId) => (type == _symbolicType)
? '$appId-symbolic.$_fileExtension'
: '$appId.$_fileExtension';
}
class GithubReleases {
@@ -55,25 +57,26 @@ class GithubReleases {
GithubReleases(this.githubReleaseOrganization, this.githubReleaseProject);
Future<List<Release>> getReleases() async {
Future<List<Release>> getReleases(bool canBeEmpty) async {
if (_releases == null) {
await _fetchReleasesAndAssets();
await _fetchReleasesAndAssets(canBeEmpty);
}
return _releases!;
}
Future<List<ReleaseAsset>?> getLatestReleaseAssets() async {
if (_releases == null) {
await _fetchReleasesAndAssets();
await _fetchReleasesAndAssets(false);
}
return _latestReleaseAssets;
}
Future<void> _fetchReleasesAndAssets() async {
Future<void> _fetchReleasesAndAssets(bool canBeEmpty) async {
final releaseJsonContent = (await http.get(Uri(
scheme: 'https',
host: 'api.github.com',
path: '/repos/$githubReleaseOrganization/$githubReleaseProject/releases')))
path:
'/repos/$githubReleaseOrganization/$githubReleaseProject/releases')))
.body;
final decodedJson = jsonDecode(releaseJsonContent) as List;
@@ -84,27 +87,33 @@ class GithubReleases {
await Future.forEach<dynamic>(decodedJson, (dynamic releaseDynamic) async {
final releaseMap = releaseDynamic as Map;
final releaseDateAndTime = DateTime.parse(releaseMap['published_at'] as String);
final releaseDateString = releaseDateAndTime.toIso8601String().split('T').first;
final releaseDateAndTime =
DateTime.parse(releaseMap['published_at'] as String);
final releaseDateString =
releaseDateAndTime.toIso8601String().split('T').first;
if (latestReleaseAssetDate == null ||
(latestReleaseAssetDate?.compareTo(releaseDateAndTime) == -1)) {
final assets = await _parseReleaseAssets(releaseMap['assets'] as List);
final assets =
await _parseGithubReleaseAssets(releaseMap['assets'] as List);
if (assets != null) {
_latestReleaseAssets = assets;
latestReleaseAssetDate = releaseDateAndTime;
}
}
releases.add(Release(version: releaseMap['name'] as String, date: releaseDateString));
releases.add(Release(
version: releaseMap['name'] as String, date: releaseDateString));
});
if (releases.isNotEmpty) {
if (releases.isNotEmpty || canBeEmpty) {
_releases = releases;
} else {
throw Exception("Github must contain at least 1 release.");
}
}
Future<List<ReleaseAsset>?> _parseReleaseAssets(List assetMaps) async {
Future<List<ReleaseAsset>?> _parseGithubReleaseAssets(List assetMaps) async {
String? x64TarballUrl;
String? x64Sha;
String? aarch64TarballUrl;
@@ -115,7 +124,8 @@ class GithubReleases {
final downloadUrl = amMap['browser_download_url'] as String;
final filename = amMap['name'] as String;
final fileExtension = filename.substring(filename.indexOf('.') + 1);
final filenameWithoutExtension = filename.substring(0, filename.indexOf('.'));
final filenameWithoutExtension =
filename.substring(0, filename.indexOf('.'));
final arch = filenameWithoutExtension.endsWith('aarch64')
? CPUArchitecture.aarch64
@@ -166,15 +176,14 @@ class GithubReleases {
return res.isEmpty ? null : res;
}
Future<String> _readSha(String shaUrl) async {
return (await http.get(Uri.parse(shaUrl))).body.split(' ').first;
}
Future<String> _readSha(String shaUrl) async =>
(await http.get(Uri.parse(shaUrl))).body.split(' ').first;
}
class FlatpakMeta {
final String appId;
final String lowercaseAppName;
final String appDataPath;
final String appStreamPath;
final String desktopPath;
final List<Icon> icons;
@@ -202,7 +211,7 @@ class FlatpakMeta {
required List<Release>? localReleases,
required List<ReleaseAsset>? localReleaseAssets,
required this.localLinuxBuildDir,
required this.appDataPath,
required this.appStreamPath,
required this.desktopPath,
required this.icons,
required this.freedesktopRuntime,
@@ -212,88 +221,117 @@ class FlatpakMeta {
: _localReleases = localReleases,
_localReleaseAssets = localReleaseAssets {
if (githubReleaseOrganization != null && githubReleaseProject != null) {
_githubReleases = GithubReleases(githubReleaseOrganization!, githubReleaseProject!);
_githubReleases =
GithubReleases(githubReleaseOrganization!, githubReleaseProject!);
}
}
Future<List<Release>> getReleases(bool fetchReleasesFromGithub) async {
Future<List<Release>> getReleases(
bool fetchReleasesFromGithub, String? addedTodaysVersion) async {
final releases = List<Release>.empty(growable: true);
if (addedTodaysVersion != null) {
releases.add(Release(
version: addedTodaysVersion,
date: DateTime.now().toIso8601String().split("T").first));
}
if (fetchReleasesFromGithub) {
if (_githubReleases == null) {
throw Exception(
'Metadata must include Github repository info if fetching releases from Github.');
}
return await _githubReleases!.getReleases();
releases.addAll(
await _githubReleases!.getReleases(addedTodaysVersion != null));
} else {
if (_localReleases == null) {
throw Exception('Metadata must include releases if not fetching releases from Github.');
if (_localReleases == null && addedTodaysVersion == null) {
throw Exception(
'Metadata must include releases if not fetching releases from Github.');
}
if (_localReleases?.isNotEmpty ?? false) {
releases.addAll(_localReleases!);
}
return _localReleases!;
}
return releases;
}
Future<List<ReleaseAsset>?> getReleaseAssets(bool fetchReleasesFromGithub) async {
Future<List<ReleaseAsset>?> getLatestReleaseAssets(
bool fetchReleasesFromGithub) async {
if (fetchReleasesFromGithub) {
if (_githubReleases == null) {
throw Exception(
'Metadata must include Github repository info if fetching releases from Github.');
}
return _githubReleases!.getLatestReleaseAssets();
return await _githubReleases!.getLatestReleaseAssets();
} else {
if (_localReleases == null) {
throw Exception('Metadata must include releases if not fetching releases from Github.');
throw Exception(
'Metadata must include releases if not fetching releases from Github.');
}
return _localReleaseAssets;
}
}
static FlatpakMeta fromJson(File jsonFile) {
static FlatpakMeta fromJson(File jsonFile, {bool skipLocalReleases = false}) {
try {
final dynamic json = jsonDecode(jsonFile.readAsStringSync());
return FlatpakMeta(
appId: json['appId'] as String,
lowercaseAppName: json['lowercaseAppName'] as String,
githubReleaseOrganization: json['githubReleaseOrganization'] as String?,
githubReleaseOrganization:
json['githubReleaseOrganization'] as String?,
githubReleaseProject: json['githubReleaseProject'] as String?,
localReleases: (json['localReleases'] as List?)?.map((dynamic r) {
final rMap = r as Map;
return Release(version: rMap['version'] as String, date: rMap['date'] as String);
}).toList(),
localReleaseAssets: (json['localReleaseAssets'] as List?)?.map((dynamic ra) {
final raMap = ra as Map;
final archString = raMap['arch'] as String;
final arch = (archString == CPUArchitecture.x86_64.flatpakArchCode)
? CPUArchitecture.x86_64
: (archString == CPUArchitecture.aarch64.flatpakArchCode)
? CPUArchitecture.aarch64
: null;
if (arch == null) {
throw Exception(
'Architecture must be either "${CPUArchitecture.x86_64.flatpakArchCode}" or "${CPUArchitecture.aarch64.flatpakArchCode}"');
}
final tarballPath = '${jsonFile.parent.path}/${raMap['tarballPath'] as String}';
final preShasum = Process.runSync('shasum', ['-a', '256', tarballPath]);
final shasum = preShasum.stdout.toString().split(' ').first;
if (preShasum.exitCode != 0) {
throw Exception(preShasum.stderr);
}
return ReleaseAsset(
arch: arch,
tarballUrlOrPath: tarballPath,
isRelativeLocalPath: true,
tarballSha256: shasum);
}).toList(),
localReleases: skipLocalReleases
? null
: (json['localReleases'] as List?)?.map((dynamic r) {
final rMap = r as Map;
return Release(
version: rMap['version'] as String,
date: rMap['date'] as String);
}).toList(),
localReleaseAssets: skipLocalReleases
? null
: (json['localReleaseAssets'] as List?)?.map((dynamic ra) {
final raMap = ra as Map;
final archString = raMap['arch'] as String;
final arch = (archString ==
CPUArchitecture.x86_64.flatpakArchCode)
? CPUArchitecture.x86_64
: (archString == CPUArchitecture.aarch64.flatpakArchCode)
? CPUArchitecture.aarch64
: null;
if (arch == null) {
throw Exception(
'Architecture must be either "${CPUArchitecture.x86_64.flatpakArchCode}" or "${CPUArchitecture.aarch64.flatpakArchCode}"');
}
final tarballFile = File(
'${jsonFile.parent.path}/${raMap['tarballPath'] as String}');
final tarballPath = tarballFile.absolute.path;
final preShasum =
Process.runSync('shasum', ['-a', '256', tarballPath]);
final shasum = preShasum.stdout.toString().split(' ').first;
if (preShasum.exitCode != 0) {
throw Exception(preShasum.stderr);
}
return ReleaseAsset(
arch: arch,
tarballUrlOrPath: tarballPath,
isRelativeLocalPath: true,
tarballSha256: shasum);
}).toList(),
localLinuxBuildDir: json['localLinuxBuildDir'] as String,
appDataPath: json['appDataPath'] as String,
appStreamPath: json['appStreamPath'] as String,
desktopPath: json['desktopPath'] as String,
icons: (json['icons'] as Map).entries.map((mapEntry) {
return Icon(type: mapEntry.key as String, path: mapEntry.value as String);
return Icon(
type: mapEntry.key as String, path: mapEntry.value as String);
}).toList(),
freedesktopRuntime: json['freedesktopRuntime'] as String,
buildCommandsAfterUnpack: (json['buildCommandsAfterUnpack'] as List?)
?.map((dynamic bc) => bc as String)
.toList(),
extraModules: json['extraModules'] as List?,
finishArgs: (json['finishArgs'] as List).map((dynamic fa) => fa as String).toList());
finishArgs: (json['finishArgs'] as List)
.map((dynamic fa) => fa as String)
.toList());
} catch (e) {
throw Exception('Could not parse JSON file, due to this error:\n$e');
}

View File

@@ -1,8 +1,20 @@
// ignore_for_file: avoid_print
import 'dart:convert';
import 'dart:io';
import 'flatpak_shared.dart';
/// Generates the Flatpak manifest.
/// (Separate from the package generation, as those are generated per each
/// architecture.)
///
/// arguments:
/// --meta [file]
/// Required argument for providing the metadata file for this script.
/// --github
/// Use this option to pull release info from Github rather than the metadata file.
void main(List<String> arguments) async {
if (Platform.isWindows) {
throw Exception('Must be run under a UNIX-like operating system.');
@@ -14,7 +26,8 @@ void main(List<String> arguments) async {
'You must run this script with a metadata file argument, using the --meta flag.');
}
if (arguments.length == metaIndex + 1) {
throw Exception('The --meta flag must be followed by the path to the metadata file.');
throw Exception(
'The --meta flag must be followed by the path to the metadata file.');
}
final metaFile = File(arguments[metaIndex + 1]);
@@ -22,21 +35,25 @@ void main(List<String> arguments) async {
throw Exception('The provided metadata file does not exist.');
}
final meta = FlatpakMeta.fromJson(metaFile);
final fetchFromGithub = arguments.contains('--github');
final outputDir = Directory('${Directory.current.path}/flatpak_generator_exports');
final meta =
FlatpakMeta.fromJson(metaFile, skipLocalReleases: fetchFromGithub);
final outputDir =
Directory('${Directory.current.path}/flatpak_generator_exports');
outputDir.createSync();
final manifestGenerator = FlatpakManifestGenerator(meta);
final manifestContent = await manifestGenerator.generateFlatpakManifest(fetchFromGithub);
final manifestContent =
await manifestGenerator.generateFlatpakManifest(fetchFromGithub);
final manifestPath = '${outputDir.path}/${meta.appId}.json';
final manifestFile = File(manifestPath);
manifestFile.writeAsStringSync(manifestContent);
print('Generated $manifestPath');
final flathubJsonContent = await manifestGenerator.generateFlathubJson(fetchFromGithub);
final flathubJsonContent =
await manifestGenerator.generateFlathubJson(fetchFromGithub);
if (flathubJsonContent != null) {
final flathubJsonPath = '${outputDir.path}/flathub.json';
final flathubJsonFile = File(flathubJsonPath);
@@ -56,7 +73,7 @@ class FlatpakManifestGenerator {
Future<String> generateFlatpakManifest(bool fetchFromGithub) async {
final appName = meta.lowercaseAppName;
final appId = meta.appId;
final assets = await meta.getReleaseAssets(fetchFromGithub);
final assets = await meta.getLatestReleaseAssets(fetchFromGithub);
if (assets == null) {
throw Exception('There are no associated assets.');
@@ -88,7 +105,7 @@ class FlatpakManifestGenerator {
...meta.icons.map((icon) =>
'install -Dm644 $appName/icons/${icon.type}/${icon.getFilename(appId)} /app/share/icons/hicolor/${icon.type}/apps/${icon.getFilename(appId)}'),
'install -Dm644 $appName/$appId.desktop /app/share/applications/$appId.desktop',
'install -Dm644 $appName/$appId.appdata.xml /app/share/applications/$appId.appdata.xml'
'install -Dm644 $appName/$appId.metainfo.xml /app/share/metainfo/$appId.metainfo.xml'
],
'sources': assets
.map((a) => {
@@ -105,7 +122,7 @@ class FlatpakManifestGenerator {
}
Future<String?> generateFlathubJson(bool fetchFromGithub) async {
final assets = await meta.getReleaseAssets(fetchFromGithub);
final assets = await meta.getLatestReleaseAssets(fetchFromGithub);
if (assets == null) {
throw Exception('There are no associated assets.');
@@ -115,7 +132,8 @@ class FlatpakManifestGenerator {
const encoder = JsonEncoder.withIndent(' ');
final onlyArchListInput = fetchFromGithub ? _githubArchSupport! : _localArchSupport!;
final onlyArchListInput =
fetchFromGithub ? _githubArchSupport! : _localArchSupport!;
final onlyArchList = List<String>.empty(growable: true);
for (final e in onlyArchListInput.entries) {
@@ -131,7 +149,8 @@ class FlatpakManifestGenerator {
}
}
void _lazyGenerateArchSupportMap(bool fetchFromGithub, List<ReleaseAsset> assets) {
void _lazyGenerateArchSupportMap(
bool fetchFromGithub, List<ReleaseAsset> assets) {
if (fetchFromGithub) {
if (_githubArchSupport == null) {
_githubArchSupport = <CPUArchitecture, bool>{

View File

@@ -13,18 +13,18 @@ packages:
dependency: transitive
description:
name: collection
sha256: "4a07be6cb69c84d677a6c3096fcf960cc3285a8330b4603e0d463d15d9bd934c"
sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a
url: "https://pub.dev"
source: hosted
version: "1.17.1"
version: "1.18.0"
http:
dependency: "direct main"
description:
name: http
sha256: "6aa2946395183537c8b880962d935877325d6a09a2867c3970c05c0fed6ac482"
sha256: "5895291c13fa8a3bd82e76d5627f69e0d85ca6a30dcac95c4ea19a5d555879c2"
url: "https://pub.dev"
source: hosted
version: "0.13.5"
version: "0.13.6"
http_parser:
dependency: transitive
description:
@@ -37,10 +37,10 @@ packages:
dependency: transitive
description:
name: meta
sha256: "3c74dbf8763d36539f114c799d8a2d87343b5067e9d796ca22b5eb8437090ee3"
sha256: d584fa6707a52763a52446f02cc621b077888fb63b93bbcb1143a7be5a0c0c04
url: "https://pub.dev"
source: hosted
version: "1.9.1"
version: "1.11.0"
path:
dependency: transitive
description:
@@ -77,9 +77,9 @@ packages:
dependency: transitive
description:
name: typed_data
sha256: "26f87ade979c47a150c9eaab93ccd2bebe70a27dc0b4b29517f2904f04eb11a5"
sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c
url: "https://pub.dev"
source: hosted
version: "1.3.1"
version: "1.3.2"
sdks:
dart: ">=2.18.5 <4.0.0"
dart: ">=2.19.0 <4.0.0"

View File

@@ -31,8 +31,7 @@ class GalleryProvider extends WgerBaseProvider with ChangeNotifier {
List<gallery.Image> images = [];
GalleryProvider(super.auth, List<gallery.Image> entries, [super.client])
: images = entries;
GalleryProvider(super.auth, List<gallery.Image> entries, [super.client]) : images = entries;
/// Clears all lists
void clear() {

View File

@@ -18,41 +18,39 @@ class PreviewExerciseImages extends StatelessWidget with ExerciseImagePickerMixi
return SizedBox(
height: 300,
child: ListView(scrollDirection: Axis.horizontal, children: [
...selectedImages
.map(
(file) => SizedBox(
height: 200,
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Stack(
children: [
Image.file(file),
Positioned(
bottom: 0,
right: 0,
child: Padding(
padding: const EdgeInsets.all(3.0),
child: Container(
decoration: BoxDecoration(
color: Colors.grey.withOpacity(0.5),
borderRadius: const BorderRadius.all(Radius.circular(20)),
),
child: IconButton(
iconSize: 20,
onPressed: () =>
context.read<AddExerciseProvider>().removeExercise(file.path),
color: Colors.white,
icon: const Icon(Icons.delete),
),
),
...selectedImages.map(
(file) => SizedBox(
height: 200,
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Stack(
children: [
Image.file(file),
Positioned(
bottom: 0,
right: 0,
child: Padding(
padding: const EdgeInsets.all(3.0),
child: Container(
decoration: BoxDecoration(
color: Colors.grey.withOpacity(0.5),
borderRadius: const BorderRadius.all(Radius.circular(20)),
),
child: IconButton(
iconSize: 20,
onPressed: () =>
context.read<AddExerciseProvider>().removeExercise(file.path),
color: Colors.white,
icon: const Icon(Icons.delete),
),
),
],
),
),
),
],
),
)
,
),
),
),
const SizedBox(
width: 10,
),

View File

@@ -28,44 +28,37 @@ class Step2Variations extends StatelessWidget {
child: Column(
children: [
// Exercise bases with variations
...exerciseProvider.exerciseBasesByVariation.keys
.map(
(key) => Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Flexible(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
//mainAxisSize: MainAxisSize.max,
children: [
...exerciseProvider.exerciseBasesByVariation[key]!
.map(
(base) => Text(
base
.getExercise(
Localizations.localeOf(context).languageCode)
.name,
overflow: TextOverflow.ellipsis,
),
)
,
const SizedBox(height: 20),
],
...exerciseProvider.exerciseBasesByVariation.keys.map(
(key) => Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Flexible(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
//mainAxisSize: MainAxisSize.max,
children: [
...exerciseProvider.exerciseBasesByVariation[key]!.map(
(base) => Text(
base
.getExercise(Localizations.localeOf(context).languageCode)
.name,
overflow: TextOverflow.ellipsis,
),
),
),
Consumer<AddExerciseProvider>(
builder: (ctx, provider, __) => Switch(
value: provider.variationId == key,
onChanged: (state) => provider.variationId = key),
),
],
const SizedBox(height: 20),
],
),
),
)
,
Consumer<AddExerciseProvider>(
builder: (ctx, provider, __) => Switch(
value: provider.variationId == key,
onChanged: (state) => provider.variationId = key),
),
],
),
),
// Exercise bases without variations
...exerciseProvider.bases
.where((b) => b.variationId == null)
.map(
...exerciseProvider.bases.where((b) => b.variationId == null).map(
(base) => Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
@@ -92,8 +85,7 @@ class Step2Variations extends StatelessWidget {
),
],
),
)
,
),
],
),
),

View File

@@ -250,27 +250,25 @@ class _DashboardCalendarWidgetState extends State<DashboardCalendarWidget>
valueListenable: _selectedEvents,
builder: (context, value, _) => Column(
children: [
...value
.map((event) => ListTile(
title: Text((() {
switch (event.type) {
case EventType.caloriesDiary:
return AppLocalizations.of(context).nutritionalDiary;
...value.map((event) => ListTile(
title: Text((() {
switch (event.type) {
case EventType.caloriesDiary:
return AppLocalizations.of(context).nutritionalDiary;
case EventType.session:
return AppLocalizations.of(context).workoutSession;
case EventType.session:
return AppLocalizations.of(context).workoutSession;
case EventType.weight:
return AppLocalizations.of(context).weight;
case EventType.weight:
return AppLocalizations.of(context).weight;
case EventType.measurement:
return AppLocalizations.of(context).measurement;
}
})()),
subtitle: Text(event.description),
//onTap: () => print('$event tapped!'),
))
case EventType.measurement:
return AppLocalizations.of(context).measurement;
}
})()),
subtitle: Text(event.description),
//onTap: () => print('$event tapped!'),
))
],
),
),

View File

@@ -341,12 +341,9 @@ class MuscleWidget extends StatelessWidget {
return Stack(
children: [
SvgPicture.asset('assets/images/muscles/$background.svg'),
...muscles
.map((m) => SvgPicture.asset('assets/images/muscles/main/muscle-${m.id}.svg'))
,
...muscles.map((m) => SvgPicture.asset('assets/images/muscles/main/muscle-${m.id}.svg')),
...musclesSecondary
.map((m) => SvgPicture.asset('assets/images/muscles/secondary/muscle-${m.id}.svg'))
,
.map((m) => SvgPicture.asset('assets/images/muscles/secondary/muscle-${m.id}.svg')),
],
);
}

View File

@@ -158,7 +158,7 @@ class NutritionalDiaryChartWidgetFl extends StatefulWidget {
const NutritionalDiaryChartWidgetFl({
super.key,
required NutritionalPlan nutritionalPlan,
}) : _nutritionalPlan = nutritionalPlan;
}) : _nutritionalPlan = nutritionalPlan;
final NutritionalPlan _nutritionalPlan;
@@ -397,7 +397,7 @@ class FlNutritionalDiaryChartWidget extends StatefulWidget {
const FlNutritionalDiaryChartWidget({
super.key,
required NutritionalPlan nutritionalPlan,
}) : _nutritionalPlan = nutritionalPlan;
}) : _nutritionalPlan = nutritionalPlan;
final Color barColor = Colors.red;
final Color touchedBarColor = Colors.deepOrange;

View File

@@ -131,16 +131,14 @@ class _WorkoutDayWidgetState extends State<WorkoutDayWidget> {
child: Column(
children: [
if (set.comment != '') MutedText(set.comment),
...set.settingsFiltered
.map(
(setting) => SettingWidget(
set: set,
setting: setting,
expanded: _expanded,
toggle: _toggleExpanded,
),
)
,
...set.settingsFiltered.map(
(setting) => SettingWidget(
set: set,
setting: setting,
expanded: _expanded,
toggle: _toggleExpanded,
),
),
const Divider(),
],
),

View File

@@ -124,23 +124,21 @@ class _DayLogWidgetState extends State<DayLogWidget> {
)
else
Container(),
...widget._exerciseData[base]!
.map(
(log) => Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(log.singleLogRepTextNoNl),
IconButton(
icon: const Icon(Icons.delete),
onPressed: () async {
showDeleteDialog(
context, exercise.name, log, exercise, widget._exerciseData);
},
),
],
...widget._exerciseData[base]!.map(
(log) => Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(log.singleLogRepTextNoNl),
IconButton(
icon: const Icon(Icons.delete),
onPressed: () async {
showDeleteDialog(
context, exercise.name, log, exercise, widget._exerciseData);
},
),
)
,
],
),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 15),
child: ExerciseLogChart(base, widget._date),