Initial commit

This commit is contained in:
2021-11-16 22:29:44 +01:00
commit bd221bb030
49 changed files with 1248 additions and 0 deletions

84
lib/background.dart Normal file
View File

@ -0,0 +1,84 @@
import 'dart:async';
import 'dart:convert';
import 'package:audioplayers/audioplayers.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter_background_service/flutter_background_service.dart';
import 'package:port_update/port_update.dart';
import 'package:shared_preferences/shared_preferences.dart';
final actions = {
UpdateAction.batteryCharging: "Battery charging",
UpdateAction.batteryDischarging: "Battery discharging",
UpdateAction.batteryFull: "Battery full",
UpdateAction.headphoneConnected: "Headphone connected",
UpdateAction.headphoneDisconnected: "Headphone disconnected",
};
final _player = AudioCache(prefix: '');
final _portUpdate = PortUpdate();
String generateEventData(SharedPreferences sharedPreferences) {
Map<String, String> data = {};
for (var action in UpdateAction.values) {
if (sharedPreferences.getBool("${action.index}.activated") ?? true) {
data[action.index.toString()] = sharedPreferences.getString("${action.index}.target") ?? "assets/audio/yamete_kudasai.mp3";
}
}
return jsonEncode(data);
}
void initBackground() {
WidgetsFlutterBinding.ensureInitialized();
FlutterBackgroundService().setNotificationInfo(
title: "Yamete Kudasai",
content: "Preparing",
);
Map<UpdateAction, String>? data;
int running = 0;
StreamSubscription<UpdateAction?>? sub = _portUpdate.stream.listen((event) async {
data ??= (jsonDecode(generateEventData(await SharedPreferences.getInstance())) as Map<String, dynamic>)
.map((key, value) => MapEntry(UpdateAction.values.elementAt(int.parse(key)), value as String));
if (data!.containsKey(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;
if (--running == 0) {
FlutterBackgroundService().setNotificationInfo(
title: 'Yamete Kudasai',
content: 'Running'
);
}
}
});
FlutterBackgroundService().setNotificationInfo(
title: "Yamete Kudasai",
content: "Running",
);
FlutterBackgroundService().onDataReceived.listen((event) async {
switch (event!['action']) {
case 'stop':
await sub.cancel();
FlutterBackgroundService().stopBackgroundService();
break;
case 'data':
data = (jsonDecode(event['value']) as Map<String, dynamic>).map((key, value) {
return MapEntry(UpdateAction.values.elementAt(int.parse(key)), value.toString());
});
break;
case 'ping':
FlutterBackgroundService().sendData({'action': 'pong'});
break;
}
});
}

87
lib/choose_audio.dart Normal file
View File

@ -0,0 +1,87 @@
import 'package:audioplayers/audioplayers.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'
};
class ChooseAudio extends StatefulWidget {
String _before;
ChooseAudio(this._before, {Key? key}) : super(key: key);
@override
State<StatefulWidget> createState() => _ChooseAudioState();
}
class _ChooseAudioState extends State<ChooseAudio> {
final _player = AudioCache(prefix: '');
int _playIndex = -1;
AudioPlayer? _playing;
@override
Widget build(BuildContext context) {
return WillPopScope(
onWillPop: () async {
Navigator.pop(context, widget._before);
return true;
},
child: Scaffold(
appBar: AppBar(
title: const Text('Choose audio')
),
body: ListView.separated(
itemBuilder: (BuildContext context, int index) {
MapEntry<String, String> item = audio.entries.elementAt(index);
if (index == _playIndex) {
if (_playing == null) {
play(item.value);
} else {
_playing!.stop().then((value) => play(item.value));
}
}
return ListTile(
leading: Radio(
activeColor: Theme.of(context).colorScheme.secondary,
value: item.value,
groupValue: widget._before,
onChanged: (String? value) {
setState(() {
widget._before = value!;
});
}),
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
),
),
);
}
@override
void dispose() {
if (_playing != null) {
_playing!.stop();
}
super.dispose();
}
void play(String path) async {
_playing = await _player.play(path);
_playing!.onPlayerCompletion.listen((event) {
setState(() {
_playIndex = -1;
});
});
}
}

190
lib/main.dart Normal file
View File

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