Compare commits

...

9 Commits

Author SHA1 Message Date
ByteDream
616b6e7468
More smartrelease link updates :3 2022-03-01 21:48:45 +01:00
ByteDream
64366ef317
Updated direct download links using smartrelease :3 2022-01-09 20:48:31 +01:00
ByteDream
f4d87fbcad
Typo fix 2021-12-05 22:48:36 +01:00
ByteDream
ffaaac756f
Update README.md 2021-11-21 22:16:05 +01:00
6711d44c59 Update v1.2.0 2021-11-20 02:27:48 +01:00
ByteDream
39422ac97d
Update README.md 2021-11-18 10:22:33 +01:00
ByteDream
471a7757e2
Update README.md 2021-11-17 23:11:07 +01:00
5cbc4bbfd7 Update v1.1.0 2021-11-17 22:31:47 +01:00
498e3dcbf5 Proper update service, no sound on normal app start 2021-11-17 22:17:54 +01:00
12 changed files with 493 additions and 187 deletions

View File

@ -1,13 +1,33 @@
# Yamete Kudasai # Yamete Kudasai
Cute anime girl moaning when something is plugged in. <p align="center">
<a href="https://smartrelease.bytedream.org/github/ByteDream/Yamete-Kudasai/yamete_kudasai-{tag}.apk">
<img src="https://img.shields.io/github/downloads/ByteDream/Yamete-Kudasai/total?style=flat-square" alt="Download Badge">
</a>
<a href="https://github.com/ByteDream/Yamete-Kudasai/releases/latest">
<img src="https://img.shields.io/github/v/release/ByteDream/Yamete-Kudasai?style=flat-square" alt="Latest release">
</a>
<a href="https://github.com/ByteDream/Yamete-Kudasai/blob/master/LICENSE">
<img src="https://img.shields.io/github/license/ByteDream/Yamete-Kudasai?style=flat-square" alt="License">
</a>
<a href="#">
<img src="https://img.shields.io/github/languages/top/ByteDream/Yamete-Kudasai?style=flat-square" alt="Top language">
</a>
</p>
<h5 align="center">Cute anime girls moaning when something is plugged in.</h5>
## Getting Started ## Getting Started
> Android only > Android only
**Download the latest release [here](https://smartrelease.bytedream.org/github/ByteDream/Yamete-Kudasai/yamete_kudasai-{tag}.apk).**
I think the sentence above says all about the app... I think the sentence above says all about the app...
It basically plays a sound whenever a event from the supported events below gets fired.
The sounds are predefined within the app and can easily be changed.
<img src="ext/preview_1.png" width=30%> <img src="ext/preview_2.png" width=30%> <img src="ext/preview_3.png" width=30%> <img src="ext/preview_1.png" width=30%> <img src="ext/preview_2.png" width=30%> <img src="ext/preview_3.png" width=30%>
@ -18,8 +38,8 @@ and a new window pops up where you can set the sound which should be played on t
Supported events: Supported events:
- **Battery** - **Battery**
- Battery charging - Battery charging (cable plugged in)
- Battery discharging - Battery discharging (cable plugged out)
- Battery full - Battery full
- **Headphone** - **Headphone**
- Headphone connected - Headphone connected
@ -29,3 +49,4 @@ Supported events:
This project is licensed under the Do What The F*ck You Want To Public License (WTFPL) - see the [LICENSE](LICENSE) file for more details. This project is licensed under the Do What The F*ck You Want To Public License (WTFPL) - see the [LICENSE](LICENSE) file for more details.
All rights for the audio and image files in [assets/audio](assets/audio), [assets/icon](assets/icon) and [android/app/src/main/res](android/app/src/main/res) are reserved to their artist and copyright holders.

View File

@ -32,7 +32,7 @@ apply plugin: 'com.android.application'
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
android { android {
compileSdkVersion 30 compileSdkVersion 31
compileOptions { compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8 sourceCompatibility JavaVersion.VERSION_1_8
@ -43,7 +43,7 @@ android {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId "org.bytedream.yamete_kudasai" applicationId "org.bytedream.yamete_kudasai"
minSdkVersion 16 minSdkVersion 16
targetSdkVersion 30 targetSdkVersion 31
versionCode flutterVersionCode.toInteger() versionCode flutterVersionCode.toInteger()
versionName flutterVersionName versionName flutterVersionName
multiDexEnabled true multiDexEnabled true

View File

@ -1,9 +1,9 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="org.bytedream.yamete_kudasai"> package="org.bytedream.yamete_kudasai">
<application <application
android:label="Yamete Kudasai" android:label="Yamete Kudasai"
android:icon="@mipmap/ic_launcher"> android:icon="@mipmap/ic_launcher">
<activity <activity
android:name=".MainActivity" android:name=".MainActivity"
android:launchMode="singleTop" android:launchMode="singleTop"
android:theme="@style/LaunchTheme" android:theme="@style/LaunchTheme"
@ -15,18 +15,18 @@
while the Flutter UI initializes. After that, this theme continues while the Flutter UI initializes. After that, this theme continues
to determine the Window background behind the Flutter UI. --> to determine the Window background behind the Flutter UI. -->
<meta-data <meta-data
android:name="io.flutter.embedding.android.NormalTheme" android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme" android:resource="@style/NormalTheme"
/> />
<!-- Displays an Android View that continues showing the launch screen <!-- Displays an Android View that continues showing the launch screen
Drawable until Flutter paints its first frame, then this splash Drawable until Flutter paints its first frame, then this splash
screen fades out. A splash screen is useful to avoid any visual screen fades out. A splash screen is useful to avoid any visual
gap between the end of Android's launch screen and the painting of gap between the end of Android's launch screen and the painting of
Flutter's first frame. --> Flutter's first frame. -->
<meta-data <meta-data
android:name="io.flutter.embedding.android.SplashScreenDrawable" android:name="io.flutter.embedding.android.SplashScreenDrawable"
android:resource="@drawable/launch_background" android:resource="@drawable/launch_background"
/> />
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN"/> <action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/> <category android:name="android.intent.category.LAUNCHER"/>
@ -38,22 +38,15 @@
android:name="flutterEmbedding" android:name="flutterEmbedding"
android:value="2" /> android:value="2" />
</application> </application>
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<queries> <queries>
<!-- If your app opens https URLs --> <!-- If your app opens https URLs -->
<intent> <intent>
<action android:name="android.intent.action.VIEW" /> <action android:name="android.intent.action.VIEW" />
<data android:scheme="https" /> <data android:scheme="https" />
</intent> </intent>
<!-- If your app makes calls -->
<intent>
<action android:name="android.intent.action.DIAL" />
<data android:scheme="tel" />
</intent>
<!-- If your app emails -->
<intent>
<action android:name="android.intent.action.SEND" />
<data android:mimeType="*/*" />
</intent>
</queries> </queries>
</manifest> </manifest>

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

@ -38,25 +38,13 @@ void initBackground() {
); );
Map<UpdateAction, String>? data; Map<UpdateAction, String>? data;
int running = 0;
StreamSubscription<UpdateAction?>? sub = _portUpdate.stream.listen((event) async { StreamSubscription<UpdateAction?>? sub = _portUpdate.stream.listen((event) async {
data ??= (jsonDecode(generateEventData(await SharedPreferences.getInstance())) as Map<String, dynamic>) data ??= (jsonDecode(generateEventData(await SharedPreferences.getInstance())) as Map<String, dynamic>)
.map((key, value) => MapEntry(UpdateAction.values.elementAt(int.parse(key)), value as String)); .map((key, value) => MapEntry(UpdateAction.values.elementAt(int.parse(key)), value as String));
if (data!.containsKey(event!)) { if (data!.containsKey(event!)) {
final player = await _player.play(data![event]!); final player = await _player.play(data![event]!);
running++;
FlutterBackgroundService().setNotificationInfo(
title: 'Yamete Kudasai',
content: 'Dispatching ${actions.values.elementAt(event.index).toLowerCase()} event'
);
await player.onPlayerCompletion.first; await player.onPlayerCompletion.first;
if (--running == 0) {
FlutterBackgroundService().setNotificationInfo(
title: 'Yamete Kudasai',
content: 'Running'
);
}
} }
}); });

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

@ -1,21 +1,40 @@
import 'dart:async'; import 'dart:async';
import 'dart:convert'; import 'dart:convert';
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';
import 'package:future_progress_dialog/future_progress_dialog.dart';
import 'package:http/http.dart' as http; 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:package_info_plus/package_info_plus.dart';
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';
void main() { void main() {
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp, DeviceOrientation.portraitDown]); SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp, DeviceOrientation.portraitDown]);
runApp(YameteKudasai()); 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 { class YameteKudasai extends StatefulWidget {
@ -26,103 +45,107 @@ class YameteKudasai extends StatefulWidget {
class _YameteKudasaiState extends State<YameteKudasai> { class _YameteKudasaiState extends State<YameteKudasai> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
http.get(Uri.https('api.github.com', 'repos/ByteDream/yamete_kudasai/releases/latest')) FlutterBackgroundService.initialize(initBackground, foreground: false);
.timeout(const Duration(seconds: 5), onTimeout: () => http.Response.bytes([], 504)).then((response) async { checkUpdate(context).then((value) => {
if (response.statusCode == 200) { if (!value) {
final packageInfo = await PackageInfo.fromPlatform(); checkFirstLaunch(context)
final tag = (jsonDecode(response.body) as Map<String, dynamic>)['tag_name'] as String;
if (int.parse(tag.substring(1).replaceAll('.', '')) > int.parse(packageInfo.version.replaceAll('.', ''))) {
showDialog(
context: context,
builder: (BuildContext context) => _buildUpdateNotification(context, tag)
);
}
} }
}); });
FlutterBackgroundService.initialize(initBackground, foreground: false);
return MaterialApp( return Scaffold(
title: "Yamete Kudasai", appBar: AppBar(
theme: ThemeData.from( title: const Text('Yamete Kudasai'),
colorScheme: const ColorScheme.highContrastDark(
primary: Color(0xFFFF0000),
primaryVariant: Color(0xFFC20000),
secondary: Colors.purple,
surface: Colors.black,
background: Colors.black26,
onPrimary: Colors.white,
),
), ),
home: Scaffold( body: Center(
appBar: AppBar( child: Column(
title: const Text('Yamete Kudasai'), children: [
), Padding(
body: Center( padding: const EdgeInsets.symmetric(vertical: 20),
child: Column( child: Row(
children: [ mainAxisAlignment: MainAxisAlignment.spaceEvenly,
Padding( children: [
padding: const EdgeInsets.symmetric(vertical: 20), ElevatedButton(
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: const [
children: [ Text("Start"),
ElevatedButton( Icon(Icons.play_arrow_outlined)
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( onPressed: () async {
child: Row( WidgetsFlutterBinding.ensureInitialized();
children: const [ FlutterBackgroundService().sendData({'action': 'stop'});
Text("Stop"), while (await isRunning()) {}
Icon(Icons.stop_outlined) FlutterBackgroundService.initialize(initBackground);
], },
), ),
onPressed: () { ElevatedButton(
FlutterBackgroundService().sendData({'action': 'stop'}); child: Row(
}, children: const [
) Text("Stop"),
], Icon(Icons.stop_outlined)
), ],
),
onPressed: () {
FlutterBackgroundService().sendData({'action': 'stop'});
},
)
],
), ),
const Divider(color: Colors.white), ),
_buildAudioSettings() const Divider(color: Colors.white),
], FutureBuilder(
), future: Sauce.sauceIndex(),
builder: (BuildContext context, AsyncSnapshot<Map<String, Sauce>> snapshot) {
if (!snapshot.hasData) {
return SizedBox.shrink();
}
return _buildAudioSettings(snapshot.data!);
}
)
],
), ),
), ),
); );
} }
Widget _buildUpdateNotification(BuildContext context, String tag) { Widget _buildUpdateNotification(BuildContext context, String tag, String apkUrl) {
return AlertDialog( return AlertDialog(
backgroundColor: Colors.black,
title: Text('Newer version is available ($tag)'), title: Text('Newer version is available ($tag)'),
actions: [ actions: [
TextButton( TextButton(
onPressed: () async { onPressed: () async {
if (await canLaunch('https://github.com/ByteDream/yamete_kudasai/releases/tag/$tag')) { await updateAPK(context, apkUrl);
await launch('https://github.com/ByteDream/yamete_kudasai/releases/tag/$tag'); await showDialog(
} context: context,
}, builder: (BuildContext context) => FutureProgressDialog(
child: const Text('Show new release') updateAPK(context, apkUrl),
decoration: const BoxDecoration(
color: Colors.transparent
),
message: const Text('Downloading update...'),
)
);
},
child: const Text('Update')
), ),
TextButton( TextButton(
onPressed: () => Navigator.of(context).pop(), onPressed: () async {
child: const Text('No thanks :)') 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() { 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) {
@ -134,56 +157,170 @@ class _YameteKudasaiState extends State<YameteKudasai> {
final entries = actions.entries; final entries = actions.entries;
return ListView.builder( return ListView.builder(
shrinkWrap: true, shrinkWrap: true,
itemCount: actions.length, itemCount: actions.length,
itemBuilder: (BuildContext context, int index) { itemBuilder: (BuildContext context, int index) {
final item = entries.elementAt(index); final item = entries.elementAt(index);
final activatedKey = "${item.key.index}.activated"; final activatedKey = "${item.key.index}.activated";
final targetKey = "${item.key.index}.target"; final targetKey = "${item.key.index}.target";
final activatedAudio = prefs.getBool(activatedKey) ?? true; final activatedAudio = prefs.getBool(activatedKey) ?? true;
final targetAudio = prefs.getString(targetKey) ?? "assets/audio/yamete_kudasai.mp3"; final targetAudio = prefs.getString(targetKey) ?? "assets/audio/yamete_kudasai.mp3";
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,
onChanged: (bool newValue) async { onChanged: (bool newValue) async {
SharedPreferences prefs = await SharedPreferences.getInstance(); SharedPreferences prefs = await SharedPreferences.getInstance();
prefs.setBool(activatedKey, !activatedAudio); prefs.setBool(activatedKey, !activatedAudio);
FlutterBackgroundService().sendData({'action': 'data', 'value': generateEventData(prefs)}); FlutterBackgroundService().sendData({'action': 'data', 'value': generateEventData(prefs)});
setState(() {}); setState(() {});
}), }),
onTap: () async { onTap: () async {
final audioFile = await Navigator.push<String>( final sauce = await Sauce.sauceIndex();
context, final audioFile = await Navigator.push<String>(
MaterialPageRoute( context,
builder: (BuildContext context) => ChooseAudio(targetAudio) MaterialPageRoute(
) builder: (BuildContext context) => ChooseAudio(targetAudio, sauce)
); )
if (audioFile != null && audioFile != targetAudio) { );
SharedPreferences prefs = await SharedPreferences.getInstance(); if (audioFile != null && audioFile != targetAudio) {
prefs.setString(targetKey, audioFile); SharedPreferences prefs = await SharedPreferences.getInstance();
FlutterBackgroundService().sendData({'action': 'data', 'value': generateEventData(prefs)}); prefs.setString(targetKey, audioFile);
setState(() {}); FlutterBackgroundService().sendData({'action': 'data', 'value': generateEventData(prefs)});
} setState(() {});
}, }
); },
} );
}
); );
} }
); );
} }
Future<bool> 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<String, dynamic>);
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<void> 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<void>();
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<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'});
await FlutterBackgroundService().onDataReceived.first.timeout(const Duration(milliseconds: 500)); await FlutterBackgroundService().onDataReceived.first.timeout(const Duration(milliseconds: 500));
return true; return true;
} on Exception catch (e) { } on Exception {
return false; return false;
} }
} }

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

@ -45,11 +45,11 @@ public class PortUpdatePlugin implements FlutterPlugin, StreamHandler {
batteryReceiver = createBatteryReceiver(eventSink); batteryReceiver = createBatteryReceiver(eventSink);
headphoneReceiver = createHeadphoneReceiver(eventSink); headphoneReceiver = createHeadphoneReceiver(eventSink);
context.registerReceiver(batteryReceiver, new IntentFilter(Intent.ACTION_BATTERY_CHANGED)); Intent batteryIntent = context.registerReceiver(batteryReceiver, new IntentFilter(Intent.ACTION_BATTERY_CHANGED));
context.registerReceiver(headphoneReceiver, new IntentFilter(Intent.ACTION_HEADSET_PLUG)); Intent headphoneIntent = context.registerReceiver(headphoneReceiver, new IntentFilter(Intent.ACTION_HEADSET_PLUG));
// lastBatteryStatus = getBatteryStatus(intent); if (batteryIntent != null) lastBatteryStatus = getBatteryStatus(batteryIntent);
// lastHeadphoneStatus = getHeadphoneStatus(intent); if (headphoneIntent != null) lastHeadphoneStatus = getHeadphoneStatus(headphoneIntent);
} }
@Override @Override

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.0.0 version: 1.2.0
environment: environment:
sdk: ">=2.12.0 <3.0.0" sdk: ">=2.12.0 <3.0.0"
@ -36,8 +36,12 @@ dependencies:
audioplayers: ^0.20.1 audioplayers: ^0.20.1
cupertino_icons: ^1.0.2 cupertino_icons: ^1.0.2
flutter_background_service: ^0.1.5 flutter_background_service: ^0.1.5
future_progress_dialog: ^0.2.0
http: ^0.13.4 http: ^0.13.4
open_file: ^3.2.1
package_info_plus: ^1.3.0 package_info_plus: ^1.3.0
path_provider: ^2.0.7
permission_handler: ^8.3.0
shared_preferences: ^2.0.8 shared_preferences: ^2.0.8
url_launcher: ^6.0.13 url_launcher: ^6.0.13
@ -69,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.