From 83a2102bf90100dbb512914e8afae79612d6310c Mon Sep 17 00:00:00 2001 From: Bo Lopker Date: Sat, 13 Apr 2024 23:17:04 -0700 Subject: [PATCH] Add a way to check for updates, implement the menu bar --- README.md | 3 +- lib/main.dart | 49 ++++--- lib/menubar.dart | 131 ++++++++++++++++++ lib/strings.dart | 7 + lib/update.dart | 66 +++++++++ lib/widgets.dart | 25 ++++ macos/Flutter/GeneratedPluginRegistrant.swift | 2 + macos/Podfile.lock | 6 + pubspec.lock | 66 ++++++++- pubspec.yaml | 2 + 10 files changed, 332 insertions(+), 25 deletions(-) create mode 100644 lib/menubar.dart create mode 100644 lib/strings.dart create mode 100644 lib/update.dart create mode 100644 lib/widgets.dart diff --git a/README.md b/README.md index 6cde431..ec24f26 100644 --- a/README.md +++ b/README.md @@ -59,7 +59,7 @@ Drag and drop images into the window, images will automatically start compressin ## Privacy -All compression is done locally on your machine. Alic also does not have any analytics or tracking, including error reporting. Alic does not send any data to the internet. Because of this, Alic will not automatically update. You will need to check the [releases page][project-release-url] for updates. +All compression is done locally on your machine. Alic also does not have any analytics or tracking, including error reporting. Alic does not passively send any data to the internet. Because of this, Alic will not automatically update. You will need to check the [releases page][project-release-url] for updates, or by clicking "Check for Updates" in the app menu bar. ## Differences from ImageOptim @@ -70,6 +70,7 @@ All compression is done locally on your machine. Alic also does not have any ana ## Roadmap - [ ] Get the app signed with an Apple Developer ID +- [x] Add a way to check for updates - [x] Add support for different optimization levels - [ ] Add support for lossless compression - [x] Add support for dropping directories diff --git a/lib/main.dart b/lib/main.dart index 9cac2c7..57999af 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -10,8 +10,9 @@ import 'package:flutter/material.dart'; import 'package:signals/signals_flutter.dart'; import 'package:window_manager/window_manager.dart'; -import './config.dart'; +import 'config.dart'; import 'glass.dart'; +import 'menubar.dart'; import 'theme.dart'; void main() async { @@ -59,29 +60,31 @@ class _HomePageState extends State { @override Widget build(BuildContext context) { return Scaffold( - body: ImageDropRegion( - onDrop: (files) { - debugPrint('Dropped: $files'); - ImageFiles.add(files); - }, - dropOverlay: Container( - color: Colors.transparent, - child: const Center( - child: Icon( - Icons.file_download, - size: 400, + body: MacMenuBar( + child: ImageDropRegion( + onDrop: (files) { + debugPrint('Dropped: $files'); + ImageFiles.add(files); + }, + dropOverlay: Container( + color: Colors.transparent, + child: const Center( + child: Icon( + Icons.file_download, + size: 400, + ), + )).asGlass( + tintColor: Colors.transparent, + ), + child: const Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Expanded( + child: FilesTable(), ), - )).asGlass( - tintColor: Colors.transparent, - ), - child: const Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Expanded( - child: FilesTable(), - ), - BottomBar(), - ], + BottomBar(), + ], + ), ), ), ); diff --git a/lib/menubar.dart b/lib/menubar.dart new file mode 100644 index 0000000..a293202 --- /dev/null +++ b/lib/menubar.dart @@ -0,0 +1,131 @@ +import 'package:flutter/material.dart'; +import 'package:package_info_plus/package_info_plus.dart'; + +import 'strings.dart'; +import 'update.dart'; +import 'widgets.dart'; + +enum MenuSelection { + about, + updates, + showMessage, +} + +class MacMenuBar extends StatefulWidget { + const MacMenuBar({super.key, required this.child}); + + final Widget child; + + @override + State createState() => _MacMenuBarState(); +} + +class _MacMenuBarState extends State { + void _checkForUpdates() async { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Checking for updates...'), + ), + ); + + // Check for updates + Update? update; + String? error; + try { + update = await checkForUpdate(); + } catch (e) { + error = e.toString(); + } + if (!mounted) return; + ScaffoldMessenger.of(context).removeCurrentSnackBar(); + if (update != null) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + duration: const Duration(seconds: 20), + showCloseIcon: true, + content: Text('Update available: ${update.version}'), + action: SnackBarAction( + label: 'Download', + onPressed: () { + update!.open(); + }, + ), + ), + ); + } else if (error != null) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + showCloseIcon: true, + content: Text('Error: $error'), + ), + ); + } else { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + showCloseIcon: true, + content: Text('No updates available'), + ), + ); + } + } + + void _handleMenuSelection(MenuSelection value) async { + final packageInfo = await PackageInfo.fromPlatform(); + if (!mounted) return; + switch (value) { + case MenuSelection.about: + showAboutDialog( + context: context, + applicationName: packageInfo.appName, + applicationVersion: + "${packageInfo.version}+${packageInfo.buildNumber}", + children: const [ + TextLink( + url: Strings.repoURL, + text: 'View on GitHub', + ) + ], + ); + case MenuSelection.showMessage: + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Hello from the menu!'), + ), + ); + case MenuSelection.updates: + _checkForUpdates(); + } + } + + @override + Widget build(BuildContext context) { + return PlatformMenuBar( + menus: [ + PlatformMenu( + label: 'Alic', + menus: [ + PlatformMenuItemGroup( + members: [ + PlatformMenuItem( + label: 'About', + onSelected: () { + _handleMenuSelection(MenuSelection.about); + }, + ), + PlatformMenuItem( + label: 'Check for Updates...', + onSelected: () { + _handleMenuSelection(MenuSelection.updates); + }, + ), + ], + ), + const PlatformProvidedMenuItem( + type: PlatformProvidedMenuItemType.quit), + ], + ), + ], + child: widget.child, + ); + } +} diff --git a/lib/strings.dart b/lib/strings.dart new file mode 100644 index 0000000..d9f13d8 --- /dev/null +++ b/lib/strings.dart @@ -0,0 +1,7 @@ +class Strings { + static const downloadURL = + 'https://github.com/blopker/alic/releases/latest/download/Alic.Image.Compressor.dmg'; + static const githubAPI = + 'https://api.github.com/repos/blopker/alic/tags?per_page=10'; + static const repoURL = 'https://github.com/blopker/alic/'; +} diff --git a/lib/update.dart b/lib/update.dart new file mode 100644 index 0000000..c97302d --- /dev/null +++ b/lib/update.dart @@ -0,0 +1,66 @@ +import 'dart:convert'; + +import 'package:flutter/material.dart'; +import 'package:http/http.dart' as http; +import 'package:package_info_plus/package_info_plus.dart'; +import 'package:url_launcher/url_launcher.dart'; + +import 'strings.dart'; + +@immutable +class Update { + final String version; + final int buildNumber; + final String url; + + const Update( + {required this.version, required this.url, required this.buildNumber}); + + void open() { + launchUrl(Uri.parse(url)); + } +} + +Future getLatestBuildNumber() async { + final uri = Uri.parse(Strings.githubAPI); + final response = + await http.get(uri, headers: {"Accept": "application/vnd.github+json"}); + + if (response.statusCode == 200) { + final List decodedData = jsonDecode(response.body); + final tags = decodedData.map((e) => e['name'].toString()).toList(); + final latestBuild = tags.firstWhere((element) => element.contains('+')); + final buildNumber = int.parse(latestBuild.split('+').last); + return Update( + version: latestBuild, + buildNumber: buildNumber, + url: Strings.downloadURL, + ); + } else { + throw Exception('Failed to fetch tags: ${response.statusCode}'); + } +} + +Future checkForUpdate({bool force = false}) async { + // 1. Get your current build number + PackageInfo packageInfo = await PackageInfo.fromPlatform(); + int currentBuildNumber = int.parse(packageInfo.buildNumber); + + // 2. Get the latest build number + final update = await getLatestBuildNumber(); + int latestBuildNumber = update.buildNumber; + + // 3. Compare + if (latestBuildNumber > currentBuildNumber || force) { + debugPrint('Update available: ${update.version}'); + return update; + } else { + debugPrint('You have the latest version'); + return null; + } +} + +void main() async { + final buildNumber = await getLatestBuildNumber(); + print('Latest Build Number: $buildNumber'); +} diff --git a/lib/widgets.dart b/lib/widgets.dart new file mode 100644 index 0000000..8c45a5d --- /dev/null +++ b/lib/widgets.dart @@ -0,0 +1,25 @@ +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:url_launcher/url_launcher.dart'; + +class TextLink extends StatelessWidget { + const TextLink({super.key, required this.url, required this.text}); + + final String url; + final String text; + + @override + Widget build(BuildContext context) { + return RichText( + text: TextSpan( + text: text, + style: const TextStyle( + color: Colors.blue, + ), + recognizer: TapGestureRecognizer() + ..onTap = () { + launchUrl(Uri.parse(url)); + }, + )); + } +} diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 08a8758..c12b5d8 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -12,6 +12,7 @@ import open_file_macos import package_info_plus import screen_retriever import super_native_extensions +import url_launcher_macos import window_manager func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { @@ -22,5 +23,6 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin")) ScreenRetrieverPlugin.register(with: registry.registrar(forPlugin: "ScreenRetrieverPlugin")) SuperNativeExtensionsPlugin.register(with: registry.registrar(forPlugin: "SuperNativeExtensionsPlugin")) + UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) WindowManagerPlugin.register(with: registry.registrar(forPlugin: "WindowManagerPlugin")) } diff --git a/macos/Podfile.lock b/macos/Podfile.lock index 8b182e3..e976098 100644 --- a/macos/Podfile.lock +++ b/macos/Podfile.lock @@ -16,6 +16,8 @@ PODS: - FlutterMacOS - super_native_extensions (0.0.1): - FlutterMacOS + - url_launcher_macos (0.0.1): + - FlutterMacOS - window_manager (0.2.0): - FlutterMacOS @@ -29,6 +31,7 @@ DEPENDENCIES: - rust_builder (from `Flutter/ephemeral/.symlinks/plugins/rust_builder/macos`) - screen_retriever (from `Flutter/ephemeral/.symlinks/plugins/screen_retriever/macos`) - super_native_extensions (from `Flutter/ephemeral/.symlinks/plugins/super_native_extensions/macos`) + - url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`) - window_manager (from `Flutter/ephemeral/.symlinks/plugins/window_manager/macos`) EXTERNAL SOURCES: @@ -50,6 +53,8 @@ EXTERNAL SOURCES: :path: Flutter/ephemeral/.symlinks/plugins/screen_retriever/macos super_native_extensions: :path: Flutter/ephemeral/.symlinks/plugins/super_native_extensions/macos + url_launcher_macos: + :path: Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos window_manager: :path: Flutter/ephemeral/.symlinks/plugins/window_manager/macos @@ -63,6 +68,7 @@ SPEC CHECKSUMS: rust_builder: d6115f3c96b081d2c66f05771d1c2509559e2073 screen_retriever: 59634572a57080243dd1bf715e55b6c54f241a38 super_native_extensions: 85efee3a7495b46b04befcfc86ed12069264ebf3 + url_launcher_macos: d2691c7dd33ed713bf3544850a623080ec693d95 window_manager: 3a1844359a6295ab1e47659b1a777e36773cd6e8 PODFILE CHECKSUM: f0c21717cb7ee9112f915044c74bfceb5b12e02a diff --git a/pubspec.lock b/pubspec.lock index e0cf41f..bdd5d37 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -459,7 +459,7 @@ packages: source: hosted version: "4.2.0" http: - dependency: transitive + dependency: "direct main" description: name: http sha256: "761a297c042deedc1ffbb156d6e2af13886bb305c2a343a4d972504cd67dd938" @@ -947,6 +947,70 @@ packages: url: "https://pub.dev" source: hosted version: "2.2.2" + url_launcher: + dependency: "direct main" + description: + name: url_launcher + sha256: "6ce1e04375be4eed30548f10a315826fd933c1e493206eab82eed01f438c8d2e" + url: "https://pub.dev" + source: hosted + version: "6.2.6" + url_launcher_android: + dependency: transitive + description: + name: url_launcher_android + sha256: "360a6ed2027f18b73c8d98e159dda67a61b7f2e0f6ec26e86c3ada33b0621775" + url: "https://pub.dev" + source: hosted + version: "6.3.1" + url_launcher_ios: + dependency: transitive + description: + name: url_launcher_ios + sha256: "9149d493b075ed740901f3ee844a38a00b33116c7c5c10d7fb27df8987fb51d5" + url: "https://pub.dev" + source: hosted + version: "6.2.5" + url_launcher_linux: + dependency: transitive + description: + name: url_launcher_linux + sha256: ab360eb661f8879369acac07b6bb3ff09d9471155357da8443fd5d3cf7363811 + url: "https://pub.dev" + source: hosted + version: "3.1.1" + url_launcher_macos: + dependency: transitive + description: + name: url_launcher_macos + sha256: b7244901ea3cf489c5335bdacda07264a6e960b1c1b1a9f91e4bc371d9e68234 + url: "https://pub.dev" + source: hosted + version: "3.1.0" + url_launcher_platform_interface: + dependency: transitive + description: + name: url_launcher_platform_interface + sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + url_launcher_web: + dependency: transitive + description: + name: url_launcher_web + sha256: "3692a459204a33e04bc94f5fb91158faf4f2c8903281ddd82915adecdb1a901d" + url: "https://pub.dev" + source: hosted + version: "2.3.0" + url_launcher_windows: + dependency: transitive + description: + name: url_launcher_windows + sha256: ecf9725510600aa2bb6d7ddabe16357691b6d2805f66216a97d1b881e21beff7 + url: "https://pub.dev" + source: hosted + version: "3.1.1" uuid: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 89449a3..0f70eb5 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -25,6 +25,8 @@ dependencies: freezed_annotation: ^2.4.1 json_annotation: ^4.8.1 package_info_plus: ^7.0.0 + http: ^1.2.1 + url_launcher: ^6.2.6 dev_dependencies: flutter_test: