From 6711d44c5945c613c431a98a7117878a93e36fc0 Mon Sep 17 00:00:00 2001 From: bytedream Date: Sat, 20 Nov 2021 02:27:48 +0100 Subject: [PATCH] Update v1.2.0 --- README.md | 4 +- ...on.mp3 => sanas_first_tutoring_lesson.mp3} | Bin assets/sauce.json | 26 +++++ assets/updates.json | 24 ++++ lib/choose_audio.dart | 108 ++++++++++++------ lib/main.dart | 77 ++++++++++++- lib/utils.dart | 71 ++++++++++++ pubspec.yaml | 4 +- 8 files changed, 271 insertions(+), 43 deletions(-) rename assets/audio/{airis_first_tutoring_lesson.mp3 => sanas_first_tutoring_lesson.mp3} (100%) create mode 100644 assets/sauce.json create mode 100644 assets/updates.json create mode 100644 lib/utils.dart diff --git a/README.md b/README.md index 785f6e4..43a65a9 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # Yamete Kudasai

- + Download Badge @@ -21,7 +21,7 @@ > Android only -**Download the latest release [here](https://github.com/ByteDream/Yamete-Kudasai/releases/download/v1.1.0/yamete_kudasai-v1.1.0.apk).** +**Download the latest release [here](https://github.com/ByteDream/Yamete-Kudasai/releases/download/v1.2.0/yamete_kudasai-v1.2.0.apk).** I think the sentence above says all about the app... diff --git a/assets/audio/airis_first_tutoring_lesson.mp3 b/assets/audio/sanas_first_tutoring_lesson.mp3 similarity index 100% rename from assets/audio/airis_first_tutoring_lesson.mp3 rename to assets/audio/sanas_first_tutoring_lesson.mp3 diff --git a/assets/sauce.json b/assets/sauce.json new file mode 100644 index 0000000..167ca46 --- /dev/null +++ b/assets/sauce.json @@ -0,0 +1,26 @@ +{ + "assets/audio/sanas_first_tutoring_lesson.mp3": { + "alias": "Sana's first tutoring lesson", + "name": "Oni ChiChi", + "season": "1", + "episode": "1", + "from": "0:03", + "to": "0:06" + }, + "assets/audio/the_helpful_pharmacist.mp3": { + "alias": "The helpful pharmacist", + "name": "Rune's Pharmacy: Tiarajima no Okusuriya-san", + "season": null, + "episode": "2", + "from": "5:55", + "to": "5:59" + }, + "assets/audio/yamete_kudasai.mp3": { + "alias": "Yamete Kudasai", + "name": "Yamete Kudasai", + "season": null, + "episode": null, + "from": null, + "to": null + } +} diff --git a/assets/updates.json b/assets/updates.json new file mode 100644 index 0000000..8da5e10 --- /dev/null +++ b/assets/updates.json @@ -0,0 +1,24 @@ +{ + "1.0.0": { + "summary": "Initial release", + "details": [ + "Initial release" + ] + }, + "1.1.0": { + "summary": "Update check & bug fixing", + "details": [ + "Update check", + "Permanent notification changes", + "No sound is played anymore when starting the app" + ] + }, + "1.2.0": { + "summary": "Sauce & update list", + "details": [ + "Added sauce information", + "Added notification after updated", + "Renamed one audio" + ] + } +} diff --git a/lib/choose_audio.dart b/lib/choose_audio.dart index 02a43dc..a5bc742 100644 --- a/lib/choose_audio.dart +++ b/lib/choose_audio.dart @@ -1,16 +1,14 @@ import 'package:audioplayers/audioplayers.dart'; +import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; - -final audio = { - 'Airi\'s first tutoring lesson': 'assets/audio/airis_first_tutoring_lesson.mp3', - 'The helpful pharmacist': 'assets/audio/the_helpful_pharmacist.mp3', - 'Yamete Kudasai': 'assets/audio/yamete_kudasai.mp3' -}; +import 'package:url_launcher/url_launcher.dart'; +import 'package:yamete_kudasai/utils.dart'; class ChooseAudio extends StatefulWidget { String _before; + final Map _sauce; - ChooseAudio(this._before, {Key? key}) : super(key: key); + ChooseAudio(this._before, this._sauce, {Key? key}) : super(key: key); @override State createState() => _ChooseAudioState(); @@ -33,36 +31,44 @@ class _ChooseAudioState extends State { title: const Text('Choose audio') ), body: ListView.separated( - itemBuilder: (BuildContext context, int index) { - MapEntry item = audio.entries.elementAt(index); - if (index == _playIndex) { - if (_playing == null) { - play(item.value); - } else { - _playing!.stop().then((value) => play(item.value)); + itemBuilder: (BuildContext context, int index) { + MapEntry item = widget._sauce.entries.elementAt(index); + if (index == _playIndex) { + if (_playing == null) { + play(item.key); + } else { + _playing!.stop().then((value) => play(item.key)); + } } - } - return ListTile( - leading: Radio( - activeColor: Theme.of(context).colorScheme.secondary, - value: item.value, - groupValue: widget._before, - onChanged: (String? value) { + return ListTile( + leading: Radio( + activeColor: Theme.of(context).colorScheme.secondary, + value: item.value.filepath, + groupValue: widget._before, + onChanged: (String? value) { + setState(() { + widget._before = value!; + }); + }), + title: Text(item.value.alias), + trailing: IconButton( + onPressed: () { + showDialog( + context: context, + builder: (BuildContext context) => _buildSauceInfo(context, item.value) + ); + }, + icon: Icon(Icons.info) + ), + onTap: () { setState(() { - widget._before = value!; + _playIndex = index; }); - }), - title: Text(item.key), - trailing: Icon(_playIndex == index ? Icons.stop_outlined : Icons.play_arrow_outlined), - onTap: () { - setState(() { - _playIndex = index; - }); - }, - ); - }, - separatorBuilder: (BuildContext context, int index) => const Divider(), - itemCount: audio.length + }, + ); + }, + separatorBuilder: (BuildContext context, int index) => const Divider(), + itemCount: widget._sauce.length ), ), ); @@ -76,6 +82,40 @@ class _ChooseAudioState extends State { super.dispose(); } + Widget _buildSauceInfo(BuildContext context, Sauce sauce) { + return AlertDialog( + backgroundColor: Colors.black, + title: Text('${sauce.alias} sauce'), + content: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('Name: ${sauce.name}'), + Text('Season: ${sauce.season ?? "?"}'), + Text('Episode: ${sauce.episode ?? "?"}'), + Text('Audio time: ${sauce.from ?? "?"} - ${sauce.to ?? "?"}'), + ], + ), + actions: [ + TextButton( + onPressed: () async { + final search = 'https://hentaihaven.com/?s=${sauce.name.replaceAll(" ", "+")}'; + if (await canLaunch(search)) { + await launch(search); + } + }, + child: const Text('Search online') + ), + TextButton( + onPressed: () { + Navigator.of(context).pop(); + }, + child: const Text('Ok') + ) + ], + ); + } + void play(String path) async { _playing = await _player.play(path); _playing!.onPlayerCompletion.listen((event) { diff --git a/lib/main.dart b/lib/main.dart index 0146fc7..1369037 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -2,6 +2,7 @@ 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'; @@ -13,6 +14,7 @@ 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'; @@ -44,7 +46,11 @@ class _YameteKudasaiState extends State { @override Widget build(BuildContext context) { FlutterBackgroundService.initialize(initBackground, foreground: false); - checkUpdate(context); + checkUpdate(context).then((value) => { + if (!value) { + checkFirstLaunch(context) + } + }); return Scaffold( appBar: AppBar( @@ -87,7 +93,15 @@ class _YameteKudasaiState extends State { ), ), const Divider(color: Colors.white), - _buildAudioSettings() + FutureBuilder( + future: Sauce.sauceIndex(), + builder: (BuildContext context, AsyncSnapshot> snapshot) { + if (!snapshot.hasData) { + return SizedBox.shrink(); + } + return _buildAudioSettings(snapshot.data!); + } + ) ], ), ), @@ -131,7 +145,7 @@ class _YameteKudasaiState extends State { ); } - Widget _buildAudioSettings() { + Widget _buildAudioSettings(Map sauceIndex) { return FutureBuilder( future: SharedPreferences.getInstance(), builder: (BuildContext context, AsyncSnapshot snapshot) { @@ -156,7 +170,7 @@ class _YameteKudasaiState extends State { return ListTile( title: Text(item.value), - subtitle: Text(audio.entries.firstWhere((element) => element.value == targetAudio).key), + subtitle: Text(sauceIndex.entries.firstWhere((element) => element.key == targetAudio).value.alias), trailing: Switch( activeColor: Theme.of(context).colorScheme.secondary, value: prefs.getBool(activatedKey) ?? true, @@ -167,10 +181,11 @@ class _YameteKudasaiState extends State { setState(() {}); }), onTap: () async { + final sauce = await Sauce.sauceIndex(); final audioFile = await Navigator.push( context, MaterialPageRoute( - builder: (BuildContext context) => ChooseAudio(targetAudio) + builder: (BuildContext context) => ChooseAudio(targetAudio, sauce) ) ); if (audioFile != null && audioFile != targetAudio) { @@ -187,7 +202,7 @@ class _YameteKudasaiState extends State { ); } - Future checkUpdate(BuildContext context) async { + 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) { @@ -200,8 +215,10 @@ class _YameteKudasaiState extends State { context: context, builder: (BuildContext context) => _buildUpdateNotification(context, tag, apkUrl) ); + return true; } } + return false; } Future updateAPK(BuildContext context, String apkUrl) async { @@ -235,6 +252,7 @@ class _YameteKudasaiState extends State { context: context, builder: (BuildContext context) { return AlertDialog( + backgroundColor: Colors.black, title: const Text('Failed to install update'), actions: [ TextButton( @@ -250,6 +268,53 @@ class _YameteKudasaiState extends State { // 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'}); diff --git a/lib/utils.dart b/lib/utils.dart new file mode 100644 index 0000000..a00afcf --- /dev/null +++ b/lib/utils.dart @@ -0,0 +1,71 @@ +import 'dart:convert'; + +import 'package:flutter/services.dart'; + +class Sauce { + final String filepath; + final String alias; + final String name; + final String? season; + final String? episode; + final String? from; + final String? to; + + const Sauce(this.filepath, + this.alias, + this.name, + this.season, + this.episode, + this.from, + this.to); + + static Future> sauceIndex() async { + final sauceJson = await rootBundle.loadString('assets/sauce.json'); + Map sauce = jsonDecode(sauceJson); + + Map sauceIndex = new Map(); + + for (MapEntry entry in sauce.entries) { + final value = entry.value as Map; + sauceIndex[entry.key] = Sauce( + entry.key, + value['alias'], + value['name'], + value['season'], + value['episode'], + value['from'], + value['to'] + ); + } + + return sauceIndex; + } +} + +class Update { + final String version; + final String summary; + final List details; + + const Update(this.version, + this.summary, + this.details); + + static Future> updatesIndex() async { + final updatesJson = await rootBundle.loadString('assets/updates.json'); + Map updates = jsonDecode(updatesJson); + + Map updatesIndex = new Map(); + + for (MapEntry entry in updates.entries) { + final value = entry.value as Map; + updatesIndex[entry.key] = Update( + entry.key, + value['summary'], + (value['details'] as List).cast() + ); + } + + return updatesIndex; + } +} diff --git a/pubspec.yaml b/pubspec.yaml index 1cb2461..fe650c1 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -15,7 +15,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. # Read more about iOS versioning at # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html -version: 1.1.0 +version: 1.2.0 environment: sdk: ">=2.12.0 <3.0.0" @@ -73,6 +73,8 @@ flutter: # To add assets to your application, add an assets section, like this: assets: - assets/audio/ + - assets/sauce.json + - assets/updates.json # An image asset can refer to one or more resolution-specific "variants", see # https://flutter.dev/assets-and-images/#resolution-aware.