From 9b35d1a733527f1478fb4ca8fce0f86d05ffc5a8 Mon Sep 17 00:00:00 2001 From: aleeeee1 Date: Sun, 12 Nov 2023 22:23:06 +0100 Subject: [PATCH] added ota in-app --- android/app/src/main/AndroidManifest.xml | 12 ++ android/app/src/main/res/xml/filepaths.xml | 4 + assets/i18n/en_US.json | 6 + lib/services/internal_api.dart | 33 +++++ lib/ui/components/bouncing_text.dart | 133 +++++++++++++++++ lib/ui/components/floating_text.dart | 161 +++++++++++++++++++++ lib/ui/components/update.dart | 124 ++++++++++++++++ lib/ui/pages/home.dart | 24 +++ pubspec.lock | 50 ++++++- pubspec.yaml | 7 +- 10 files changed, 552 insertions(+), 2 deletions(-) create mode 100644 android/app/src/main/res/xml/filepaths.xml create mode 100644 lib/ui/components/bouncing_text.dart create mode 100644 lib/ui/components/floating_text.dart create mode 100644 lib/ui/components/update.dart diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index b099622..35cf8d2 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -2,6 +2,8 @@ + + + + + + \ No newline at end of file diff --git a/android/app/src/main/res/xml/filepaths.xml b/android/app/src/main/res/xml/filepaths.xml new file mode 100644 index 0000000..5c724e9 --- /dev/null +++ b/android/app/src/main/res/xml/filepaths.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/assets/i18n/en_US.json b/assets/i18n/en_US.json index 5e92031..c32f1f7 100644 --- a/assets/i18n/en_US.json +++ b/assets/i18n/en_US.json @@ -118,5 +118,11 @@ "roomFormPwdSwitch": "Password", "roomFormPwdLabel": "Password", "roomFormCreateBtn": "Create" + }, + + "updateDialog": { + "title": "New update available!", + "downloadingUpdate": "Downloading...", + "updateBtn": "Update" } } diff --git a/lib/services/internal_api.dart b/lib/services/internal_api.dart index c8e7f87..8349963 100644 --- a/lib/services/internal_api.dart +++ b/lib/services/internal_api.dart @@ -1,5 +1,6 @@ import 'package:flutter/foundation.dart'; import 'package:get/get.dart'; +import 'package:package_info_plus/package_info_plus.dart'; import 'package:universal_io/io.dart'; import 'package:animated_theme_switcher/animated_theme_switcher.dart'; import 'package:device_info_plus/device_info_plus.dart'; @@ -8,9 +9,12 @@ import 'package:shared_preferences/shared_preferences.dart'; import 'package:sushi_room/utils/globals.dart' as globals; import 'package:permission_handler/permission_handler.dart'; import 'package:app_settings/app_settings.dart'; +import 'package:http/http.dart' as http; +import 'package:html/parser.dart'; class InternalAPI { late SharedPreferences prefs; + static String repoLink = "https://github.com/SushiRoom/app"; Future init() async { prefs = await SharedPreferences.getInstance(); @@ -98,4 +102,33 @@ class InternalAPI { backgroundColor: Theme.of(context).colorScheme.error, ); } + + Future getVersion() async { + final PackageInfo info = await PackageInfo.fromPlatform(); + return info.version; + } + + Future getLatestVersion() async { + var url = Uri.parse( + "$repoLink/releases/latest", + ); + + try { + var response = await http.get(url); + var document = parse(response.body); + var release = document.getElementsByTagName('h1').firstWhere((element) => element.text.startsWith("Release")); + var version = release.text.replaceAll("Release ", ""); + + return version; + } catch (e) { + return ""; + } + } + + Future getLatestVersionUrl() async { + String version = await getLatestVersion(); + String url = "$repoLink/releases/download/$version/app-release.apk"; + + return url; + } } diff --git a/lib/ui/components/bouncing_text.dart b/lib/ui/components/bouncing_text.dart new file mode 100644 index 0000000..bc276aa --- /dev/null +++ b/lib/ui/components/bouncing_text.dart @@ -0,0 +1,133 @@ +import 'dart:async'; +import 'dart:math'; + +import 'package:flutter/material.dart'; + +class BouncingDVD extends StatefulWidget { + final String text; + + final double height; + final double width; + + const BouncingDVD({ + Key? key, + required this.text, + required this.height, + required this.width, + }) : super(key: key); + + @override + State createState() => _BouncingDVDState(); +} + +class _BouncingDVDState extends State { + Random random = Random(); + Color? dvdColor; + late double dvdWidth, dvdHeight; + double x = 90, y = 30, xSpeed = 50, ySpeed = 50, speed = 150; + + pickColor() { + Timer(const Duration(milliseconds: 100), () { + if (!mounted) return; + ColorScheme scheme = Theme.of(context).colorScheme; + List colors = [ + scheme.primary, + scheme.onSurface, + ]; + + var tmp = colors[random.nextInt(colors.length)]; + while (tmp == dvdColor) { + tmp = colors[random.nextInt(colors.length)]; + } + + dvdColor = tmp; + }); + } + + @override + initState() { + super.initState(); + + Size textSize = _textSize(widget.text, const TextStyle(fontSize: 20)); + dvdWidth = textSize.width; + dvdHeight = textSize.height; + + pickColor(); + update(); + } + + update() { + Timer.periodic(Duration(milliseconds: speed.toInt()), (timer) { + if (!mounted) { + timer.cancel(); + return; + } + + double screenWidth = widget.width; + double screenHeight = widget.height; + x += xSpeed; + y += ySpeed; + + if (x + dvdWidth >= screenWidth) { + xSpeed = -xSpeed; + x = screenWidth - dvdWidth; + pickColor(); + } else if (x <= 0) { + xSpeed = -xSpeed; + x = 0; + pickColor(); + } + + if (y + dvdHeight >= screenHeight) { + ySpeed = -ySpeed; + y = screenHeight - dvdHeight; + pickColor(); + } else if (y <= 0) { + ySpeed = -ySpeed; + y = 0; + pickColor(); + } + + setState(() {}); + }); + } + + @override + Widget build(BuildContext context) { + return Stack( + children: [ + AnimatedPositioned( + duration: Duration(milliseconds: speed.toInt()), + left: x, + top: y, + child: Text( + widget.text, + style: TextStyle( + fontSize: 50, + fontWeight: FontWeight.bold, + color: dvdColor, + ), + ), + ), + ], + ); + } + + Size _textSize(String text, TextStyle style) { + final TextPainter textPainter = TextPainter( + text: TextSpan( + text: text, + style: const TextStyle( + fontSize: 50, + fontWeight: FontWeight.bold, + ), + ), + maxLines: 1, + textDirection: TextDirection.ltr) + ..layout( + minWidth: 0, + maxWidth: double.infinity, + ); + return textPainter.size; + } +} diff --git a/lib/ui/components/floating_text.dart b/lib/ui/components/floating_text.dart new file mode 100644 index 0000000..2787220 --- /dev/null +++ b/lib/ui/components/floating_text.dart @@ -0,0 +1,161 @@ +import 'dart:math'; +import 'package:flutter/material.dart'; +import 'package:animated_text_kit/animated_text_kit.dart'; + +class FloatingText extends AnimatedText { + late AnimationController _controller; + + FloatingText({ + required String text, + TextAlign textAlign = TextAlign.start, + TextStyle? textStyle, + Duration duration = const Duration(milliseconds: 2500), + }) : super( + text: text, + textAlign: textAlign, + textStyle: textStyle, + duration: duration, + ); + + @override + void initAnimation(AnimationController controller) { + _controller = controller; + + _controller.addListener(() { + if (_controller.isCompleted) { + _controller.repeat(); + } + }); + } + + @override + Widget animatedBuilder(BuildContext context, Widget? child) { + final double rotationAngle = sin(_controller.value * 2 * pi) * 0.005; + const double amplitude = 30; + + return Transform.translate( + offset: Offset( + sin(_controller.value * 2 * pi) * amplitude, + cos(_controller.value * 2 * pi) * amplitude, + ), + filterQuality: FilterQuality.high, + child: Transform.rotate( + angle: rotationAngle, + filterQuality: FilterQuality.high, + child: child, + ), + ); + } +} + +class StarParticle { + late double x; + late double y; + late double size; + late double speed; + late double rotation; + late Color color; + + StarParticle({ + required this.x, + required this.y, + required this.size, + required this.speed, + required this.rotation, + required this.color, + }); +} + +class StarField extends StatefulWidget { + final Widget child; + + const StarField({ + super.key, + required this.child, + }); + + @override + StarFieldState createState() => StarFieldState(); +} + +class StarFieldState extends State with TickerProviderStateMixin { + late List particles; + late AnimationController controller; + + @override + void initState() { + super.initState(); + particles = _generateParticles(); + controller = AnimationController( + duration: const Duration(seconds: 4), + vsync: this, + )..repeat(); + } + + List _generateParticles() { + Random random = Random(); + List particles = []; + + for (int i = 0; i < 100; i++) { + particles.add(StarParticle( + x: random.nextDouble() * 400, // Adjust the range based on your needs + y: random.nextDouble() * 400, + size: random.nextDouble() * 2 + 1, + speed: random.nextDouble() * 0.5 + 0.2, + rotation: random.nextDouble() * 2 * pi, + color: Colors.yellow, + )); + } + + return particles; + } + + @override + Widget build(BuildContext context) { + return AnimatedBuilder( + animation: controller, + builder: (context, child) { + return CustomPaint( + painter: StarFieldPainter(particles, controller.value), + child: widget.child, + ); + }, + ); + } + + @override + void dispose() { + controller.dispose(); + super.dispose(); + } +} + +class StarFieldPainter extends CustomPainter { + final List particles; + final double animationValue; + + StarFieldPainter(this.particles, this.animationValue); + + @override + void paint(Canvas canvas, Size size) { + for (var particle in particles) { + particle.x += particle.speed * cos(particle.rotation); + particle.y += particle.speed * sin(particle.rotation); + + if (particle.x < 0 || particle.x > size.width || particle.y < 0 || particle.y > size.height) { + // If the particle goes out of bounds, reset its position + particle.x = size.width / 2; + particle.y = size.height / 2; + } + + double alpha = (1 - (particle.y / size.height)) * animationValue; + Paint paint = Paint()..color = particle.color.withOpacity(alpha); + canvas.drawCircle(Offset(particle.x, particle.y), particle.size, paint); + } + } + + @override + bool shouldRepaint(covariant CustomPainter oldDelegate) { + return true; + } +} diff --git a/lib/ui/components/update.dart b/lib/ui/components/update.dart new file mode 100644 index 0000000..bf83e6e --- /dev/null +++ b/lib/ui/components/update.dart @@ -0,0 +1,124 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_i18n/flutter_i18n.dart'; +import 'package:get/get.dart'; +import 'package:ota_update/ota_update.dart'; +import 'package:sushi_room/services/internal_api.dart'; +import 'package:sushi_room/ui/components/bouncing_text.dart'; + +class OtaSheet extends StatefulWidget { + final String version; + const OtaSheet({ + super.key, + required this.version, + }); + + @override + State createState() => _OtaSheetState(); +} + +class _OtaSheetState extends State { + bool updating = false; + var progress = 0; + + InternalAPI internalAPI = Get.find(); + + startUpdate() async { + String url = await internalAPI.getLatestVersionUrl(); + try { + OtaUpdate().execute(url).listen( + (OtaEvent event) { + setState(() { + progress = int.tryParse(event.value!) ?? progress; + }); + }, + cancelOnError: true, + onError: (e) { + setState(() { + updating = false; + }); + }); + } catch (e) { + debugPrint(e.toString()); + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: I18nText("updateDialog.title"), + ), + body: SafeArea( + child: Column( + children: [ + Expanded( + child: Center( + child: updating + ? Column( + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.max, + children: [ + Stack( + alignment: Alignment.center, + children: [ + Center( + child: SizedBox( + height: 200, + width: 200, + child: CircularProgressIndicator( + value: progress / 100, + strokeWidth: 8, + strokeCap: StrokeCap.round, + ), + ), + ), + Center( + child: Text( + "$progress%", + style: const TextStyle( + fontSize: 50, + fontWeight: FontWeight.bold, + ), + ), + ), + ], + ), + const SizedBox(height: 20), + I18nText("updateDialog.downloadingUpdate") + ], + ) + : LayoutBuilder( + builder: (context, constraints) { + return BouncingDVD( + text: widget.version, + height: constraints.maxHeight, + width: constraints.maxWidth, + ); + }, + ), + ), + ), + Container( + padding: const EdgeInsets.symmetric( + horizontal: 20.0, + vertical: 10, + ), + width: double.infinity, + child: FilledButton.tonal( + onPressed: updating + ? null + : () { + setState(() { + updating = true; + }); + startUpdate(); + }, + child: I18nText('updateDialog.updateBtn'), + ), + ) + ], + ), + ), + ); + } +} diff --git a/lib/ui/pages/home.dart b/lib/ui/pages/home.dart index 3dd35ff..332ea4b 100644 --- a/lib/ui/pages/home.dart +++ b/lib/ui/pages/home.dart @@ -14,6 +14,8 @@ import 'package:sushi_room/models/room.dart'; import 'package:sushi_room/services/internal_api.dart'; import 'package:sushi_room/services/rooms_api.dart'; import 'package:sushi_room/services/routes.dart'; +import 'package:sushi_room/ui/components/update.dart'; +import 'package:universal_io/io.dart'; import 'package:url_launcher/url_launcher.dart'; class HomePage extends StatefulWidget { @@ -92,11 +94,33 @@ class _HomePageState extends State { }); } + checkForUpdate(_) async { + if (!Platform.isAndroid) return; + + String latest = await internalAPI.getLatestVersion(); + String current = await internalAPI.getVersion(); + + debugPrint("$latest $current"); + if (latest != "" && latest != current) { + // ignore: use_build_context_synchronously + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => OtaSheet( + version: latest, + ), + fullscreenDialog: true, + ), + ); + } + } + @override void initState() { setUpListener(); checkAnyLeftRoom(); + WidgetsBinding.instance.addPostFrameCallback(checkForUpdate); super.initState(); } diff --git a/pubspec.lock b/pubspec.lock index ea66bb5..f31420d 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -9,6 +9,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.6" + animated_text_kit: + dependency: "direct main" + description: + name: animated_text_kit + sha256: "37392a5376c9a1a503b02463c38bc0342ef814ddbb8f9977bc90f2a84b22fa92" + url: "https://pub.dev" + source: hosted + version: "4.2.2" animated_theme_switcher: dependency: "direct main" description: @@ -113,6 +121,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.3" + csslib: + dependency: transitive + description: + name: csslib + sha256: "706b5707578e0c1b4b7550f64078f0a0f19dec3f50a178ffae7006b0a9ca58fb" + url: "https://pub.dev" + source: hosted + version: "1.0.0" cupertino_icons: dependency: "direct main" description: @@ -381,8 +397,16 @@ packages: url: "https://pub.dev" source: hosted version: "6.0.0" + html: + dependency: "direct main" + description: + name: html + sha256: "3a7812d5bcd2894edf53dfaf8cd640876cf6cef50a8f238745c8b8120ea74d3a" + url: "https://pub.dev" + source: hosted + version: "0.15.4" http: - dependency: transitive + dependency: "direct main" description: name: http sha256: "759d1a329847dd0f39226c688d3e06a6b8679668e350e2891a6474f8b4bb8525" @@ -501,6 +525,30 @@ packages: url: "https://pub.dev" source: hosted version: "0.5.0" + ota_update: + dependency: "direct main" + description: + name: ota_update + sha256: "7ce0af2119458cb7cc3122ecdc4e18ec3c0108162bfab9a4b064d686e1a5fc22" + url: "https://pub.dev" + source: hosted + version: "5.1.0" + package_info_plus: + dependency: "direct main" + description: + name: package_info_plus + sha256: "7e76fad405b3e4016cd39d08f455a4eb5199723cf594cd1b8916d47140d93017" + url: "https://pub.dev" + source: hosted + version: "4.2.0" + package_info_plus_platform_interface: + dependency: transitive + description: + name: package_info_plus_platform_interface + sha256: "9bc8ba46813a4cc42c66ab781470711781940780fd8beddd0c3da62506d3a6c6" + url: "https://pub.dev" + source: hosted + version: "2.0.1" path: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 5668074..f7c43f0 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -2,12 +2,13 @@ name: sushi_room description: SushiRoom App publish_to: "none" -version: 1.1.0+1 +version: 1.0.0+1 environment: sdk: ">=3.1.2 <4.0.0" dependencies: + animated_text_kit: ^4.2.2 animated_theme_switcher: ^2.0.8 animations: ^2.0.8 app_settings: ^5.0.0 @@ -28,9 +29,13 @@ dependencies: fluttertoast: ^8.2.2 get: ^4.6.6 google_fonts: ^6.0.0 + html: ^0.15.4 + http: ^1.1.0 internet_connection_checker_plus: ^2.1.0 location: ^5.0.3 mobile_scanner: ^3.4.1 + ota_update: ^5.1.0 + package_info_plus: ^4.2.0 permission_handler: ^11.0.0 qr_flutter: ^4.1.0 shared_preferences: ^2.2.1