import 'dart:async'; import 'dart:convert'; import 'dart:io'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_background_service/flutter_background_service.dart'; import 'package:future_progress_dialog/future_progress_dialog.dart'; import 'package:http/http.dart' as http; import 'package:open_file/open_file.dart'; import 'package:package_info_plus/package_info_plus.dart'; import 'package:permission_handler/permission_handler.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:url_launcher/url_launcher.dart'; import 'package:yamete_kudasai/background.dart'; import 'package:yamete_kudasai/utils.dart'; import 'choose_audio.dart'; void main() { WidgetsFlutterBinding.ensureInitialized(); SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp, DeviceOrientation.portraitDown]); runApp(MaterialApp( title: "Yamete Kudasai", theme: ThemeData.from( colorScheme: const ColorScheme.highContrastDark( primary: Color(0xFFFF0000), primaryVariant: Color(0xFFC20000), secondary: Colors.purple, surface: Colors.black, background: Colors.black12, onPrimary: Colors.white, ), ), home: YameteKudasai(), )); } class YameteKudasai extends StatefulWidget { @override _YameteKudasaiState createState() => _YameteKudasaiState(); } class _YameteKudasaiState extends State { @override Widget build(BuildContext context) { FlutterBackgroundService.initialize(initBackground, foreground: false); checkUpdate(context).then((value) => { if (!value) { checkFirstLaunch(context) } }); return Scaffold( appBar: AppBar( title: const Text('Yamete Kudasai'), ), body: Center( child: Column( children: [ Padding( padding: const EdgeInsets.symmetric(vertical: 20), child: Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ ElevatedButton( child: Row( children: const [ Text("Start"), Icon(Icons.play_arrow_outlined) ], ), onPressed: () async { WidgetsFlutterBinding.ensureInitialized(); FlutterBackgroundService().sendData({'action': 'stop'}); while (await isRunning()) {} FlutterBackgroundService.initialize(initBackground); }, ), ElevatedButton( child: Row( children: const [ Text("Stop"), Icon(Icons.stop_outlined) ], ), onPressed: () { FlutterBackgroundService().sendData({'action': 'stop'}); }, ) ], ), ), const Divider(color: Colors.white), FutureBuilder( future: Sauce.sauceIndex(), builder: (BuildContext context, AsyncSnapshot> snapshot) { if (!snapshot.hasData) { return SizedBox.shrink(); } return _buildAudioSettings(snapshot.data!); } ) ], ), ), ); } Widget _buildUpdateNotification(BuildContext context, String tag, String apkUrl) { return AlertDialog( backgroundColor: Colors.black, title: Text('Newer version is available ($tag)'), actions: [ TextButton( onPressed: () async { await updateAPK(context, apkUrl); await showDialog( context: context, builder: (BuildContext context) => FutureProgressDialog( updateAPK(context, apkUrl), decoration: const BoxDecoration( color: Colors.transparent ), message: const Text('Downloading update...'), ) ); }, child: const Text('Update') ), TextButton( onPressed: () async { if (await canLaunch('https://github.com/ByteDream/Yamete-Kudasai/releases/tag/$tag')) { await launch('https://github.com/ByteDream/Yamete-Kudasai/releases/tag/$tag'); } }, child: const Text('Show new release') ), TextButton( onPressed: () => Navigator.of(context).pop(), child: const Text('No thanks :)') ) ], ); } Widget _buildAudioSettings(Map sauceIndex) { return FutureBuilder( future: SharedPreferences.getInstance(), builder: (BuildContext context, AsyncSnapshot snapshot) { if (!snapshot.hasData) { return const SizedBox.shrink(); } final prefs = snapshot.data!; final entries = actions.entries; return ListView.builder( shrinkWrap: true, itemCount: actions.length, itemBuilder: (BuildContext context, int index) { final item = entries.elementAt(index); final activatedKey = "${item.key.index}.activated"; final targetKey = "${item.key.index}.target"; final activatedAudio = prefs.getBool(activatedKey) ?? true; final targetAudio = prefs.getString(targetKey) ?? "assets/audio/yamete_kudasai.mp3"; return ListTile( title: Text(item.value), subtitle: Text(sauceIndex.entries.firstWhere((element) => element.key == targetAudio).value.alias), trailing: Switch( activeColor: Theme.of(context).colorScheme.secondary, value: prefs.getBool(activatedKey) ?? true, onChanged: (bool newValue) async { SharedPreferences prefs = await SharedPreferences.getInstance(); prefs.setBool(activatedKey, !activatedAudio); FlutterBackgroundService().sendData({'action': 'data', 'value': generateEventData(prefs)}); setState(() {}); }), onTap: () async { final sauce = await Sauce.sauceIndex(); final audioFile = await Navigator.push( context, MaterialPageRoute( builder: (BuildContext context) => ChooseAudio(targetAudio, sauce) ) ); if (audioFile != null && audioFile != targetAudio) { SharedPreferences prefs = await SharedPreferences.getInstance(); prefs.setString(targetKey, audioFile); FlutterBackgroundService().sendData({'action': 'data', 'value': generateEventData(prefs)}); setState(() {}); } }, ); } ); } ); } Future checkUpdate(BuildContext context) async { final response = await http.get(Uri.https('api.github.com', 'repos/ByteDream/Yamete-Kudasai/releases/latest')) .timeout(const Duration(seconds: 5), onTimeout: () => http.Response.bytes([], 504)); if (response.statusCode == 200) { final packageInfo = await PackageInfo.fromPlatform(); final json = (jsonDecode(response.body) as Map); final tag = json['tag_name'] as String; final apkUrl = json['assets'][0]['browser_download_url'] as String; if (int.parse(tag.substring(1).replaceAll('.', '')) > int.parse(packageInfo.version.replaceAll('.', ''))) { await showDialog( context: context, builder: (BuildContext context) => _buildUpdateNotification(context, tag, apkUrl) ); return true; } } return false; } Future updateAPK(BuildContext context, String apkUrl) async { ResultType result; if ((await Permission.storage.request()).isGranted) { final file = File('/storage/emulated/0/Download/${apkUrl.split('/').last}'); final completer = Completer(); showDialog( context: context, builder: (BuildContext context) { return FutureProgressDialog( completer.future, decoration: const BoxDecoration( color: Colors.transparent ), message: const Text('Downloading update...'), ); } ); if (!(await file.exists())) { final resp = await http.get(Uri.parse(apkUrl)); await file.writeAsBytes(resp.bodyBytes); } result = (await OpenFile.open(file.path)).type; completer.complete(); } else { result = ResultType.error; } if (result != ResultType.done) { showDialog( context: context, builder: (BuildContext context) { return AlertDialog( backgroundColor: Colors.black, title: const Text('Failed to install update'), actions: [ TextButton( onPressed: () => Navigator.of(context).pop(), child: const Text('Ok'), ) ], ); } ); } Navigator.pop(context); // await file.delete(); } Future checkFirstLaunch(BuildContext context) async { final prefs = await SharedPreferences.getInstance(); final packageInfo = await PackageInfo.fromPlatform(); final lastVersion = prefs.getString("version"); final currentVersion = packageInfo.version; if ((lastVersion ?? "") != currentVersion) { final updateIndex = await Update.updatesIndex(); await showDialog( context: context, builder: (BuildContext context) => _buildUpdateNotice(context, updateIndex[currentVersion]!) ); await prefs.setString("version", currentVersion); } } Widget _buildUpdateNotice(BuildContext context, Update update) { return AlertDialog( backgroundColor: Colors.black, title: Text('Updated to ${update.version}'), actions: [ TextButton( onPressed: () async { final url = 'https://github.com/ByteDream/Yamete-Kudasai/releases/tag/v${update.version}'; if (await canLaunch(url)) { await launch(url); } }, child: const Text('See more...') ), TextButton( onPressed: () => Navigator.of(context).pop(), child: const Text('Ok'), ) ], content: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(update.summary), Text(' » ${update.details.join("\n » ")}') ], ) ); } Future isRunning() async { try { FlutterBackgroundService().sendData({'action': 'ping'}); await FlutterBackgroundService().onDataReceived.first.timeout(const Duration(milliseconds: 500)); return true; } on Exception { return false; } } }