diff --git a/flatpak/README b/flatpak/README new file mode 100644 index 00000000..42b64942 --- /dev/null +++ b/flatpak/README @@ -0,0 +1,8 @@ +To generate a release: + +1. Build the Linux release using Flutter. +2. Add the new release version and date to the start of the "releases" list in the "spec.json" file (and adjust other parameters in the file if needed). +3. Run "dart flatpak_generator.dart spec.json" in this folder. +4. Upload the generated tar.gz file as a Github release, using the app's version name for the tag (e.g. "1.0.0"). +5. Test the Flatpak using the guide at https://docs.flatpak.org/en/latest/first-build.html, using the generated json manifest as your Flatpak manifest. +6. Update your Flathub manifest file in your Flathub Github repo. (If the "flathub.json" file is not there yet, upload that too.) diff --git a/flatpak/de.wger.flutter.appdata.xml b/flatpak/de.wger.flutter.appdata.xml new file mode 100755 index 00000000..80dec61f --- /dev/null +++ b/flatpak/de.wger.flutter.appdata.xml @@ -0,0 +1,72 @@ + + + de.wger.flutter + wger + Fitness/workout, nutrition and weight tracker + + CC0-1.0 + AGPL-3.0-or-later + + touch + pointing + keyboard + + wger + https://wger.de/ + https://github.com/wger-project/flutter/issues + + + workstation + mobile + + + +

From fitness lovers to fitness lovers – get your health organized with WGER, your Workout Manager!

+

Have you already found your #1 fitness app and do you love to create your own sports routines? No matter what type of sporty beast you are – we all have something in common: We love to keep track of our health data <3

+

So we don’t judge you for still managing your fitness journey with your handy little workout log book but welcome to 2021!

+

We have developed a 100% free digital health and fitness tracker app for you, sized down to the most relevant features to make your life easier. Get started, keep training and celebrate your progress!

+

wger is an Open Source project and all about:

+ +

Your Body: + No need to google for the ingredients of your favourite treats – choose your daily meals from more than 78000 products and see the nutritional values. Add meals to the nutritional plan and keep an overview of your diet in the calendar.

+

Your Workouts: + You know what is best for your body. Create your own workouts out of a growing variety from 200 different exercises. Then, use the Gym Mode to guide you through the training while you log your weights with one tap.

+

Your Progress: + Never lose sight of your goals. Track your weight and keep your statistics.

+

Your Data: + wger is your personalized fitness diary – but you own your data. Use the REST API to access and do amazing things with it.

+

Please note: This free app is not based on additional fundings and we don’t ask you to donate money. More than that it is a community project which is growing constantly. So be prepared for new features anytime!

+

OpenSource – what does that mean?

+

Open Source means that the whole source code for this app and the server it talks to is free and available to anybody:

+ +

Join our community and become a part of sport enthusiasts and IT geeks from all over the world. We keep working on adjusting and optimizing the app customized to our needs. We love your input so feel free to jump in anytime and contribute your wishes and ideas!

