2020-02-25 10:57:12 +01:00
|
|
|
import 'dart:async';
|
2020-07-25 07:42:48 +02:00
|
|
|
import 'dart:math' as math;
|
2020-02-25 10:57:12 +01:00
|
|
|
|
2020-03-14 04:14:24 +01:00
|
|
|
import 'package:audio_service/audio_service.dart';
|
2020-05-18 19:03:45 +02:00
|
|
|
import 'package:dio/dio.dart';
|
2020-07-26 12:20:42 +02:00
|
|
|
import 'package:flutter/foundation.dart';
|
|
|
|
import 'package:just_audio/just_audio.dart';
|
2020-05-18 19:03:45 +02:00
|
|
|
|
2020-07-26 12:20:42 +02:00
|
|
|
import '../local_storage/key_value_storage.dart';
|
|
|
|
import '../local_storage/sqflite_localpodcast.dart';
|
2020-05-06 18:50:32 +02:00
|
|
|
import '../type/episodebrief.dart';
|
2020-07-22 11:34:32 +02:00
|
|
|
import '../type/play_histroy.dart';
|
|
|
|
import '../type/playlist.dart';
|
2020-02-25 10:57:12 +01:00
|
|
|
|
2020-03-14 04:14:24 +01:00
|
|
|
MediaControl playControl = MediaControl(
|
|
|
|
androidIcon: 'drawable/ic_stat_play_circle_filled',
|
|
|
|
label: 'Play',
|
|
|
|
action: MediaAction.play,
|
|
|
|
);
|
|
|
|
MediaControl pauseControl = MediaControl(
|
|
|
|
androidIcon: 'drawable/ic_stat_pause_circle_filled',
|
|
|
|
label: 'Pause',
|
|
|
|
action: MediaAction.pause,
|
|
|
|
);
|
|
|
|
MediaControl skipToNextControl = MediaControl(
|
|
|
|
androidIcon: 'drawable/baseline_skip_next_white_24',
|
|
|
|
label: 'Next',
|
|
|
|
action: MediaAction.skipToNext,
|
|
|
|
);
|
|
|
|
MediaControl skipToPreviousControl = MediaControl(
|
|
|
|
androidIcon: 'drawable/ic_action_skip_previous',
|
|
|
|
label: 'Previous',
|
|
|
|
action: MediaAction.skipToPrevious,
|
|
|
|
);
|
|
|
|
MediaControl stopControl = MediaControl(
|
|
|
|
androidIcon: 'drawable/baseline_close_white_24',
|
|
|
|
label: 'Stop',
|
|
|
|
action: MediaAction.stop,
|
|
|
|
);
|
2020-07-24 19:17:47 +02:00
|
|
|
MediaControl forward = MediaControl(
|
|
|
|
androidIcon: 'drawable/baseline_fast_forward_white_24',
|
|
|
|
label: 'forward',
|
2020-03-14 04:14:24 +01:00
|
|
|
action: MediaAction.fastForward,
|
|
|
|
);
|
|
|
|
|
|
|
|
void _audioPlayerTaskEntrypoint() async {
|
|
|
|
AudioServiceBackground.run(() => AudioPlayerTask());
|
|
|
|
}
|
2020-02-25 10:57:12 +01:00
|
|
|
|
2020-07-22 11:34:32 +02:00
|
|
|
/// Sleep timer mode.
|
|
|
|
enum SleepTimerMode { endOfEpisode, timer, undefined }
|
2020-07-30 19:18:56 +02:00
|
|
|
enum PlayerHeight { short, mid, tall }
|
2020-07-22 11:34:32 +02:00
|
|
|
//enum ShareStatus { generate, download, complete, undefined, error }
|
2020-02-09 13:29:09 +01:00
|
|
|
|
2020-07-22 11:34:32 +02:00
|
|
|
class AudioPlayerNotifier extends ChangeNotifier {
|
2020-02-25 10:57:12 +01:00
|
|
|
DBHelper dbHelper = DBHelper();
|
2020-07-22 11:34:32 +02:00
|
|
|
var positionStorage = KeyValueStorage(audioPositionKey);
|
|
|
|
var autoPlayStorage = KeyValueStorage(autoPlayKey);
|
|
|
|
var autoSleepTimerStorage = KeyValueStorage(autoSleepTimerKey);
|
|
|
|
var defaultSleepTimerStorage = KeyValueStorage(defaultSleepTimerKey);
|
|
|
|
var autoSleepTimerModeStorage = KeyValueStorage(autoSleepTimerModeKey);
|
|
|
|
var autoSleepTimerStartStorage = KeyValueStorage(autoSleepTimerStartKey);
|
|
|
|
var autoSleepTimerEndStorage = KeyValueStorage(autoSleepTimerEndKey);
|
|
|
|
var fastForwardSecondsStorage = KeyValueStorage(fastForwardSecondsKey);
|
|
|
|
var rewindSecondsStorage = KeyValueStorage(rewindSecondsKey);
|
2020-07-30 19:18:56 +02:00
|
|
|
var playerHeightStorage = KeyValueStorage(playerHeightKey);
|
2020-08-01 09:31:18 +02:00
|
|
|
var speedStorage = KeyValueStorage(speedKey);
|
|
|
|
var skipSilenceStorage = KeyValueStorage(skipSilenceKey);
|
2020-08-09 17:10:32 +02:00
|
|
|
var boostVolumeStorage = KeyValueStorage(boostVolumeKey);
|
|
|
|
var volumeGainStorage = KeyValueStorage(volumeGainKey);
|
2020-07-22 11:34:32 +02:00
|
|
|
|
|
|
|
/// Current playing episdoe.
|
|
|
|
EpisodeBrief _episode;
|
2020-03-31 18:36:20 +02:00
|
|
|
|
2020-07-22 11:34:32 +02:00
|
|
|
/// Current playlist.
|
2020-08-06 09:26:49 +02:00
|
|
|
Playlist _queue;
|
2020-02-25 10:57:12 +01:00
|
|
|
|
2020-07-22 11:34:32 +02:00
|
|
|
/// Notifier for playlist change.
|
|
|
|
bool _queueUpdate = false;
|
2020-02-09 13:29:09 +01:00
|
|
|
|
2020-07-22 11:34:32 +02:00
|
|
|
/// Player state.
|
|
|
|
AudioProcessingState _audioState = AudioProcessingState.none;
|
2020-03-14 04:14:24 +01:00
|
|
|
|
2020-07-22 11:34:32 +02:00
|
|
|
/// Player playing.
|
|
|
|
bool _playing = false;
|
2020-02-25 10:57:12 +01:00
|
|
|
|
2020-07-22 11:34:32 +02:00
|
|
|
/// Fastforward second.
|
2020-07-25 07:42:48 +02:00
|
|
|
int _fastForwardSeconds = 0;
|
2020-04-11 19:23:12 +02:00
|
|
|
|
2020-07-22 11:34:32 +02:00
|
|
|
/// Rewind seconds.
|
2020-07-25 07:42:48 +02:00
|
|
|
int _rewindSeconds = 0;
|
2020-04-22 20:10:57 +02:00
|
|
|
|
2020-07-22 11:34:32 +02:00
|
|
|
/// No slide, set true if slide on seekbar.
|
2020-03-14 04:14:24 +01:00
|
|
|
bool _noSlide = true;
|
2020-07-22 11:34:32 +02:00
|
|
|
|
|
|
|
/// Current episode duration.
|
2020-03-14 04:14:24 +01:00
|
|
|
int _backgroundAudioDuration = 0;
|
2020-07-22 11:34:32 +02:00
|
|
|
|
|
|
|
/// Current episode positin.
|
2020-03-14 04:14:24 +01:00
|
|
|
int _backgroundAudioPosition = 0;
|
2020-07-22 11:34:32 +02:00
|
|
|
|
|
|
|
/// Erroe maeesage.
|
2020-02-25 10:57:12 +01:00
|
|
|
String _remoteErrorMessage;
|
2020-03-14 04:14:24 +01:00
|
|
|
|
2020-07-22 11:34:32 +02:00
|
|
|
/// Seekbar value, min 0, max 1.0.
|
2020-02-25 10:57:12 +01:00
|
|
|
double _seekSliderValue = 0.0;
|
2020-07-22 11:34:32 +02:00
|
|
|
|
|
|
|
/// Record plyaer position.
|
2020-03-14 04:14:24 +01:00
|
|
|
int _lastPostion = 0;
|
2020-07-22 11:34:32 +02:00
|
|
|
|
|
|
|
/// Set true if sleep timer mode is end of episode.
|
2020-03-03 17:04:23 +01:00
|
|
|
bool _stopOnComplete = false;
|
2020-07-22 11:34:32 +02:00
|
|
|
|
|
|
|
/// Sleep timer timer.
|
2020-03-03 17:04:23 +01:00
|
|
|
Timer _stopTimer;
|
2020-07-22 11:34:32 +02:00
|
|
|
|
|
|
|
/// Sleep timer time left.
|
2020-03-19 20:58:30 +01:00
|
|
|
int _timeLeft = 0;
|
2020-07-22 11:34:32 +02:00
|
|
|
|
|
|
|
/// Start sleep timer.
|
2020-04-02 11:52:26 +02:00
|
|
|
bool _startSleepTimer = false;
|
2020-07-22 11:34:32 +02:00
|
|
|
|
|
|
|
/// Control sleep timer anamation.
|
2020-03-19 20:58:30 +01:00
|
|
|
double _switchValue = 0;
|
2020-07-22 11:34:32 +02:00
|
|
|
|
|
|
|
/// Sleep timer mode.
|
2020-04-11 19:23:12 +02:00
|
|
|
SleepTimerMode _sleepTimerMode = SleepTimerMode.undefined;
|
2020-07-22 11:34:32 +02:00
|
|
|
|
2020-06-27 20:27:39 +02:00
|
|
|
//Auto stop at the end of episode when you start play at scheduled time.
|
|
|
|
bool _autoSleepTimer;
|
2020-07-22 11:34:32 +02:00
|
|
|
|
2020-04-22 20:10:57 +02:00
|
|
|
//set autoplay episode in playlist
|
2020-06-27 20:27:39 +02:00
|
|
|
bool _autoPlay;
|
2020-07-22 11:34:32 +02:00
|
|
|
|
|
|
|
/// Datetime now.
|
2020-03-14 04:14:24 +01:00
|
|
|
DateTime _current;
|
2020-07-22 11:34:32 +02:00
|
|
|
|
|
|
|
/// Current position.
|
2020-03-14 04:14:24 +01:00
|
|
|
int _currentPosition;
|
2020-07-22 11:34:32 +02:00
|
|
|
|
|
|
|
/// Current speed.
|
2020-04-18 06:48:02 +02:00
|
|
|
double _currentSpeed = 1;
|
2020-07-22 11:34:32 +02:00
|
|
|
|
2020-08-01 09:31:18 +02:00
|
|
|
///Update episode card when setting changed
|
2020-06-10 09:42:40 +02:00
|
|
|
bool _episodeState = false;
|
2020-03-14 04:14:24 +01:00
|
|
|
|
2020-07-30 19:18:56 +02:00
|
|
|
/// Player height.
|
|
|
|
PlayerHeight _playerHeight;
|
|
|
|
|
2020-08-01 09:31:18 +02:00
|
|
|
/// Player skip silence.
|
|
|
|
bool _skipSilence;
|
|
|
|
|
2020-08-09 17:10:32 +02:00
|
|
|
/// Boost volumn
|
|
|
|
bool _boostVolume;
|
|
|
|
|
|
|
|
/// Boost volume gain.
|
|
|
|
int _volumeGain;
|
|
|
|
|
2020-08-01 09:31:18 +02:00
|
|
|
// ignore: prefer_final_fields
|
|
|
|
bool _playerRunning = false;
|
|
|
|
|
2020-07-22 11:34:32 +02:00
|
|
|
AudioProcessingState get audioState => _audioState;
|
2020-03-14 04:14:24 +01:00
|
|
|
int get backgroundAudioDuration => _backgroundAudioDuration;
|
|
|
|
int get backgroundAudioPosition => _backgroundAudioPosition;
|
2020-02-25 10:57:12 +01:00
|
|
|
double get seekSliderValue => _seekSliderValue;
|
|
|
|
String get remoteErrorMessage => _remoteErrorMessage;
|
2020-08-01 09:31:18 +02:00
|
|
|
bool get playerRunning => _playerRunning;
|
2020-07-22 11:34:32 +02:00
|
|
|
bool get buffering => _audioState != AudioProcessingState.ready;
|
2020-03-01 13:17:06 +01:00
|
|
|
int get lastPositin => _lastPostion;
|
2020-02-25 10:57:12 +01:00
|
|
|
Playlist get queue => _queue;
|
2020-07-22 11:34:32 +02:00
|
|
|
bool get playing => _playing;
|
2020-03-31 18:36:20 +02:00
|
|
|
bool get queueUpdate => _queueUpdate;
|
2020-02-20 16:44:42 +01:00
|
|
|
EpisodeBrief get episode => _episode;
|
2020-03-03 17:04:23 +01:00
|
|
|
bool get stopOnComplete => _stopOnComplete;
|
2020-04-02 11:52:26 +02:00
|
|
|
bool get startSleepTimer => _startSleepTimer;
|
2020-04-11 19:23:12 +02:00
|
|
|
SleepTimerMode get sleepTimerMode => _sleepTimerMode;
|
2020-03-19 20:58:30 +01:00
|
|
|
int get timeLeft => _timeLeft;
|
|
|
|
double get switchValue => _switchValue;
|
2020-04-18 06:48:02 +02:00
|
|
|
double get currentSpeed => _currentSpeed;
|
2020-06-10 09:42:40 +02:00
|
|
|
bool get episodeState => _episodeState;
|
2020-06-27 20:27:39 +02:00
|
|
|
bool get autoSleepTimer => _autoSleepTimer;
|
2020-07-25 07:42:48 +02:00
|
|
|
int get fastForwardSeconds => _fastForwardSeconds;
|
|
|
|
int get rewindSeconds => _rewindSeconds;
|
2020-07-30 19:18:56 +02:00
|
|
|
PlayerHeight get playerHeight => _playerHeight;
|
2020-08-01 09:31:18 +02:00
|
|
|
bool get skipSilence => _skipSilence;
|
2020-08-09 17:10:32 +02:00
|
|
|
bool get boostVolume => _boostVolume;
|
|
|
|
int get volumeGain => _volumeGain;
|
2020-03-03 17:04:23 +01:00
|
|
|
|
2020-03-19 20:58:30 +01:00
|
|
|
set setSwitchValue(double value) {
|
|
|
|
_switchValue = value;
|
|
|
|
notifyListeners();
|
2020-03-03 17:04:23 +01:00
|
|
|
}
|
2020-03-01 13:17:06 +01:00
|
|
|
|
2020-06-10 09:42:40 +02:00
|
|
|
set setEpisodeState(bool boo) {
|
|
|
|
_episodeState = !_episodeState;
|
|
|
|
notifyListeners();
|
|
|
|
}
|
|
|
|
|
2020-07-30 19:18:56 +02:00
|
|
|
set setPlayerHeight(PlayerHeight mode) {
|
|
|
|
_playerHeight = mode;
|
|
|
|
notifyListeners();
|
|
|
|
_savePlayerHeight();
|
|
|
|
}
|
|
|
|
|
2020-08-01 09:31:18 +02:00
|
|
|
Future _initAudioData() async {
|
2020-07-30 19:25:00 +02:00
|
|
|
var index = await playerHeightStorage.getInt(defaultValue: 0);
|
2020-07-30 19:18:56 +02:00
|
|
|
_playerHeight = PlayerHeight.values[index];
|
2020-08-01 09:31:18 +02:00
|
|
|
_currentSpeed = await speedStorage.getDoubel(defaultValue: 1.0);
|
|
|
|
_skipSilence = await skipSilenceStorage.getBool(defaultValue: false);
|
2020-08-09 17:10:32 +02:00
|
|
|
_boostVolume = await boostVolumeStorage.getBool(defaultValue: false);
|
|
|
|
_volumeGain = await volumeGainStorage.getInt(defaultValue: 3000);
|
2020-07-30 19:18:56 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
Future _savePlayerHeight() async {
|
|
|
|
await playerHeightStorage.saveInt(_playerHeight.index);
|
|
|
|
}
|
|
|
|
|
2020-04-22 20:10:57 +02:00
|
|
|
Future _getAutoPlay() async {
|
2020-07-26 12:20:42 +02:00
|
|
|
var i = await autoPlayStorage.getInt();
|
2020-06-27 20:27:39 +02:00
|
|
|
_autoPlay = i == 0;
|
2020-04-22 20:10:57 +02:00
|
|
|
}
|
|
|
|
|
2020-06-27 20:27:39 +02:00
|
|
|
Future _getAutoSleepTimer() async {
|
2020-07-26 12:20:42 +02:00
|
|
|
var i = await autoSleepTimerStorage.getInt();
|
2020-06-27 20:27:39 +02:00
|
|
|
_autoSleepTimer = i == 1;
|
2020-04-22 20:10:57 +02:00
|
|
|
}
|
|
|
|
|
2020-04-11 19:23:12 +02:00
|
|
|
set setSleepTimerMode(SleepTimerMode timer) {
|
|
|
|
_sleepTimerMode = timer;
|
|
|
|
notifyListeners();
|
|
|
|
}
|
|
|
|
|
2020-03-14 04:14:24 +01:00
|
|
|
@override
|
2020-07-31 10:50:48 +02:00
|
|
|
void addListener(VoidCallback listener) {
|
2020-03-14 04:14:24 +01:00
|
|
|
super.addListener(listener);
|
2020-08-01 09:31:18 +02:00
|
|
|
_initAudioData();
|
|
|
|
// _queueUpdate = false;
|
|
|
|
// _getAutoSleepTimer();
|
2020-07-31 10:50:48 +02:00
|
|
|
AudioService.connect();
|
2020-07-26 12:20:42 +02:00
|
|
|
var running = AudioService.running;
|
2020-04-02 11:52:26 +02:00
|
|
|
if (running) {}
|
2020-03-14 04:14:24 +01:00
|
|
|
}
|
|
|
|
|
2020-07-22 11:34:32 +02:00
|
|
|
Future<void> loadPlaylist() async {
|
2020-08-06 09:26:49 +02:00
|
|
|
_queue = Playlist();
|
2020-03-01 13:17:06 +01:00
|
|
|
await _queue.getPlaylist();
|
2020-04-22 20:10:57 +02:00
|
|
|
await _getAutoPlay();
|
|
|
|
_lastPostion = await positionStorage.getInt();
|
2020-03-31 18:36:20 +02:00
|
|
|
if (_lastPostion > 0 && _queue.playlist.length > 0) {
|
2020-07-26 12:20:42 +02:00
|
|
|
final episode = _queue.playlist.first;
|
|
|
|
final duration = episode.duration * 1000;
|
|
|
|
final seekValue = duration != 0 ? _lastPostion / duration : 1;
|
|
|
|
final history = PlayHistory(
|
|
|
|
episode.title, episode.enclosureUrl, _lastPostion ~/ 1000, seekValue);
|
2020-03-31 18:36:20 +02:00
|
|
|
await dbHelper.saveHistory(history);
|
|
|
|
}
|
2020-07-26 12:20:42 +02:00
|
|
|
var lastWorkStorage = KeyValueStorage(lastWorkKey);
|
2020-04-22 20:10:57 +02:00
|
|
|
await lastWorkStorage.saveInt(0);
|
2020-02-25 13:28:48 +01:00
|
|
|
}
|
2020-02-25 10:57:12 +01:00
|
|
|
|
2020-08-02 17:29:41 +02:00
|
|
|
playlistLoad() async {
|
|
|
|
await _queue.getPlaylist();
|
|
|
|
_backgroundAudioDuration = 0;
|
|
|
|
_backgroundAudioPosition = 0;
|
|
|
|
_seekSliderValue = 0;
|
|
|
|
_episode = _queue.playlist.first;
|
|
|
|
_queueUpdate = !_queueUpdate;
|
|
|
|
_audioState = AudioProcessingState.none;
|
|
|
|
_playerRunning = true;
|
|
|
|
notifyListeners();
|
|
|
|
_startAudioService(_lastPostion ?? 0, _queue.playlist.first.enclosureUrl);
|
|
|
|
}
|
|
|
|
|
2020-07-22 11:34:32 +02:00
|
|
|
Future<void> episodeLoad(EpisodeBrief episode,
|
|
|
|
{int startPosition = 0}) async {
|
2020-06-16 06:40:51 +02:00
|
|
|
print(episode.enclosureUrl);
|
2020-07-26 12:20:42 +02:00
|
|
|
final episodeNew = await dbHelper.getRssItemWithUrl(episode.enclosureUrl);
|
2020-04-25 15:50:27 +02:00
|
|
|
//TODO load episode from last position when player running
|
2020-07-22 11:34:32 +02:00
|
|
|
if (playerRunning) {
|
2020-07-26 12:20:42 +02:00
|
|
|
final history = PlayHistory(_episode.title, _episode.enclosureUrl,
|
|
|
|
backgroundAudioPosition ~/ 1000, seekSliderValue);
|
2020-03-01 13:17:06 +01:00
|
|
|
await dbHelper.saveHistory(history);
|
2020-03-31 18:36:20 +02:00
|
|
|
AudioService.addQueueItemAt(episodeNew.toMediaItem(), 0);
|
2020-03-14 04:14:24 +01:00
|
|
|
_queue.playlist
|
|
|
|
.removeWhere((item) => item.enclosureUrl == episode.enclosureUrl);
|
2020-03-31 18:36:20 +02:00
|
|
|
_queue.playlist.insert(0, episodeNew);
|
2020-03-14 04:14:24 +01:00
|
|
|
notifyListeners();
|
|
|
|
await _queue.savePlaylist();
|
|
|
|
} else {
|
|
|
|
await _queue.getPlaylist();
|
2020-04-11 19:23:12 +02:00
|
|
|
await _queue.delFromPlaylist(episode);
|
|
|
|
await _queue.addToPlayListAt(episodeNew, 0);
|
2020-03-14 04:14:24 +01:00
|
|
|
_backgroundAudioDuration = 0;
|
|
|
|
_backgroundAudioPosition = 0;
|
|
|
|
_seekSliderValue = 0;
|
2020-03-31 18:36:20 +02:00
|
|
|
_episode = episodeNew;
|
2020-08-01 09:31:18 +02:00
|
|
|
_playerRunning = true;
|
2020-03-14 04:14:24 +01:00
|
|
|
notifyListeners();
|
2020-04-11 19:23:12 +02:00
|
|
|
//await _queue.savePlaylist();
|
2020-04-25 15:50:27 +02:00
|
|
|
_startAudioService(startPosition, episodeNew.enclosureUrl);
|
2020-06-16 06:40:51 +02:00
|
|
|
if (episodeNew.isNew == 1) {
|
|
|
|
await dbHelper.removeEpisodeNewMark(episodeNew.enclosureUrl);
|
|
|
|
}
|
2020-03-01 13:17:06 +01:00
|
|
|
}
|
2020-03-14 04:14:24 +01:00
|
|
|
}
|
|
|
|
|
2020-04-25 15:50:27 +02:00
|
|
|
_startAudioService(int position, String url) async {
|
2020-04-11 19:23:12 +02:00
|
|
|
_stopOnComplete = false;
|
|
|
|
_sleepTimerMode = SleepTimerMode.undefined;
|
2020-08-01 09:31:18 +02:00
|
|
|
_switchValue = 0;
|
2020-07-22 11:34:32 +02:00
|
|
|
|
|
|
|
/// Connect to audio service.
|
2020-03-14 04:14:24 +01:00
|
|
|
if (!AudioService.connected) {
|
|
|
|
await AudioService.connect();
|
|
|
|
}
|
2020-07-22 11:34:32 +02:00
|
|
|
|
|
|
|
/// Get fastword and rewind seconds.
|
|
|
|
_fastForwardSeconds =
|
|
|
|
await fastForwardSecondsStorage.getInt(defaultValue: 30);
|
|
|
|
_rewindSeconds = await rewindSecondsStorage.getInt(defaultValue: 10);
|
|
|
|
|
|
|
|
/// Start audio service.
|
2020-03-14 04:14:24 +01:00
|
|
|
await AudioService.start(
|
2020-04-06 14:18:08 +02:00
|
|
|
backgroundTaskEntrypoint: _audioPlayerTaskEntrypoint,
|
|
|
|
androidNotificationChannelName: 'Tsacdop',
|
2020-07-22 11:34:32 +02:00
|
|
|
androidNotificationColor: 0xFF4d91be,
|
2020-04-06 14:18:08 +02:00
|
|
|
androidNotificationIcon: 'drawable/ic_notification',
|
2020-07-22 11:34:32 +02:00
|
|
|
androidEnableQueue: true,
|
|
|
|
androidStopForegroundOnPause: true,
|
|
|
|
fastForwardInterval: Duration(seconds: _fastForwardSeconds),
|
|
|
|
rewindInterval: Duration(seconds: _rewindSeconds));
|
|
|
|
|
|
|
|
//Check autoplay setting, if true only add one episode, else add playlist.
|
2020-06-27 20:27:39 +02:00
|
|
|
await _getAutoPlay();
|
2020-03-19 20:58:30 +01:00
|
|
|
if (_autoPlay) {
|
2020-07-26 12:20:42 +02:00
|
|
|
for (var episode in _queue.playlist) {
|
2020-03-14 04:14:24 +01:00
|
|
|
await AudioService.addQueueItem(episode.toMediaItem());
|
2020-07-26 12:20:42 +02:00
|
|
|
}
|
2020-03-14 04:14:24 +01:00
|
|
|
} else {
|
|
|
|
await AudioService.addQueueItem(_queue.playlist.first.toMediaItem());
|
|
|
|
}
|
2020-06-28 17:47:29 +02:00
|
|
|
//Check auto sleep timer setting
|
2020-06-27 20:27:39 +02:00
|
|
|
await _getAutoSleepTimer();
|
|
|
|
if (_autoSleepTimer) {
|
2020-07-26 12:20:42 +02:00
|
|
|
var startTime =
|
2020-06-27 20:27:39 +02:00
|
|
|
await autoSleepTimerStartStorage.getInt(defaultValue: 1380);
|
2020-07-26 12:20:42 +02:00
|
|
|
var endTime = await autoSleepTimerEndStorage.getInt(defaultValue: 360);
|
|
|
|
var currentTime = DateTime.now().hour * 60 + DateTime.now().minute;
|
2020-06-27 20:27:39 +02:00
|
|
|
if ((startTime > endTime &&
|
2020-06-29 14:13:42 +02:00
|
|
|
(currentTime > startTime || currentTime < endTime)) ||
|
2020-06-27 20:27:39 +02:00
|
|
|
((startTime < endTime) &&
|
|
|
|
(currentTime > startTime && currentTime < endTime))) {
|
2020-07-26 12:20:42 +02:00
|
|
|
var mode = await autoSleepTimerModeStorage.getInt();
|
2020-06-27 20:27:39 +02:00
|
|
|
_sleepTimerMode = SleepTimerMode.values[mode];
|
2020-07-26 12:20:42 +02:00
|
|
|
var defaultTimer =
|
2020-06-27 20:27:39 +02:00
|
|
|
await defaultSleepTimerStorage.getInt(defaultValue: 30);
|
|
|
|
sleepTimer(defaultTimer);
|
|
|
|
}
|
|
|
|
}
|
2020-08-09 17:10:32 +02:00
|
|
|
|
|
|
|
/// Set player speed.
|
2020-08-01 09:31:18 +02:00
|
|
|
if (_currentSpeed != 1.0) {
|
|
|
|
await AudioService.customAction('setSpeed', _currentSpeed);
|
|
|
|
}
|
2020-08-09 17:10:32 +02:00
|
|
|
|
|
|
|
/// Set slipsilence.
|
2020-08-01 09:31:18 +02:00
|
|
|
if (_skipSilence) {
|
|
|
|
await AudioService.customAction('setSkipSilence', skipSilence);
|
|
|
|
}
|
2020-08-09 17:10:32 +02:00
|
|
|
|
|
|
|
/// Set boostValome.
|
|
|
|
if (_boostVolume) {
|
|
|
|
await AudioService.customAction(
|
|
|
|
'setBoostVolume', [_boostVolume, _volumeGain]);
|
|
|
|
}
|
|
|
|
|
2020-03-14 04:14:24 +01:00
|
|
|
await AudioService.play();
|
2020-04-11 19:23:12 +02:00
|
|
|
|
|
|
|
AudioService.currentMediaItemStream
|
|
|
|
.where((event) => event != null)
|
|
|
|
.listen((item) async {
|
2020-07-26 12:20:42 +02:00
|
|
|
var episode = await dbHelper.getRssItemWithMediaId(item.id);
|
2020-07-22 11:34:32 +02:00
|
|
|
|
|
|
|
_backgroundAudioDuration = item.duration?.inMilliseconds ?? 0;
|
2020-05-09 17:42:13 +02:00
|
|
|
if (episode != null) {
|
|
|
|
_episode = episode;
|
2020-07-22 11:34:32 +02:00
|
|
|
_backgroundAudioDuration = item.duration.inMilliseconds ?? 0;
|
2020-05-09 17:42:13 +02:00
|
|
|
if (position > 0 &&
|
|
|
|
_backgroundAudioDuration > 0 &&
|
|
|
|
_episode.enclosureUrl == url) {
|
2020-07-22 11:34:32 +02:00
|
|
|
AudioService.seekTo(Duration(milliseconds: position));
|
2020-05-09 17:42:13 +02:00
|
|
|
position = 0;
|
|
|
|
}
|
|
|
|
notifyListeners();
|
|
|
|
} else {
|
2020-07-22 11:34:32 +02:00
|
|
|
// _queue.playlist.removeAt(0);
|
2020-05-09 17:42:13 +02:00
|
|
|
AudioService.skipToNext();
|
2020-03-14 04:14:24 +01:00
|
|
|
}
|
|
|
|
});
|
2020-07-22 11:34:32 +02:00
|
|
|
AudioService.playbackStateStream
|
|
|
|
.distinct()
|
|
|
|
.where((event) => event != null)
|
|
|
|
.listen((event) async {
|
2020-03-14 04:14:24 +01:00
|
|
|
_current = DateTime.now();
|
2020-07-25 12:32:05 +02:00
|
|
|
_audioState = event.processingState;
|
2020-07-22 11:34:32 +02:00
|
|
|
_playing = event?.playing;
|
|
|
|
_currentSpeed = event.speed;
|
|
|
|
_currentPosition = event.currentPosition.inMilliseconds ?? 0;
|
|
|
|
|
|
|
|
if (_audioState == AudioProcessingState.stopped) {
|
2020-04-18 21:46:10 +02:00
|
|
|
if (_switchValue > 0) _switchValue = 0;
|
|
|
|
}
|
2020-03-31 18:36:20 +02:00
|
|
|
|
2020-07-22 11:34:32 +02:00
|
|
|
/// Get error state.
|
|
|
|
if (_audioState == AudioProcessingState.error) {
|
2020-03-31 18:36:20 +02:00
|
|
|
_remoteErrorMessage = 'Network Error';
|
|
|
|
}
|
2020-07-22 11:34:32 +02:00
|
|
|
|
|
|
|
/// Reset error state.
|
|
|
|
if (_audioState != AudioProcessingState.error && _playing) {
|
2020-03-31 18:36:20 +02:00
|
|
|
_remoteErrorMessage = null;
|
2020-03-14 04:14:24 +01:00
|
|
|
}
|
|
|
|
notifyListeners();
|
|
|
|
});
|
2020-03-31 18:36:20 +02:00
|
|
|
|
2020-08-01 09:31:18 +02:00
|
|
|
AudioService.customEventStream.distinct().listen((event) async {
|
|
|
|
if (event is String && _episode.title == event) {
|
|
|
|
print(event);
|
|
|
|
_queue.delFromPlaylist(_episode);
|
|
|
|
_lastPostion = 0;
|
|
|
|
notifyListeners();
|
|
|
|
await positionStorage.saveInt(_lastPostion);
|
|
|
|
final history = PlayHistory(_episode.title, _episode.enclosureUrl,
|
|
|
|
backgroundAudioPosition ~/ 1000, seekSliderValue);
|
2020-08-02 17:29:41 +02:00
|
|
|
await dbHelper.saveHistory(history);
|
2020-08-01 09:31:18 +02:00
|
|
|
}
|
|
|
|
if (event is Map && event['playerRunning'] == false) {
|
|
|
|
_playerRunning = false;
|
|
|
|
notifyListeners();
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
2020-04-24 06:19:56 +02:00
|
|
|
//double s = _currentSpeed ?? 1.0;
|
2020-07-26 12:20:42 +02:00
|
|
|
var getPosition = 0;
|
2020-04-26 07:41:25 +02:00
|
|
|
Timer.periodic(Duration(milliseconds: 500), (timer) {
|
2020-07-26 12:20:42 +02:00
|
|
|
var s = _currentSpeed ?? 1.0;
|
2020-03-14 04:14:24 +01:00
|
|
|
if (_noSlide) {
|
2020-07-25 07:42:48 +02:00
|
|
|
if (_playing && !buffering) {
|
2020-04-18 06:48:02 +02:00
|
|
|
getPosition = _currentPosition +
|
2020-04-24 06:19:56 +02:00
|
|
|
((DateTime.now().difference(_current).inMilliseconds) * s)
|
|
|
|
.toInt();
|
2020-07-25 07:42:48 +02:00
|
|
|
_backgroundAudioPosition =
|
|
|
|
math.min(getPosition, _backgroundAudioDuration);
|
2020-07-26 12:20:42 +02:00
|
|
|
} else {
|
2020-04-24 06:19:56 +02:00
|
|
|
_backgroundAudioPosition = _currentPosition ?? 0;
|
2020-07-26 12:20:42 +02:00
|
|
|
}
|
2020-03-14 04:14:24 +01:00
|
|
|
|
|
|
|
if (_backgroundAudioDuration != null &&
|
|
|
|
_backgroundAudioDuration != 0 &&
|
|
|
|
_backgroundAudioPosition != null) {
|
|
|
|
_seekSliderValue =
|
|
|
|
_backgroundAudioPosition / _backgroundAudioDuration ?? 0;
|
2020-07-26 12:20:42 +02:00
|
|
|
} else {
|
2020-03-31 18:36:20 +02:00
|
|
|
_seekSliderValue = 0;
|
2020-07-26 12:20:42 +02:00
|
|
|
}
|
2020-03-31 18:36:20 +02:00
|
|
|
|
2020-05-06 14:08:41 +02:00
|
|
|
if (_backgroundAudioPosition > 0 &&
|
|
|
|
_backgroundAudioPosition < _backgroundAudioDuration) {
|
2020-03-14 04:14:24 +01:00
|
|
|
_lastPostion = _backgroundAudioPosition;
|
2020-04-22 20:10:57 +02:00
|
|
|
positionStorage.saveInt(_lastPostion);
|
2020-03-14 04:14:24 +01:00
|
|
|
}
|
|
|
|
notifyListeners();
|
|
|
|
}
|
2020-07-22 11:34:32 +02:00
|
|
|
if (_audioState == AudioProcessingState.stopped) {
|
2020-03-14 04:14:24 +01:00
|
|
|
timer.cancel();
|
|
|
|
}
|
|
|
|
});
|
2020-03-01 13:17:06 +01:00
|
|
|
}
|
|
|
|
|
2020-02-25 10:57:12 +01:00
|
|
|
playNext() async {
|
2020-06-14 10:03:03 +02:00
|
|
|
await AudioService.skipToNext();
|
2020-08-09 17:10:32 +02:00
|
|
|
_queueUpdate = !_queueUpdate;
|
|
|
|
notifyListeners();
|
2020-02-25 10:57:12 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
addToPlaylist(EpisodeBrief episode) async {
|
2020-07-25 12:32:05 +02:00
|
|
|
var episodeNew = await dbHelper.getRssItemWithUrl(episode.enclosureUrl);
|
|
|
|
if (!_queue.playlist.contains(episodeNew)) {
|
2020-07-22 11:34:32 +02:00
|
|
|
if (playerRunning) {
|
2020-07-25 12:32:05 +02:00
|
|
|
await AudioService.addQueueItem(episodeNew.toMediaItem());
|
2020-04-22 20:10:57 +02:00
|
|
|
}
|
2020-07-25 12:32:05 +02:00
|
|
|
await _queue.addToPlayList(episodeNew);
|
2020-08-03 15:15:37 +02:00
|
|
|
_queueUpdate = !_queueUpdate;
|
2020-04-22 20:10:57 +02:00
|
|
|
notifyListeners();
|
2020-03-14 04:14:24 +01:00
|
|
|
}
|
2020-02-25 10:57:12 +01:00
|
|
|
}
|
|
|
|
|
2020-03-14 04:14:24 +01:00
|
|
|
addToPlaylistAt(EpisodeBrief episode, int index) async {
|
2020-07-25 12:32:05 +02:00
|
|
|
var episodeNew = await dbHelper.getRssItemWithUrl(episode.enclosureUrl);
|
2020-07-22 11:34:32 +02:00
|
|
|
if (playerRunning) {
|
2020-07-25 12:32:05 +02:00
|
|
|
await AudioService.addQueueItemAt(episodeNew.toMediaItem(), index);
|
2020-03-14 04:14:24 +01:00
|
|
|
}
|
2020-07-25 12:32:05 +02:00
|
|
|
await _queue.addToPlayListAt(episodeNew, index);
|
2020-03-31 18:36:20 +02:00
|
|
|
_queueUpdate = !_queueUpdate;
|
2020-03-14 04:14:24 +01:00
|
|
|
notifyListeners();
|
|
|
|
}
|
|
|
|
|
2020-04-22 20:10:57 +02:00
|
|
|
addNewEpisode(List<String> group) async {
|
2020-07-26 12:20:42 +02:00
|
|
|
var newEpisodes = <EpisodeBrief>[];
|
|
|
|
if (group.first == 'All') {
|
2020-04-22 20:10:57 +02:00
|
|
|
newEpisodes = await dbHelper.getRecentNewRssItem();
|
2020-07-26 12:20:42 +02:00
|
|
|
} else {
|
2020-04-22 20:10:57 +02:00
|
|
|
newEpisodes = await dbHelper.getGroupNewRssItem(group);
|
2020-07-26 12:20:42 +02:00
|
|
|
}
|
|
|
|
if (newEpisodes.length > 0 && newEpisodes.length < 100) {
|
|
|
|
for (var episode in newEpisodes) {
|
|
|
|
await addToPlaylist(episode);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (group.first == 'All') {
|
2020-04-22 20:10:57 +02:00
|
|
|
await dbHelper.removeAllNewMark();
|
2020-07-26 12:20:42 +02:00
|
|
|
} else {
|
2020-04-22 20:10:57 +02:00
|
|
|
await dbHelper.removeGroupNewMark(group);
|
2020-07-26 12:20:42 +02:00
|
|
|
}
|
2020-04-22 20:10:57 +02:00
|
|
|
}
|
|
|
|
|
2020-03-14 04:14:24 +01:00
|
|
|
updateMediaItem(EpisodeBrief episode) async {
|
2020-07-26 12:20:42 +02:00
|
|
|
var index = _queue.playlist
|
2020-03-14 04:14:24 +01:00
|
|
|
.indexWhere((item) => item.enclosureUrl == episode.enclosureUrl);
|
|
|
|
if (index > 0) {
|
2020-07-26 12:20:42 +02:00
|
|
|
var episodeNew = await dbHelper.getRssItemWithUrl(episode.enclosureUrl);
|
2020-03-14 04:14:24 +01:00
|
|
|
await delFromPlaylist(episode);
|
2020-03-31 18:36:20 +02:00
|
|
|
await addToPlaylistAt(episodeNew, index);
|
2020-03-14 04:14:24 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-03-31 18:36:20 +02:00
|
|
|
Future<int> delFromPlaylist(EpisodeBrief episode) async {
|
2020-07-25 12:32:05 +02:00
|
|
|
var episodeNew = await dbHelper.getRssItemWithUrl(episode.enclosureUrl);
|
2020-07-22 11:34:32 +02:00
|
|
|
if (playerRunning) {
|
2020-07-25 12:32:05 +02:00
|
|
|
await AudioService.removeQueueItem(episodeNew.toMediaItem());
|
2020-03-14 04:14:24 +01:00
|
|
|
}
|
2020-07-26 12:20:42 +02:00
|
|
|
var index = await _queue.delFromPlaylist(episodeNew);
|
2020-04-06 14:18:08 +02:00
|
|
|
_queueUpdate = !_queueUpdate;
|
2020-03-01 13:17:06 +01:00
|
|
|
notifyListeners();
|
2020-03-31 18:36:20 +02:00
|
|
|
return index;
|
2020-03-01 13:17:06 +01:00
|
|
|
}
|
|
|
|
|
2020-08-09 17:10:32 +02:00
|
|
|
Future reorderPlaylist(int oldIndex, int newIndex) async {
|
|
|
|
var episode = _queue.playlist[oldIndex];
|
|
|
|
if (playerRunning) {
|
|
|
|
await AudioService.removeQueueItem(episode.toMediaItem());
|
|
|
|
await AudioService.addQueueItemAt(episode.toMediaItem(), newIndex);
|
|
|
|
}
|
|
|
|
await _queue.addToPlayListAt(episode, newIndex);
|
|
|
|
if (newIndex == 0) {
|
|
|
|
_lastPostion = 0;
|
|
|
|
await positionStorage.saveInt(0);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-08-06 09:26:49 +02:00
|
|
|
Future<bool> moveToTop(EpisodeBrief episode) async {
|
2020-03-19 20:58:30 +01:00
|
|
|
await delFromPlaylist(episode);
|
2020-07-22 11:34:32 +02:00
|
|
|
if (playerRunning) {
|
2020-08-06 09:26:49 +02:00
|
|
|
await AudioService.addQueueItemAt(episode.toMediaItem(), 1);
|
|
|
|
await _queue.addToPlayListAt(episode, 1, existed: false);
|
2020-03-19 20:58:30 +01:00
|
|
|
} else {
|
2020-08-06 09:26:49 +02:00
|
|
|
await _queue.addToPlayListAt(episode, 0, existed: false);
|
2020-03-19 20:58:30 +01:00
|
|
|
_lastPostion = 0;
|
2020-04-22 20:10:57 +02:00
|
|
|
positionStorage.saveInt(_lastPostion);
|
2020-03-19 20:58:30 +01:00
|
|
|
}
|
2020-08-06 09:26:49 +02:00
|
|
|
_queueUpdate = !_queueUpdate;
|
2020-03-31 18:36:20 +02:00
|
|
|
notifyListeners();
|
2020-08-06 09:26:49 +02:00
|
|
|
return true;
|
2020-03-19 20:58:30 +01:00
|
|
|
}
|
|
|
|
|
2020-08-09 17:10:32 +02:00
|
|
|
Future<void> pauseAduio() async {
|
|
|
|
await AudioService.pause();
|
2020-02-25 10:57:12 +01:00
|
|
|
}
|
|
|
|
|
2020-08-09 17:10:32 +02:00
|
|
|
Future<void> resumeAudio() async {
|
2020-07-22 11:34:32 +02:00
|
|
|
if (_audioState != AudioProcessingState.connecting &&
|
|
|
|
_audioState != AudioProcessingState.none) AudioService.play();
|
2020-02-25 10:57:12 +01:00
|
|
|
}
|
|
|
|
|
2020-03-14 04:14:24 +01:00
|
|
|
forwardAudio(int s) {
|
2020-07-26 12:20:42 +02:00
|
|
|
var pos = _backgroundAudioPosition + s * 1000;
|
2020-07-22 11:34:32 +02:00
|
|
|
AudioService.seekTo(Duration(milliseconds: pos));
|
2020-02-25 10:57:12 +01:00
|
|
|
}
|
2020-04-02 11:52:26 +02:00
|
|
|
|
2020-07-24 19:17:47 +02:00
|
|
|
fastForward() async {
|
|
|
|
await AudioService.fastForward();
|
|
|
|
}
|
|
|
|
|
|
|
|
rewind() async {
|
|
|
|
await AudioService.rewind();
|
|
|
|
}
|
|
|
|
|
2020-04-02 11:52:26 +02:00
|
|
|
seekTo(int position) async {
|
2020-07-22 11:34:32 +02:00
|
|
|
if (_audioState != AudioProcessingState.connecting &&
|
2020-07-26 12:20:42 +02:00
|
|
|
_audioState != AudioProcessingState.none) {
|
2020-07-22 11:34:32 +02:00
|
|
|
await AudioService.seekTo(Duration(milliseconds: position));
|
2020-07-26 12:20:42 +02:00
|
|
|
}
|
2020-03-31 18:36:20 +02:00
|
|
|
}
|
2020-02-25 10:57:12 +01:00
|
|
|
|
2020-03-14 04:14:24 +01:00
|
|
|
sliderSeek(double val) async {
|
2020-07-22 11:34:32 +02:00
|
|
|
if (_audioState != AudioProcessingState.connecting &&
|
|
|
|
_audioState != AudioProcessingState.none) {
|
2020-03-31 18:36:20 +02:00
|
|
|
_noSlide = false;
|
|
|
|
_seekSliderValue = val;
|
|
|
|
notifyListeners();
|
|
|
|
_currentPosition = (val * _backgroundAudioDuration).toInt();
|
2020-07-22 11:34:32 +02:00
|
|
|
await AudioService.seekTo(Duration(milliseconds: _currentPosition));
|
2020-03-31 18:36:20 +02:00
|
|
|
_noSlide = true;
|
|
|
|
}
|
2020-02-09 13:29:09 +01:00
|
|
|
}
|
2020-03-14 04:14:24 +01:00
|
|
|
|
2020-08-01 09:31:18 +02:00
|
|
|
/// Set player speed.
|
2020-04-18 06:48:02 +02:00
|
|
|
setSpeed(double speed) async {
|
|
|
|
await AudioService.customAction('setSpeed', speed);
|
|
|
|
_currentSpeed = speed;
|
2020-08-01 09:31:18 +02:00
|
|
|
await speedStorage.saveDouble(_currentSpeed);
|
|
|
|
notifyListeners();
|
|
|
|
}
|
|
|
|
|
|
|
|
setSkipSilence({@required bool skipSilence}) async {
|
|
|
|
await AudioService.customAction('setSkipSilence', skipSilence);
|
|
|
|
_skipSilence = skipSilence;
|
|
|
|
await skipSilenceStorage.saveBool(_skipSilence);
|
2020-04-18 06:48:02 +02:00
|
|
|
notifyListeners();
|
|
|
|
}
|
|
|
|
|
2020-08-09 17:10:32 +02:00
|
|
|
setBoostVolume({@required bool boostVolume, int gain}) async {
|
|
|
|
await AudioService.customAction(
|
|
|
|
'setBoostVolume', [boostVolume, _volumeGain]);
|
|
|
|
_boostVolume = boostVolume;
|
|
|
|
notifyListeners();
|
|
|
|
await boostVolumeStorage.saveBool(boostVolume);
|
|
|
|
}
|
|
|
|
|
2020-04-02 11:52:26 +02:00
|
|
|
//Set sleep timer
|
2020-03-03 17:04:23 +01:00
|
|
|
sleepTimer(int mins) {
|
2020-04-11 19:23:12 +02:00
|
|
|
if (_sleepTimerMode == SleepTimerMode.timer) {
|
|
|
|
_startSleepTimer = true;
|
|
|
|
_switchValue = 1;
|
|
|
|
notifyListeners();
|
|
|
|
_timeLeft = mins * 60;
|
|
|
|
Timer.periodic(Duration(seconds: 1), (timer) {
|
|
|
|
if (_timeLeft == 0) {
|
|
|
|
timer.cancel();
|
|
|
|
notifyListeners();
|
|
|
|
} else {
|
|
|
|
_timeLeft = _timeLeft - 1;
|
|
|
|
notifyListeners();
|
|
|
|
}
|
|
|
|
});
|
|
|
|
_stopTimer = Timer(Duration(minutes: mins), () {
|
|
|
|
_stopOnComplete = false;
|
|
|
|
_startSleepTimer = false;
|
|
|
|
_switchValue = 0;
|
|
|
|
AudioService.stop();
|
2020-07-22 11:34:32 +02:00
|
|
|
notifyListeners();
|
2020-07-25 14:28:24 +02:00
|
|
|
// AudioService.disconnect();
|
2020-04-11 19:23:12 +02:00
|
|
|
});
|
|
|
|
} else if (_sleepTimerMode == SleepTimerMode.endOfEpisode) {
|
|
|
|
_stopOnComplete = true;
|
|
|
|
_switchValue = 1;
|
2020-04-06 14:18:08 +02:00
|
|
|
notifyListeners();
|
2020-04-11 19:23:12 +02:00
|
|
|
if (_queue.playlist.length > 1 && _autoPlay) {
|
|
|
|
AudioService.customAction('stopAtEnd');
|
|
|
|
}
|
|
|
|
}
|
2020-03-03 17:04:23 +01:00
|
|
|
}
|
2020-03-14 04:14:24 +01:00
|
|
|
|
2020-03-03 17:04:23 +01:00
|
|
|
//Cancel sleep timer
|
2020-03-14 04:14:24 +01:00
|
|
|
cancelTimer() {
|
2020-04-11 19:23:12 +02:00
|
|
|
if (_sleepTimerMode == SleepTimerMode.timer) {
|
|
|
|
_stopTimer.cancel();
|
|
|
|
_timeLeft = 0;
|
|
|
|
_startSleepTimer = false;
|
|
|
|
_switchValue = 0;
|
|
|
|
notifyListeners();
|
|
|
|
} else if (_sleepTimerMode == SleepTimerMode.endOfEpisode) {
|
|
|
|
AudioService.customAction('cancelStopAtEnd');
|
|
|
|
_switchValue = 0;
|
|
|
|
_stopOnComplete = false;
|
|
|
|
notifyListeners();
|
|
|
|
}
|
2020-03-03 17:04:23 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
@override
|
2020-03-14 04:14:24 +01:00
|
|
|
void dispose() async {
|
2020-07-22 11:34:32 +02:00
|
|
|
// await AudioService.stop();
|
2020-03-14 04:14:24 +01:00
|
|
|
await AudioService.disconnect();
|
2020-04-11 19:23:12 +02:00
|
|
|
//_playerRunning = false;
|
2020-03-01 13:17:06 +01:00
|
|
|
super.dispose();
|
2020-02-25 10:57:12 +01:00
|
|
|
}
|
2020-03-14 04:14:24 +01:00
|
|
|
}
|
2020-02-25 10:57:12 +01:00
|
|
|
|
2020-03-14 04:14:24 +01:00
|
|
|
class AudioPlayerTask extends BackgroundAudioTask {
|
2020-04-27 19:26:33 +02:00
|
|
|
KeyValueStorage cacheStorage = KeyValueStorage(cacheMaxKey);
|
|
|
|
|
2020-07-26 12:20:42 +02:00
|
|
|
final List<MediaItem> _queue = [];
|
|
|
|
final AudioPlayer _audioPlayer = AudioPlayer();
|
2020-07-22 11:34:32 +02:00
|
|
|
AudioProcessingState _skipState;
|
2020-08-06 09:26:49 +02:00
|
|
|
bool _playing;
|
2020-07-22 11:34:32 +02:00
|
|
|
bool _interrupted = false;
|
2020-04-11 19:23:12 +02:00
|
|
|
bool _stopAtEnd;
|
2020-07-18 11:52:31 +02:00
|
|
|
int _cacheMax;
|
2020-03-14 04:14:24 +01:00
|
|
|
bool get hasNext => _queue.length > 0;
|
|
|
|
|
2020-04-23 19:46:36 +02:00
|
|
|
MediaItem get mediaItem => _queue.length > 0 ? _queue.first : null;
|
2020-03-14 04:14:24 +01:00
|
|
|
|
2020-07-22 11:34:32 +02:00
|
|
|
StreamSubscription<AudioPlaybackState> _playerStateSubscription;
|
|
|
|
StreamSubscription<AudioPlaybackEvent> _eventSubscription;
|
2020-03-14 04:14:24 +01:00
|
|
|
|
|
|
|
@override
|
2020-07-22 11:34:32 +02:00
|
|
|
Future<void> onStart(Map<String, dynamic> params) async {
|
2020-04-11 19:23:12 +02:00
|
|
|
_stopAtEnd = false;
|
2020-07-22 11:34:32 +02:00
|
|
|
_playerStateSubscription = _audioPlayer.playbackStateStream
|
2020-03-14 04:14:24 +01:00
|
|
|
.where((state) => state == AudioPlaybackState.completed)
|
|
|
|
.listen((state) {
|
|
|
|
_handlePlaybackCompleted();
|
2020-02-25 10:57:12 +01:00
|
|
|
});
|
2020-07-22 11:34:32 +02:00
|
|
|
|
|
|
|
_eventSubscription = _audioPlayer.playbackEventStream.listen((event) {
|
2020-03-31 18:36:20 +02:00
|
|
|
if (event.playbackError != null) {
|
2020-07-22 11:34:32 +02:00
|
|
|
_playing = false;
|
|
|
|
_setState(processingState: _skipState ?? AudioProcessingState.error);
|
2020-03-14 04:14:24 +01:00
|
|
|
}
|
2020-07-22 11:34:32 +02:00
|
|
|
final bufferingState =
|
|
|
|
event.buffering ? AudioProcessingState.buffering : null;
|
|
|
|
switch (event.state) {
|
|
|
|
case AudioPlaybackState.paused:
|
|
|
|
_setState(
|
|
|
|
processingState: bufferingState ?? AudioProcessingState.ready,
|
|
|
|
position: event.position,
|
|
|
|
);
|
|
|
|
break;
|
|
|
|
case AudioPlaybackState.playing:
|
|
|
|
_setState(
|
|
|
|
processingState: bufferingState ?? AudioProcessingState.ready,
|
|
|
|
position: event.position,
|
|
|
|
);
|
|
|
|
break;
|
|
|
|
case AudioPlaybackState.connecting:
|
|
|
|
_setState(
|
|
|
|
processingState: _skipState ?? AudioProcessingState.connecting,
|
|
|
|
position: event.position,
|
|
|
|
);
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
break;
|
2020-03-14 04:14:24 +01:00
|
|
|
}
|
|
|
|
});
|
2020-02-25 10:57:12 +01:00
|
|
|
}
|
|
|
|
|
2020-04-11 19:23:12 +02:00
|
|
|
void _handlePlaybackCompleted() async {
|
2020-03-14 04:14:24 +01:00
|
|
|
if (hasNext) {
|
|
|
|
onSkipToNext();
|
|
|
|
} else {
|
2020-03-19 20:58:30 +01:00
|
|
|
_audioPlayer.stop();
|
|
|
|
_queue.removeAt(0);
|
2020-04-11 19:23:12 +02:00
|
|
|
await AudioServiceBackground.setQueue(_queue);
|
2020-03-14 04:14:24 +01:00
|
|
|
onStop();
|
2020-02-25 10:57:12 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-03-14 04:14:24 +01:00
|
|
|
void playPause() {
|
2020-07-26 12:20:42 +02:00
|
|
|
if (AudioServiceBackground.state.playing) {
|
2020-03-14 04:14:24 +01:00
|
|
|
onPause();
|
2020-07-26 12:20:42 +02:00
|
|
|
} else {
|
2020-03-14 04:14:24 +01:00
|
|
|
onPlay();
|
2020-07-26 12:20:42 +02:00
|
|
|
}
|
2020-02-25 10:57:12 +01:00
|
|
|
}
|
|
|
|
|
2020-03-14 04:14:24 +01:00
|
|
|
@override
|
|
|
|
Future<void> onSkipToNext() async {
|
2020-07-22 11:34:32 +02:00
|
|
|
_skipState = AudioProcessingState.skippingToNext;
|
|
|
|
_playing = false;
|
2020-04-11 19:23:12 +02:00
|
|
|
await _audioPlayer.stop();
|
2020-07-22 11:34:32 +02:00
|
|
|
if (_queue.length > 0) {
|
|
|
|
AudioServiceBackground.sendCustomEvent(_queue.first.title);
|
|
|
|
_queue.removeAt(0);
|
|
|
|
}
|
2020-04-11 19:23:12 +02:00
|
|
|
await AudioServiceBackground.setQueue(_queue);
|
|
|
|
if (_queue.length == 0 || _stopAtEnd) {
|
|
|
|
_skipState = null;
|
2020-03-19 20:58:30 +01:00
|
|
|
onStop();
|
2020-03-14 04:14:24 +01:00
|
|
|
} else {
|
2020-04-23 19:46:36 +02:00
|
|
|
await AudioServiceBackground.setQueue(_queue);
|
|
|
|
await AudioServiceBackground.setMediaItem(mediaItem);
|
2020-07-22 11:34:32 +02:00
|
|
|
await _audioPlayer.setUrl(mediaItem.id, cacheMax: _cacheMax);
|
2020-04-23 19:46:36 +02:00
|
|
|
print(mediaItem.title);
|
2020-07-26 12:20:42 +02:00
|
|
|
var duration = await _audioPlayer.durationFuture;
|
|
|
|
if (duration != null) {
|
2020-04-24 06:19:56 +02:00
|
|
|
await AudioServiceBackground.setMediaItem(
|
2020-07-22 11:34:32 +02:00
|
|
|
mediaItem.copyWith(duration: duration));
|
2020-07-26 12:20:42 +02:00
|
|
|
}
|
2020-03-19 20:58:30 +01:00
|
|
|
_skipState = null;
|
|
|
|
// Resume playback if we were playing
|
2020-04-11 19:23:12 +02:00
|
|
|
// if (_playing) {
|
2020-04-24 06:19:56 +02:00
|
|
|
//onPlay();
|
|
|
|
playFromStart();
|
2020-04-11 19:23:12 +02:00
|
|
|
// } else {
|
|
|
|
// _setState(state: BasicPlaybackState.paused);
|
|
|
|
// }
|
2020-03-03 17:04:23 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-03-14 04:14:24 +01:00
|
|
|
@override
|
|
|
|
void onPlay() async {
|
|
|
|
if (_skipState == null) {
|
|
|
|
if (_playing == null) {
|
|
|
|
_playing = true;
|
2020-07-18 11:52:31 +02:00
|
|
|
_cacheMax = await cacheStorage.getInt(
|
|
|
|
defaultValue: (200 * 1024 * 1024).toInt());
|
|
|
|
if (_cacheMax == 0) {
|
|
|
|
await cacheStorage.saveInt((200 * 1024 * 1024).toInt());
|
|
|
|
_cacheMax = 200 * 1024 * 1024;
|
|
|
|
}
|
2020-07-22 11:34:32 +02:00
|
|
|
await _audioPlayer.setUrl(mediaItem.id, cacheMax: _cacheMax);
|
2020-04-23 19:46:36 +02:00
|
|
|
var duration = await _audioPlayer.durationFuture;
|
2020-07-26 12:20:42 +02:00
|
|
|
if (duration != null) {
|
2020-04-23 19:46:36 +02:00
|
|
|
await AudioServiceBackground.setMediaItem(
|
2020-07-22 11:34:32 +02:00
|
|
|
mediaItem.copyWith(duration: duration));
|
2020-07-26 12:20:42 +02:00
|
|
|
}
|
2020-04-24 06:19:56 +02:00
|
|
|
playFromStart();
|
2020-07-22 11:34:32 +02:00
|
|
|
} else {
|
2020-04-23 19:46:36 +02:00
|
|
|
_playing = true;
|
2020-04-25 15:50:27 +02:00
|
|
|
if (_audioPlayer.playbackEvent.state != AudioPlaybackState.connecting ||
|
2020-07-26 12:20:42 +02:00
|
|
|
_audioPlayer.playbackEvent.state != AudioPlaybackState.none) {
|
2020-04-24 06:19:56 +02:00
|
|
|
_audioPlayer.play();
|
2020-07-26 12:20:42 +02:00
|
|
|
}
|
2020-04-22 20:10:57 +02:00
|
|
|
}
|
2020-04-24 06:19:56 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
playFromStart() async {
|
|
|
|
_playing = true;
|
2020-04-25 15:50:27 +02:00
|
|
|
if (_audioPlayer.playbackEvent.state != AudioPlaybackState.connecting ||
|
2020-07-26 12:20:42 +02:00
|
|
|
_audioPlayer.playbackEvent.state != AudioPlaybackState.none) {
|
2020-04-25 15:50:27 +02:00
|
|
|
try {
|
|
|
|
_audioPlayer.play();
|
|
|
|
} catch (e) {
|
2020-07-22 11:34:32 +02:00
|
|
|
_setState(processingState: AudioProcessingState.error);
|
2020-04-25 15:50:27 +02:00
|
|
|
}
|
2020-07-26 12:20:42 +02:00
|
|
|
}
|
2020-04-24 06:19:56 +02:00
|
|
|
if (mediaItem.extras['skip'] > 0) {
|
|
|
|
_audioPlayer.seek(Duration(seconds: mediaItem.extras['skip']));
|
2020-03-14 04:14:24 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
@override
|
|
|
|
void onPause() {
|
|
|
|
if (_skipState == null) {
|
2020-06-10 09:42:40 +02:00
|
|
|
if (_playing == null) {
|
2020-07-22 11:34:32 +02:00
|
|
|
} else if (_playing) {
|
2020-06-10 09:42:40 +02:00
|
|
|
_playing = false;
|
|
|
|
_audioPlayer.pause();
|
|
|
|
}
|
2020-03-14 04:14:24 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
@override
|
2020-07-22 11:34:32 +02:00
|
|
|
void onSeekTo(Duration position) {
|
2020-04-25 15:50:27 +02:00
|
|
|
if (_audioPlayer.playbackEvent.state != AudioPlaybackState.connecting ||
|
2020-07-26 12:20:42 +02:00
|
|
|
_audioPlayer.playbackEvent.state != AudioPlaybackState.none) {
|
2020-07-22 11:34:32 +02:00
|
|
|
_audioPlayer.seek(position);
|
2020-07-26 12:20:42 +02:00
|
|
|
}
|
2020-03-14 04:14:24 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
@override
|
|
|
|
void onClick(MediaButton button) {
|
2020-07-26 12:20:42 +02:00
|
|
|
if (button == MediaButton.media) {
|
2020-06-14 10:03:03 +02:00
|
|
|
playPause();
|
2020-07-26 12:20:42 +02:00
|
|
|
} else if (button == MediaButton.next) {
|
2020-07-22 11:34:32 +02:00
|
|
|
_seekRelative(fastForwardInterval);
|
2020-07-26 12:20:42 +02:00
|
|
|
} else if (button == MediaButton.previous) _seekRelative(-rewindInterval);
|
2020-07-22 11:34:32 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
Future<void> _seekRelative(Duration offset) async {
|
|
|
|
var newPosition = _audioPlayer.playbackEvent.position + offset;
|
2020-07-25 07:42:48 +02:00
|
|
|
print(newPosition.inSeconds);
|
|
|
|
// if (newPosition < Duration.zero) newPosition = Duration.zero;
|
|
|
|
// if (newPosition > mediaItem.duration) newPosition = mediaItem.duration;
|
|
|
|
|
|
|
|
print(newPosition.inSeconds);
|
|
|
|
onSeekTo(newPosition);
|
2020-03-14 04:14:24 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
@override
|
2020-07-22 11:34:32 +02:00
|
|
|
Future<void> onStop() async {
|
2020-03-14 04:14:24 +01:00
|
|
|
await _audioPlayer.stop();
|
2020-05-06 14:08:41 +02:00
|
|
|
await _audioPlayer.dispose();
|
2020-07-22 11:34:32 +02:00
|
|
|
_playing = false;
|
|
|
|
_playerStateSubscription.cancel();
|
|
|
|
_eventSubscription.cancel();
|
|
|
|
await _setState(processingState: AudioProcessingState.none);
|
2020-08-01 09:31:18 +02:00
|
|
|
AudioServiceBackground.sendCustomEvent({'playerRunning': false});
|
2020-07-22 11:34:32 +02:00
|
|
|
await super.onStop();
|
2020-03-14 04:14:24 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
@override
|
|
|
|
void onAddQueueItem(MediaItem mediaItem) async {
|
|
|
|
_queue.add(mediaItem);
|
2020-04-11 19:23:12 +02:00
|
|
|
await AudioServiceBackground.setQueue(_queue);
|
2020-03-14 04:14:24 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
@override
|
|
|
|
void onRemoveQueueItem(MediaItem mediaItem) async {
|
|
|
|
_queue.removeWhere((item) => item.id == mediaItem.id);
|
|
|
|
await AudioServiceBackground.setQueue(_queue);
|
|
|
|
}
|
|
|
|
|
|
|
|
@override
|
|
|
|
void onAddQueueItemAt(MediaItem mediaItem, int index) async {
|
|
|
|
if (index == 0) {
|
|
|
|
await _audioPlayer.stop();
|
|
|
|
_queue.removeWhere((item) => item.id == mediaItem.id);
|
|
|
|
_queue.insert(0, mediaItem);
|
2020-04-11 19:23:12 +02:00
|
|
|
await AudioServiceBackground.setQueue(_queue);
|
|
|
|
await AudioServiceBackground.setMediaItem(mediaItem);
|
2020-07-22 11:34:32 +02:00
|
|
|
await _audioPlayer.setUrl(mediaItem.id, cacheMax: _cacheMax);
|
2020-07-26 12:20:42 +02:00
|
|
|
var duration = await _audioPlayer.durationFuture ?? Duration.zero;
|
2020-03-14 04:14:24 +01:00
|
|
|
AudioServiceBackground.setMediaItem(
|
2020-07-22 11:34:32 +02:00
|
|
|
mediaItem.copyWith(duration: duration));
|
2020-04-24 06:19:56 +02:00
|
|
|
playFromStart();
|
|
|
|
//onPlay();
|
2020-03-03 17:04:23 +01:00
|
|
|
} else {
|
2020-03-14 04:14:24 +01:00
|
|
|
_queue.insert(index, mediaItem);
|
2020-04-11 19:23:12 +02:00
|
|
|
await AudioServiceBackground.setQueue(_queue);
|
2020-03-03 17:04:23 +01:00
|
|
|
}
|
2020-03-14 04:14:24 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
@override
|
2020-07-25 07:42:48 +02:00
|
|
|
void onFastForward() async {
|
|
|
|
await _seekRelative(fastForwardInterval);
|
2020-03-14 04:14:24 +01:00
|
|
|
}
|
|
|
|
|
2020-06-10 09:42:40 +02:00
|
|
|
@override
|
2020-07-25 07:42:48 +02:00
|
|
|
void onRewind() async {
|
|
|
|
await _seekRelative(-rewindInterval);
|
2020-06-10 09:42:40 +02:00
|
|
|
}
|
|
|
|
|
2020-03-14 04:14:24 +01:00
|
|
|
@override
|
2020-07-22 11:34:32 +02:00
|
|
|
void onAudioFocusLost(AudioInterruption interruption) {
|
|
|
|
if (_playing) _interrupted = true;
|
|
|
|
switch (interruption) {
|
|
|
|
case AudioInterruption.pause:
|
|
|
|
case AudioInterruption.temporaryPause:
|
|
|
|
case AudioInterruption.unknownPause:
|
|
|
|
onPause();
|
|
|
|
break;
|
|
|
|
case AudioInterruption.temporaryDuck:
|
|
|
|
_audioPlayer.setVolume(0.5);
|
|
|
|
break;
|
2020-02-25 10:57:12 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-03-14 04:14:24 +01:00
|
|
|
@override
|
|
|
|
void onAudioBecomingNoisy() {
|
|
|
|
if (_skipState == null) {
|
2020-06-10 09:42:40 +02:00
|
|
|
if (_playing == null) {
|
|
|
|
} else if (_audioPlayer.playbackEvent.state ==
|
|
|
|
AudioPlaybackState.playing) {
|
|
|
|
_playing = false;
|
|
|
|
_audioPlayer.pause();
|
|
|
|
}
|
2020-03-14 04:14:24 +01:00
|
|
|
}
|
2020-02-25 10:57:12 +01:00
|
|
|
}
|
|
|
|
|
2020-03-14 04:14:24 +01:00
|
|
|
@override
|
2020-07-22 11:34:32 +02:00
|
|
|
void onAudioFocusGained(AudioInterruption interruption) {
|
|
|
|
switch (interruption) {
|
|
|
|
case AudioInterruption.temporaryPause:
|
|
|
|
if (!_playing && _interrupted) onPlay();
|
|
|
|
break;
|
|
|
|
case AudioInterruption.temporaryDuck:
|
|
|
|
_audioPlayer.setVolume(1.0);
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
break;
|
2020-03-14 04:14:24 +01:00
|
|
|
}
|
2020-07-22 11:34:32 +02:00
|
|
|
_interrupted = false;
|
2020-02-25 10:57:12 +01:00
|
|
|
}
|
|
|
|
|
2020-03-14 04:14:24 +01:00
|
|
|
@override
|
2020-05-08 13:35:08 +02:00
|
|
|
Future onCustomAction(funtion, argument) async {
|
2020-03-14 04:14:24 +01:00
|
|
|
switch (funtion) {
|
2020-04-11 19:23:12 +02:00
|
|
|
case 'stopAtEnd':
|
|
|
|
_stopAtEnd = true;
|
2020-03-14 04:14:24 +01:00
|
|
|
break;
|
2020-04-11 19:23:12 +02:00
|
|
|
case 'cancelStopAtEnd':
|
|
|
|
_stopAtEnd = false;
|
2020-03-14 04:14:24 +01:00
|
|
|
break;
|
2020-04-18 06:48:02 +02:00
|
|
|
case 'setSpeed':
|
|
|
|
await _audioPlayer.setSpeed(argument);
|
2020-04-25 15:50:27 +02:00
|
|
|
break;
|
2020-08-01 09:31:18 +02:00
|
|
|
case 'setSkipSilence':
|
|
|
|
await _setSkipSilence(argument);
|
|
|
|
break;
|
2020-08-09 17:10:32 +02:00
|
|
|
case 'setBoostVolume':
|
|
|
|
await _setBoostVolume(argument[0], argument[1]);
|
|
|
|
break;
|
2020-03-14 04:14:24 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-08-01 09:31:18 +02:00
|
|
|
Future _setSkipSilence(bool boo) async {
|
|
|
|
await _audioPlayer.setSkipSilence(boo);
|
|
|
|
var duration = await _audioPlayer.durationFuture ?? Duration.zero;
|
|
|
|
AudioServiceBackground.setMediaItem(mediaItem.copyWith(duration: duration));
|
|
|
|
}
|
|
|
|
|
2020-08-09 17:10:32 +02:00
|
|
|
Future _setBoostVolume(bool boo, int gain) async {
|
|
|
|
await _audioPlayer.setBoostVolume(boo, gain: gain);
|
|
|
|
}
|
|
|
|
|
2020-07-22 11:34:32 +02:00
|
|
|
Future<void> _setState({
|
|
|
|
AudioProcessingState processingState,
|
|
|
|
Duration position,
|
|
|
|
Duration bufferedPosition,
|
|
|
|
}) async {
|
2020-03-14 04:14:24 +01:00
|
|
|
if (position == null) {
|
2020-07-22 11:34:32 +02:00
|
|
|
position = _audioPlayer.playbackEvent.position;
|
2020-04-24 06:19:56 +02:00
|
|
|
}
|
2020-07-22 11:34:32 +02:00
|
|
|
await AudioServiceBackground.setState(
|
|
|
|
controls: getControls(),
|
2020-03-14 04:14:24 +01:00
|
|
|
systemActions: [MediaAction.seekTo],
|
2020-07-22 11:34:32 +02:00
|
|
|
processingState:
|
|
|
|
processingState ?? AudioServiceBackground.state.processingState,
|
2020-08-06 09:26:49 +02:00
|
|
|
playing: _playing ?? false,
|
2020-03-14 04:14:24 +01:00
|
|
|
position: position,
|
2020-07-22 11:34:32 +02:00
|
|
|
bufferedPosition: bufferedPosition ?? position,
|
|
|
|
speed: _audioPlayer.speed,
|
2020-03-14 04:14:24 +01:00
|
|
|
);
|
2020-02-25 10:57:12 +01:00
|
|
|
}
|
|
|
|
|
2020-07-22 11:34:32 +02:00
|
|
|
List<MediaControl> getControls() {
|
2020-03-14 04:14:24 +01:00
|
|
|
if (_playing) {
|
2020-07-24 19:17:47 +02:00
|
|
|
return [pauseControl, forward, skipToNextControl, stopControl];
|
2020-03-14 04:14:24 +01:00
|
|
|
} else {
|
2020-07-24 19:17:47 +02:00
|
|
|
return [playControl, forward, skipToNextControl, stopControl];
|
2020-03-14 04:14:24 +01:00
|
|
|
}
|
|
|
|
}
|
2020-02-25 10:57:12 +01:00
|
|
|
}
|