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