+ +
+ + + + wger's dashboard + https://github.com/wger-project/flutter/raw/master/fastlane/metadata/android/en-US/images/phoneScreenshots/01%20-%20dashboard.png + + + + + + + + + + de.wger.flutter.desktop +
\ No newline at end of file diff --git a/flatpak/de.wger.flutter.desktop b/flatpak/de.wger.flutter.desktop new file mode 100755 index 00000000..5e4cf795 --- /dev/null +++ b/flatpak/de.wger.flutter.desktop @@ -0,0 +1,11 @@ +[Desktop Entry] +Version=1.0 +Type=Application +Name=wger +Comment=Fitness/workout, nutrition and weight tracker +Categories=Education;Utility;Sports; +Icon=de.wger.flutter +Exec=wger +StartupWMClass=wger +X-Purism-FormFactor=Workstation;Mobile; +X-KDE-FormFactors=desktop;tablet;handset;mediacenter; diff --git a/flatpak/flatpak_generator.dart b/flatpak/flatpak_generator.dart new file mode 100644 index 00000000..9d4a6225 --- /dev/null +++ b/flatpak/flatpak_generator.dart @@ -0,0 +1,305 @@ +// ignore_for_file: avoid_classes_with_only_static_members, avoid_print + +import 'dart:convert'; +import 'dart:io'; + +void main(List arguments) { + if (arguments.length != 1) { + throw Exception('Must have only one argument: the path to the JSON specification.'); + } + if (!Platform.isLinux) { + throw Exception('Must be run under x86_64 Linux'); + } + final jsonFile = File(arguments[0]); + if (!jsonFile.existsSync()) { + throw Exception('The provided JSON file does not exist.'); + } + final specJson = SpecJson.fromJson(jsonFile); + + final outputDir = Directory.current; + + final packageGenerator = PackageGenerator(inputDir: jsonFile.parent, specJson: specJson); + packageGenerator.generatePackage(Directory.current, _Platform.x86_64); + + if (specJson.linuxArmReleaseBundleDirPath != null) { + packageGenerator.generatePackage(Directory.current, _Platform.aarch64); + } + + final sha256x64 = packageGenerator.sha256x64; + + if (sha256x64 == null) { + throw Exception('Could not generate SHA256 for the created package'); + } + + final sha256aarch64 = packageGenerator.sha256aarch64; + + final manifestContent = + FlatpakManifestGenerator(specJson).getFlatpakManifest(sha256x64, sha256aarch64); + final manifestPath = '${outputDir.path}/${specJson.appId}.json'; + final manifestFile = File(manifestPath); + manifestFile.writeAsStringSync(manifestContent); + print('Generated $manifestPath'); + + if (specJson.linuxArmReleaseBundleDirPath == null) { + final flathubJsonPath = '${outputDir.path}/${FlathubJsonGenerator.filename}'; + final flathubJsonFile = File(flathubJsonPath); + flathubJsonFile.writeAsStringSync(FlathubJsonGenerator.generate()); + print('Generated $flathubJsonPath'); + } +} + +enum _Platform { x86_64, aarch64 } + +class PackageGenerator { + final Directory inputDir; + final SpecJson specJson; + String? sha256x64; + String? sha256aarch64; + + PackageGenerator({required this.inputDir, required this.specJson}); + + void generatePackage(Directory outputDir, _Platform platform) { + final tempDir = outputDir.createTempSync('flutter_package_generator'); + final appId = specJson.appId; + + // desktop file + final desktopFile = File('${inputDir.path}/${specJson.desktopPath}'); + + if (!desktopFile.existsSync()) { + throw Exception( + 'The desktop file does not exist under the specified path: ${desktopFile.path}'); + } + + desktopFile.copySync( + '${tempDir.path}/$appId.desktop'); //todo does "$appName" have to be in the path too? + + // icons + final iconTempDir = Directory('${tempDir.path}/icons'); + + for (final icon in specJson.icons) { + final iconFile = File('${inputDir.path}/${icon.path}'); + if (!iconFile.existsSync()) { + 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}/$appId.${icon.fileExtension}'); + } + + // appdata file + final origAppDataFile = File('${inputDir.path}/${specJson.appDataPath}'); + if (!origAppDataFile.existsSync()) { + throw Exception( + 'The app data file does not exist under the specified path: ${origAppDataFile.path}'); + } + + final editedAppDataContent = + AppDataModifier.replaceVersions(origAppDataFile.readAsStringSync(), specJson.releases); + + final editedAppDataFile = File('${tempDir.path}/$appId.appdata.xml'); + editedAppDataFile.writeAsStringSync(editedAppDataContent); + + // build files + final bundlePath = platform == _Platform.aarch64 + ? specJson.linuxArmReleaseBundleDirPath + : specJson.linuxReleaseBundleDirPath; + final buildDir = Directory(bundlePath!); + if (!buildDir.existsSync()) { + throw Exception( + 'The linux build directory does not exist under the specified path: ${buildDir.path}'); + } + final destDir = Directory('${tempDir.path}/bin'); + destDir.createSync(); + + final platformSuffix = platform == _Platform.aarch64 ? 'aarch64' : 'x86_64'; + final packagePath = + '${outputDir.absolute.path}/${specJson.lowercaseAppName}-linux-$platformSuffix.tar.gz'; + + Process.runSync('cp', [ + '-r', + '${buildDir.absolute.path}/.', + destDir.absolute.path + ]); //todo test with spaces in name + Process.runSync('tar', ['-czvf', packagePath, '.'], + workingDirectory: tempDir.absolute.path); //todo test with spaces in name + + print('Generated $packagePath'); + + final preShasum = Process.runSync('shasum', ['-a', '256', packagePath]); + + if (platform == _Platform.aarch64) { + sha256aarch64 = preShasum.stdout.toString().split(' ').first; + } else { + sha256x64 = preShasum.stdout.toString().split(' ').first; + } + + tempDir.deleteSync(recursive: true); + } +} + +// updates releases in ${appName}.appdata.xml +class AppDataModifier { + static String replaceVersions(String origAppDataContent, List versions) { + final joinedReleases = + versions.map((v) => '').join('\n'); + final releasesSection = '\n$joinedReleases\n'; + if (origAppDataContent.contains('', multiLine: true), releasesSection); + } else { + return origAppDataContent.replaceFirst('', '$releasesSection\n'); + } + } +} + +// ${appId}.json +class FlatpakManifestGenerator { + final SpecJson specJson; + + FlatpakManifestGenerator(this.specJson); + + String getFlatpakManifest(String sha256x64, String? sha256aarch64) { + final appName = specJson.lowercaseAppName; + final appId = specJson.appId; + const encoder = JsonEncoder.withIndent(' '); + return encoder.convert({ + 'app-id': appId, + 'runtime': 'org.freedesktop.Platform', + 'runtime-version': specJson.runtimeVersion, + 'sdk': 'org.freedesktop.Sdk', + 'command': appName, + 'separate-locales': false, + 'finish-args': specJson.finishArgs, + 'modules': [ + ...specJson.extraModules ?? [], + { + 'name': appName, + 'buildsystem': 'simple', + 'build-commands': [ + 'cp -R $appName/bin/ /app/$appName', + 'chmod +x /app/$appName/$appName', + 'mkdir /app/bin/', + 'mkdir /app/lib/', + 'ln -s /app/$appName/$appName /app/bin/$appName', + ...specJson.flatpakCommandsAfterUnpack ?? [], + ...specJson.icons.map((icon) => + 'install -Dm644 $appName/icons/${icon.type}/$appId.${icon.fileExtension} /app/share/icons/hicolor/${icon.type}/apps/$appId.${icon.fileExtension}'), //TODO THIS DOES NOT ACCOUNT FOR THE symbolic icon name!!! + 'install -Dm644 $appName/$appId.desktop /app/share/applications/$appId.desktop', + 'install -Dm644 $appName/$appId.appdata.xml /app/share/applications/$appId.appdata.xml' + ], + 'sources': [ + { + 'type': 'archive', + 'only-arches': ['x86_64'], + 'url': + 'https://github.com/${specJson.githubReleaseOrganization}/${specJson.githubReleaseProject}/releases/download/${specJson.releases.first.version}/${specJson.lowercaseAppName}-linux-x86_64.tar.gz', + 'sha256': sha256x64, + 'dest': specJson.lowercaseAppName + }, + if (specJson.linuxArmReleaseBundleDirPath != null) + { + 'type': 'archive', + 'only-arches': ['aarch64'], + 'url': + 'https://github.com/${specJson.githubReleaseOrganization}/${specJson.githubReleaseProject}/releases/download/${specJson.releases.first.version}/${specJson.lowercaseAppName}-linux-aarch64.tar.gz', + 'sha256': sha256aarch64, + 'dest': specJson.lowercaseAppName + } + ] + } + ] + }); + } +} + +// flathub.json +class FlathubJsonGenerator { + static const String filename = 'flathub.json'; + + static String generate() { + const encoder = JsonEncoder.withIndent(' '); + return encoder.convert({ + 'only-arches': ['x86_64'] + }); + } +} + +class Release { + final String version; + final String date; + + Release({required this.version, required this.date}); +} + +class Icon { + final String type; + final String path; + late final String fileExtension; + + Icon({required this.type, required this.path}) { + fileExtension = path.split('.').last; + } +} + +class SpecJson { + //todo allow extra modules + final String appId; + final String lowercaseAppName; + final List releases; + final String runtimeVersion; + final String linuxReleaseBundleDirPath; + final String? linuxArmReleaseBundleDirPath; + final String appDataPath; + final String desktopPath; + final List icons; + final List? flatpakCommandsAfterUnpack; + final List? extraModules; + final List finishArgs; + final String githubReleaseOrganization; + final String githubReleaseProject; + + SpecJson( + {required this.appId, + required this.lowercaseAppName, + required this.releases, + required this.runtimeVersion, + required this.linuxReleaseBundleDirPath, + this.linuxArmReleaseBundleDirPath, + required this.appDataPath, + required this.desktopPath, + required this.icons, + required this.flatpakCommandsAfterUnpack, + this.extraModules, + required this.finishArgs, + required this.githubReleaseOrganization, + required this.githubReleaseProject}); + + static SpecJson fromJson(File jsonFile) { + try { + final json = jsonDecode(jsonFile.readAsStringSync()); + return SpecJson( + appId: json['appId'], + lowercaseAppName: json['lowercaseAppName'], + releases: (json['releases'] as List).map((r) { + final rMap = r as Map; + return Release(version: rMap['version'], date: rMap['date']); + }).toList(), + runtimeVersion: json['runtimeVersion'], + linuxReleaseBundleDirPath: json['linuxReleaseBundleDirPath'], + appDataPath: json['appDataPath'], + desktopPath: json['desktopPath'], + icons: (json['icons'] as Map).entries.map((mapEntry) { + return Icon(type: mapEntry.key as String, path: mapEntry.value as String); + }).toList(), + flatpakCommandsAfterUnpack: + (json['buildCommandsAfterUnpack'] as List?)?.map((bc) => bc as String)?.toList(), + linuxArmReleaseBundleDirPath: json['linuxArmReleaseBundleDirPath'] as String?, + extraModules: json['extraModules'] as List?, + finishArgs: (json['finishArgs'] as List).map((fa) => fa as String).toList(), + githubReleaseOrganization: json['githubReleaseOrganization'], + githubReleaseProject: json['githubReleaseProject']); + } catch (e) { + throw Exception('Could not parse JSON file, due to this error:\n$e'); + } + } +} diff --git a/flatpak/logo128.png b/flatpak/logo128.png new file mode 100644 index 00000000..98f5c2d5 Binary files /dev/null and b/flatpak/logo128.png differ diff --git a/flatpak/logo512.png b/flatpak/logo512.png new file mode 100644 index 00000000..7f0d0224 Binary files /dev/null and b/flatpak/logo512.png differ diff --git a/flatpak/logo64.png b/flatpak/logo64.png new file mode 100644 index 00000000..c969d6a9 Binary files /dev/null and b/flatpak/logo64.png differ diff --git a/flatpak/spec.json b/flatpak/spec.json new file mode 100644 index 00000000..d897e6aa --- /dev/null +++ b/flatpak/spec.json @@ -0,0 +1,28 @@ +{ + "appId": "de.wger.flutter", + "releases": [ + { + "version": "1.5.3", + "date": "2023-03-16" + } + ], + "lowercaseAppName": "wger", + "runtimeVersion": "22.08", + "linuxReleaseBundleDirPath": "../build/linux/x64/release/bundle", + "appDataPath": "de.wger.flutter.appdata.xml", + "desktopPath": "de.wger.flutter.desktop", + "icons": { + "64x64": "logo64.png", + "128x126": "logo128.png", + "512x512": "logo512.png" + }, + "finishArgs": [ + "--share=ipc", + "--share=network", + "--socket=fallback-x11", + "--socket=wayland", + "--device=dri" + ], + "githubReleaseOrganization": "wger-project", + "githubReleaseProject": "flutter" +} \ No newline at end of file