Update v1.2.0

This commit is contained in:
bytedream 2021-11-20 02:27:48 +01:00
parent 39422ac97d
commit 6711d44c59
8 changed files with 271 additions and 43 deletions

View File

@ -1,7 +1,7 @@
# Yamete Kudasai # Yamete Kudasai
<p align="center"> <p align="center">
<a href="https://github.com/ByteDream/Yamete-Kudasai/releases/download/v1.1.0/yamete_kudasai-v1.1.0.apk"> <a href="https://github.com/ByteDream/Yamete-Kudasai/releases/download/v1.2.0/yamete_kudasai-v1.2.0.apk">
<img src="https://img.shields.io/github/downloads/ByteDream/Yamete-Kudasai/total?style=flat-square" alt="Download Badge"> <img src="https://img.shields.io/github/downloads/ByteDream/Yamete-Kudasai/total?style=flat-square" alt="Download Badge">
</a> </a>
<a href="https://github.com/ByteDream/Yamete-Kudasai/releases/latest"> <a href="https://github.com/ByteDream/Yamete-Kudasai/releases/latest">
@ -21,7 +21,7 @@
> Android only > 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... I think the sentence above says all about the app...

26
assets/sauce.json Normal file
View File

@ -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
}
}

24
assets/updates.json Normal file
View File

@ -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"
]
}
}

View File

