mirror of
https://github.com/bytedream/Yamete-Kudasai.git
synced 2025-05-11 13:15:09 +02:00
Compare commits
9 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
616b6e7468 | ||
|
64366ef317 | ||
|
f4d87fbcad | ||
|
ffaaac756f | ||
6711d44c59 | |||
|
39422ac97d | ||
|
471a7757e2 | ||
5cbc4bbfd7 | |||
498e3dcbf5 |
27
README.md
27
README.md
@ -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.
|
||||||
|
@ -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
|
||||||
|
@ -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
26
assets/sauce.json
Normal 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
24
assets/updates.json
Normal 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"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
@ -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'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -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) {
|
||||||
|
363
lib/main.dart
363
lib/main.dart
@ -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
71
lib/utils.dart
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
@ -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
|
||||||
|
@ -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.
|
||||||
|
Loading…
x
Reference in New Issue
Block a user