@ -1,16 +1,14 @@
import 'package:audioplayers/audioplayers.dart'; import 'package:audioplayers/audioplayers.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:url_launcher/url_launcher.dart';
final audio = { import 'package:yamete_kudasai/utils.dart';
'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'
};
class ChooseAudio extends StatefulWidget { class ChooseAudio extends StatefulWidget {
String _before; String _before;
final Map<String, Sauce> _sauce;
ChooseAudio(this._before, {Key? key}) : super(key: key); ChooseAudio(this._before, this._sauce, {Key? key}) : super(key: key);
@override @override
State<StatefulWidget> createState() => _ChooseAudioState(); State<StatefulWidget> createState() => _ChooseAudioState();
@ -33,36 +31,44 @@ class _ChooseAudioState extends State<ChooseAudio> {
title: const Text('Choose audio') title: const Text('Choose audio')
), ),
body: ListView.separated( body: ListView.separated(
itemBuilder: (BuildContext context, int index) { itemBuilder: (BuildContext context, int index) {
MapEntry<String, String> item = audio.entries.elementAt(index); MapEntry<String, Sauce> item = widget._sauce.entries.elementAt(index);
if (index == _playIndex) { if (index == _playIndex) {
if (_playing == null) { if (_playing == null) {
play(item.value); play(item.key);
} else { } else {
_playing!.stop().then((value) => play(item.value)); _playing!.stop().then((value) => play(item.key));
}
} }
} return ListTile(
return ListTile( leading: Radio(
leading: Radio( activeColor: Theme.of(context).colorScheme.secondary,
activeColor: Theme.of(context).colorScheme.secondary, value: item.value.filepath,
value: item.value, groupValue: widget._before,
groupValue: widget._before, onChanged: (String? value) {
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(() { setState(() {
widget._before = value!; _playIndex = index;
}); });
}), },
title: Text(item.key), );
trailing: Icon(_playIndex == index ? Icons.stop_outlined : Icons.play_arrow_outlined), },
onTap: () { separatorBuilder: (BuildContext context, int index) => const Divider(),
setState(() { itemCount: widget._sauce.length
_playIndex = index;
});
},
);
},
separatorBuilder: (BuildContext context, int index) => const Divider(),
itemCount: audio.length
), ),
), ),
); );
@ -76,6 +82,40 @@ class _ChooseAudioState extends State<ChooseAudio> {
super.dispose(); 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 { void play(String path) async {
_playing = await _player.play(path); _playing = await _player.play(path);
_playing!.onPlayerCompletion.listen((event) { _playing!.onPlayerCompletion.listen((event) {

View File

@ -2,6 +2,7 @@ import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_background_service/flutter_background_service.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:shared_preferences/shared_preferences.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
import 'package:yamete_kudasai/background.dart'; import 'package:yamete_kudasai/background.dart';
import 'package:yamete_kudasai/utils.dart';
import 'choose_audio.dart'; import 'choose_audio.dart';
@ -44,7 +46,11 @@ class _YameteKudasaiState extends State<YameteKudasai> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
FlutterBackgroundService.initialize(initBackground, foreground: false); FlutterBackgroundService.initialize(initBackground, foreground: false);
checkUpdate(context); checkUpdate(context).then((value) => {
if (!value) {
checkFirstLaunch(context)
}
});
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
@ -87,7 +93,15 @@ class _YameteKudasaiState extends State<YameteKudasai> {
), ),
), ),
const Divider(color: Colors.white), const Divider(color: Colors.white),
_buildAudioSettings() FutureBuilder(
future: Sauce.sauceIndex(),
builder: (BuildContext context, AsyncSnapshot<Map<String, Sauce>> snapshot) {
if (!snapshot.hasData) {
return SizedBox.shrink();
}
return _buildAudioSettings(snapshot.data!);
}
)
], ],
), ),
), ),
@ -131,7 +145,7 @@ class _YameteKudasaiState extends State<YameteKudasai> {
); );
} }
Widget _buildAudioSettings() { Widget _buildAudioSettings(Map<String, Sauce> sauceIndex) {
return FutureBuilder( return FutureBuilder(
future: SharedPreferences.getInstance(), future: SharedPreferences.getInstance(),
builder: (BuildContext context, AsyncSnapshot<SharedPreferences> snapshot) { builder: (BuildContext context, AsyncSnapshot<SharedPreferences> snapshot) {
@ -156,7 +170,7 @@ class _YameteKudasaiState extends State<YameteKudasai> {
return ListTile( return ListTile(
title: Text(item.value), 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( trailing: Switch(
activeColor: Theme.of(context).colorScheme.secondary, activeColor: Theme.of(context).colorScheme.secondary,
value: prefs.getBool(activatedKey) ?? true, value: prefs.getBool(activatedKey) ?? true,
@ -167,10 +181,11 @@ class _YameteKudasaiState extends State<YameteKudasai> {
setState(() {}); setState(() {});
}), }),
onTap: () async { onTap: () async {
final sauce = await Sauce.sauceIndex();
final audioFile = await Navigator.push<String>( final audioFile = await Navigator.push<String>(
context, context,
MaterialPageRoute( MaterialPageRoute(
builder: (BuildContext context) => ChooseAudio(targetAudio) builder: (BuildContext context) => ChooseAudio(targetAudio, sauce)
) )
); );
if (audioFile != null && audioFile != targetAudio) { if (audioFile != null && audioFile != targetAudio) {
@ -187,7 +202,7 @@ class _YameteKudasaiState extends State<YameteKudasai> {
); );
} }
Future<void> checkUpdate(BuildContext context) async { Future<bool> checkUpdate(BuildContext context) async {
final response = await http.get(Uri.https('api.github.com', 'repos/ByteDream/Yamete-Kudasai/releases/latest')) 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)); .timeout(const Duration(seconds: 5), onTimeout: () => http.Response.bytes([], 504));
if (response.statusCode == 200) { if (response.statusCode == 200) {
@ -200,8 +215,10 @@ class _YameteKudasaiState extends State<YameteKudasai> {
context: context, context: context,
builder: (BuildContext context) => _buildUpdateNotification(context, tag, apkUrl) builder: (BuildContext context) => _buildUpdateNotification(context, tag, apkUrl)
); );
return true;
} }
} }
return false;
} }
Future<void> updateAPK(BuildContext context, String apkUrl) async { Future<void> updateAPK(BuildContext context, String apkUrl) async {
@ -235,6 +252,7 @@ class _YameteKudasaiState extends State<YameteKudasai> {
context: context, context: context,
builder: (BuildContext context) { builder: (BuildContext context) {
return AlertDialog( return AlertDialog(
backgroundColor: Colors.black,
title: const Text('Failed to install update'), title: const Text('Failed to install update'),
actions: [ actions: [
TextButton( TextButton(
@ -250,6 +268,53 @@ class _YameteKudasaiState extends State<YameteKudasai> {
// await file.delete(); // await file.delete();
} }
Future<void> 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<bool> isRunning() async { Future<bool> isRunning() async {
try { try {
FlutterBackgroundService().sendData({'action': 'ping'}); FlutterBackgroundService().sendData({'action': 'ping'});

71
lib/utils.dart Normal file
View File

@ -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<Map<String, Sauce>> sauceIndex() async {
final sauceJson = await rootBundle.loadString('assets/sauce.json');
Map<String, dynamic> sauce = jsonDecode(sauceJson);
Map<String, Sauce> sauceIndex = new Map();
for (MapEntry<String, dynamic> entry in sauce.entries) {
final value = entry.value as Map<String, dynamic>;
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<String> details;
const Update(this.version,
this.summary,
this.details);
static Future<Map<String, Update>> updatesIndex() async {
final updatesJson = await rootBundle.loadString('assets/updates.json');
Map<String, dynamic> updates = jsonDecode(updatesJson);
Map<String, Update> updatesIndex = new Map();
for (MapEntry<String, dynamic> entry in updates.entries) {
final value = entry.value as Map<String, dynamic>;
updatesIndex[entry.key] = Update(
entry.key,
value['summary'],
(value['details'] as List).cast<String>()
);
}
return updatesIndex;
}
}

View File

@ -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. # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
# Read more about iOS versioning at # Read more about iOS versioning at
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
version: 1.1.0 version: 1.2.0
environment: environment:
sdk: ">=2.12.0 <3.0.0" sdk: ">=2.12.0 <3.0.0"
@ -73,6 +73,8 @@ flutter:
# To add assets to your application, add an assets section, like this: # To add assets to your application, add an assets section, like this:
assets: assets:
- assets/audio/ - assets/audio/
- assets/sauce.json
- assets/updates.json
# An image asset can refer to one or more resolution-specific "variants", see # An image asset can refer to one or more resolution-specific "variants", see
# https://flutter.dev/assets-and-images/#resolution-aware. # https://flutter.dev/assets-and-images/#resolution-aware.