removed many R/a/dio specifc code. Hid some UI that could be used later.
|
|
@ -1,23 +0,0 @@
|
||||||
<!DOCTYPE html>
|
|
||||||
<html lang="fr">
|
|
||||||
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
|
||||||
<!-- seems like the font cannot be changed in KiwiIRC without writing a full theme, so loading the CSS below is useless-->
|
|
||||||
<!--
|
|
||||||
<link rel="stylesheet" href="stylesheet.css" type="text/css" charset="utf-8" /> -->
|
|
||||||
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
<div style="height: auto">
|
|
||||||
<iframe src="https://kiwiirc.com/nextclient/?theme=dark#irc://irc.rizon.net:+6697/#r/a/dio"
|
|
||||||
style="border:0; width:100%; height:100%; position: absolute; top: 0px; left: 0px">
|
|
||||||
</iframe>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</body>
|
|
||||||
|
|
||||||
</html>
|
|
||||||
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 44 KiB |
|
|
@ -71,7 +71,7 @@ abstract class BaseNotification(private val notificationChannelId: String,
|
||||||
builder.setSmallIcon(R.drawable.lollipop_logo)
|
builder.setSmallIcon(R.drawable.lollipop_logo)
|
||||||
builder.color = -0xf58b01 // same color as Accent. Can't use c.getColor since it's API23+
|
builder.color = -0xf58b01 // same color as Accent. Can't use c.getColor since it's API23+
|
||||||
} else {
|
} else {
|
||||||
builder.setSmallIcon(R.drawable.normal_logo)
|
builder.setSmallIcon(R.drawable.logo_roundsquare)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
|
|
|
||||||
|
|
@ -8,8 +8,6 @@ import android.util.Log
|
||||||
import androidx.preference.PreferenceManager
|
import androidx.preference.PreferenceManager
|
||||||
import fr.forum_thalie.tsumugi.alarm.RadioAlarm
|
import fr.forum_thalie.tsumugi.alarm.RadioAlarm
|
||||||
import fr.forum_thalie.tsumugi.playerstore.PlayerStore
|
import fr.forum_thalie.tsumugi.playerstore.PlayerStore
|
||||||
import fr.forum_thalie.tsumugi.streamerNotificationService.WorkerStore
|
|
||||||
import fr.forum_thalie.tsumugi.streamerNotificationService.startStreamerMonitor
|
|
||||||
|
|
||||||
class BootBroadcastReceiver : BroadcastReceiver(){
|
class BootBroadcastReceiver : BroadcastReceiver(){
|
||||||
|
|
||||||
|
|
@ -19,8 +17,6 @@ class BootBroadcastReceiver : BroadcastReceiver(){
|
||||||
preferenceStore = PreferenceManager.getDefaultSharedPreferences(context)
|
preferenceStore = PreferenceManager.getDefaultSharedPreferences(context)
|
||||||
|
|
||||||
if (arg1.action == Intent.ACTION_BOOT_COMPLETED) {
|
if (arg1.action == Intent.ACTION_BOOT_COMPLETED) {
|
||||||
WorkerStore.instance.init(context)
|
|
||||||
startStreamerMonitor(context) // will actually start it only if enabled in settings
|
|
||||||
RadioAlarm.instance.setNextAlarm(context) // schedule next alarm
|
RadioAlarm.instance.setNextAlarm(context) // schedule next alarm
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -29,8 +25,6 @@ class BootBroadcastReceiver : BroadcastReceiver(){
|
||||||
RadioAlarm.instance.setNextAlarm(context) // schedule next alarm
|
RadioAlarm.instance.setNextAlarm(context) // schedule next alarm
|
||||||
if (PlayerStore.instance.streamerName.value.isNullOrBlank())
|
if (PlayerStore.instance.streamerName.value.isNullOrBlank())
|
||||||
PlayerStore.instance.initPicture(context)
|
PlayerStore.instance.initPicture(context)
|
||||||
if (!PlayerStore.instance.isInitialized)
|
|
||||||
PlayerStore.instance.initApi()
|
|
||||||
|
|
||||||
val i = Intent(context, RadioService::class.java)
|
val i = Intent(context, RadioService::class.java)
|
||||||
i.putExtra("action", Actions.PLAY_OR_FALLBACK.name)
|
i.putExtra("action", Actions.PLAY_OR_FALLBACK.name)
|
||||||
|
|
|
||||||
|
|
@ -15,12 +15,8 @@ import fr.forum_thalie.tsumugi.playerstore.PlayerStore
|
||||||
|
|
||||||
import java.util.Timer
|
import java.util.Timer
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import androidx.preference.PreferenceManager
|
|
||||||
import com.google.android.material.snackbar.Snackbar
|
import com.google.android.material.snackbar.Snackbar
|
||||||
import fr.forum_thalie.tsumugi.alarm.RadioAlarm
|
import fr.forum_thalie.tsumugi.alarm.RadioAlarm
|
||||||
import fr.forum_thalie.tsumugi.streamerNotificationService.WorkerStore
|
|
||||||
import fr.forum_thalie.tsumugi.streamerNotificationService.startStreamerMonitor
|
|
||||||
import fr.forum_thalie.tsumugi.ui.songs.request.Requestor
|
|
||||||
|
|
||||||
|
|
||||||
/* Log to file import
|
/* Log to file import
|
||||||
|
|
@ -39,11 +35,11 @@ class MainActivity : BaseActivity() {
|
||||||
* Called on first creation and when restoring state.
|
* Called on first creation and when restoring state.
|
||||||
*/
|
*/
|
||||||
private fun setupBottomNavigationBar() {
|
private fun setupBottomNavigationBar() {
|
||||||
val bottomNavigationView = findViewById<BottomNavigationView>(R.id.bottom_nav)
|
val bottomNavigationView : BottomNavigationView = findViewById(R.id.bottom_nav)
|
||||||
|
|
||||||
//val navGraphIds = listOf(R.navigation.home, R.navigation.list, R.navigation.form)
|
//val navGraphIds = listOf(R.navigation.home, R.navigation.list, R.navigation.form)
|
||||||
val navGraphIds = listOf(R.navigation.navigation_nowplaying, R.navigation.navigation_songs,
|
val navGraphIds = listOf(R.navigation.navigation_nowplaying, R.navigation.navigation_songs,
|
||||||
R.navigation.navigation_news, R.navigation.navigation_chat)
|
R.navigation.navigation_news)
|
||||||
|
|
||||||
// Setup the bottom navigation view with a list of navigation graphs
|
// Setup the bottom navigation view with a list of navigation graphs
|
||||||
val controller = bottomNavigationView.setupWithNavController(
|
val controller = bottomNavigationView.setupWithNavController(
|
||||||
|
|
@ -77,15 +73,15 @@ class MainActivity : BaseActivity() {
|
||||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||||
// Handle item selection
|
// Handle item selection
|
||||||
return when (item.itemId) {
|
return when (item.itemId) {
|
||||||
|
/*
|
||||||
R.id.action_refresh -> {
|
R.id.action_refresh -> {
|
||||||
PlayerStore.instance.queue.clear()
|
PlayerStore.instance.queue.clear()
|
||||||
PlayerStore.instance.lp.clear()
|
PlayerStore.instance.lp.clear()
|
||||||
PlayerStore.instance.initApi()
|
|
||||||
Requestor.instance.initFavorites()
|
|
||||||
val s = Snackbar.make(findViewById(R.id.nav_host_container), "Refreshing data..." as CharSequence, Snackbar.LENGTH_LONG)
|
val s = Snackbar.make(findViewById(R.id.nav_host_container), "Refreshing data..." as CharSequence, Snackbar.LENGTH_LONG)
|
||||||
s.show()
|
s.show()
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
R.id.action_settings -> {
|
R.id.action_settings -> {
|
||||||
val i = Intent(this, ParametersActivity::class.java)
|
val i = Intent(this, ParametersActivity::class.java)
|
||||||
startActivity(i)
|
startActivity(i)
|
||||||
|
|
@ -118,10 +114,6 @@ class MainActivity : BaseActivity() {
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
WorkerStore.instance.init(this)
|
|
||||||
startStreamerMonitor(this) // this checks the preferenceStore before actually starting a service, don't worry.
|
|
||||||
|
|
||||||
RadioAlarm.instance.cancelAlarm(c = this)
|
RadioAlarm.instance.cancelAlarm(c = this)
|
||||||
RadioAlarm.instance.setNextAlarm(c = this) // this checks the preferenceStore before actually setting an alarm, don't worry.
|
RadioAlarm.instance.setNextAlarm(c = this) // this checks the preferenceStore before actually setting an alarm, don't worry.
|
||||||
|
|
||||||
|
|
@ -132,8 +124,6 @@ class MainActivity : BaseActivity() {
|
||||||
colorRedList = (ResourcesCompat.getColorStateList(resources, R.color.button_red, null))
|
colorRedList = (ResourcesCompat.getColorStateList(resources, R.color.button_red, null))
|
||||||
colorGreenListCompat = (ResourcesCompat.getColorStateList(resources, R.color.button_green_compat, null))
|
colorGreenListCompat = (ResourcesCompat.getColorStateList(resources, R.color.button_green_compat, null))
|
||||||
|
|
||||||
PlayerStore.instance.initApi() // the service will call the initApi on defining the streamerName Observer too, but it's better to initialize the API as soon as the user opens the activity.
|
|
||||||
|
|
||||||
// Post-UI Launch
|
// Post-UI Launch
|
||||||
if (PlayerStore.instance.isInitialized)
|
if (PlayerStore.instance.isInitialized)
|
||||||
{
|
{
|
||||||
|
|
@ -149,9 +139,6 @@ class MainActivity : BaseActivity() {
|
||||||
// initialize some API data
|
// initialize some API data
|
||||||
PlayerStore.instance.initPicture(this)
|
PlayerStore.instance.initPicture(this)
|
||||||
PlayerStore.instance.streamerName.value = "" // initializing the streamer name will trigger an initApi from the observer in the Service.
|
PlayerStore.instance.streamerName.value = "" // initializing the streamer name will trigger an initApi from the observer in the Service.
|
||||||
|
|
||||||
// initialize the favorites
|
|
||||||
Requestor.instance.initFavorites()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isTimerStarted)
|
if (!isTimerStarted)
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,6 @@ class ParametersActivity : BaseActivity() {
|
||||||
ActionOpenParam.ALARM.name -> AlarmFragment()
|
ActionOpenParam.ALARM.name -> AlarmFragment()
|
||||||
ActionOpenParam.SLEEP.name -> SleepFragment()
|
ActionOpenParam.SLEEP.name -> SleepFragment()
|
||||||
ActionOpenParam.CUSTOMIZE.name -> CustomizeFragment()
|
ActionOpenParam.CUSTOMIZE.name -> CustomizeFragment()
|
||||||
ActionOpenParam.STREAMER_NOTIFICATION_SERVICE.name -> StreamerNotifServiceFragment()
|
|
||||||
else -> MainPreferenceFragment()
|
else -> MainPreferenceFragment()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -107,29 +107,6 @@ class RadioService : MediaBrowserServiceCompat() {
|
||||||
// ################### OBSERVERS ####################
|
// ################### OBSERVERS ####################
|
||||||
// ##################################################
|
// ##################################################
|
||||||
|
|
||||||
private val titleObserver: Observer<String> = Observer {
|
|
||||||
if (PlayerStore.instance.playbackState.value == PlaybackStateCompat.STATE_PLAYING)
|
|
||||||
{
|
|
||||||
Log.d(tag, radioTag + "SONG CHANGED AND PLAYING")
|
|
||||||
// we activate latency compensation only if it's been at least 2 songs...
|
|
||||||
when {
|
|
||||||
PlayerStore.instance.isStreamDown -> {
|
|
||||||
// if we reach here, it means that the observer has been called by a new song and that the stream was down previously.
|
|
||||||
// so the stream is now back to normal.
|
|
||||||
PlayerStore.instance.isStreamDown = false
|
|
||||||
PlayerStore.instance.initApi()
|
|
||||||
}
|
|
||||||
PlayerStore.instance.currentSong.title.value == getString(R.string.ed) -> {
|
|
||||||
PlayerStore.instance.isStreamDown = true
|
|
||||||
}
|
|
||||||
else -> {
|
|
||||||
PlayerStore.instance.fetchApi(numberOfSongs >= 2)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
nowPlayingNotification.update(this)
|
|
||||||
}
|
|
||||||
|
|
||||||
private val volumeObserver: Observer<Int> = Observer {
|
private val volumeObserver: Observer<Int> = Observer {
|
||||||
setVolume(it)
|
setVolume(it)
|
||||||
}
|
}
|
||||||
|
|
@ -145,20 +122,14 @@ class RadioService : MediaBrowserServiceCompat() {
|
||||||
stopPlaying()
|
stopPlaying()
|
||||||
}
|
}
|
||||||
|
|
||||||
private val startTimeObserver = Observer<Long> {
|
private val titleObserver = Observer<String> {
|
||||||
// We're listening to startTime to determine if we have to update Queue and Lp.
|
// We're checking if a new song arrives. If so, we put the currentSong in Lp and update the backup.
|
||||||
// this is because startTime is set by the API and never by the ICY, so both cases are covered (playing and stopped)
|
if (PlayerStore.instance.currentSong != PlayerStore.instance.currentSongBackup
|
||||||
// should be OK even when a new streamer comes in.
|
&& it != noConnectionValue)
|
||||||
if (it != PlayerStore.instance.currentSongBackup.startTime.value) // we have a new song
|
|
||||||
{
|
{
|
||||||
PlayerStore.instance.updateLp()
|
PlayerStore.instance.updateLp()
|
||||||
PlayerStore.instance.updateQueue()
|
|
||||||
}
|
}
|
||||||
}
|
nowPlayingNotification.update(this)
|
||||||
|
|
||||||
private val streamerObserver = Observer<String> {
|
|
||||||
PlayerStore.instance.initApi()
|
|
||||||
nowPlayingNotification.update(this) // should update the streamer icon
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private val streamerPictureObserver = Observer<Bitmap> {
|
private val streamerPictureObserver = Observer<Bitmap> {
|
||||||
|
|
@ -224,9 +195,7 @@ class RadioService : MediaBrowserServiceCompat() {
|
||||||
nowPlayingNotification.create(this, mediaSession)
|
nowPlayingNotification.create(this, mediaSession)
|
||||||
|
|
||||||
|
|
||||||
PlayerStore.instance.streamerName.observeForever(streamerObserver)
|
|
||||||
PlayerStore.instance.currentSong.title.observeForever(titleObserver)
|
PlayerStore.instance.currentSong.title.observeForever(titleObserver)
|
||||||
PlayerStore.instance.currentSong.startTime.observeForever(startTimeObserver)
|
|
||||||
PlayerStore.instance.volume.observeForever(volumeObserver)
|
PlayerStore.instance.volume.observeForever(volumeObserver)
|
||||||
PlayerStore.instance.isPlaying.observeForever(isPlayingObserver)
|
PlayerStore.instance.isPlaying.observeForever(isPlayingObserver)
|
||||||
PlayerStore.instance.isMuted.observeForever(isMutedObserver)
|
PlayerStore.instance.isMuted.observeForever(isMutedObserver)
|
||||||
|
|
@ -234,12 +203,6 @@ class RadioService : MediaBrowserServiceCompat() {
|
||||||
|
|
||||||
startForeground(radioServiceId, nowPlayingNotification.notification)
|
startForeground(radioServiceId, nowPlayingNotification.notification)
|
||||||
|
|
||||||
// start ticker for when the player is stopped
|
|
||||||
val periodString = PreferenceManager.getDefaultSharedPreferences(this).getString("fetchPeriod", "10") ?: "10"
|
|
||||||
val period: Long = Integer.parseInt(periodString).toLong()
|
|
||||||
if (period > 0)
|
|
||||||
apiTicker.schedule(ApiFetchTick(), 0, period * 1000)
|
|
||||||
|
|
||||||
PlayerStore.instance.isServiceStarted.value = true
|
PlayerStore.instance.isServiceStarted.value = true
|
||||||
Log.d(tag, radioTag + "created")
|
Log.d(tag, radioTag + "created")
|
||||||
}
|
}
|
||||||
|
|
@ -303,7 +266,6 @@ class RadioService : MediaBrowserServiceCompat() {
|
||||||
player.release()
|
player.release()
|
||||||
unregisterReceiver(receiver)
|
unregisterReceiver(receiver)
|
||||||
PlayerStore.instance.currentSong.title.removeObserver(titleObserver)
|
PlayerStore.instance.currentSong.title.removeObserver(titleObserver)
|
||||||
PlayerStore.instance.currentSong.startTime.removeObserver(startTimeObserver)
|
|
||||||
PlayerStore.instance.volume.removeObserver(volumeObserver)
|
PlayerStore.instance.volume.removeObserver(volumeObserver)
|
||||||
PlayerStore.instance.isPlaying.removeObserver(isPlayingObserver)
|
PlayerStore.instance.isPlaying.removeObserver(isPlayingObserver)
|
||||||
PlayerStore.instance.isMuted.removeObserver(isMutedObserver)
|
PlayerStore.instance.isMuted.removeObserver(isMutedObserver)
|
||||||
|
|
|
||||||
|
|
@ -11,11 +11,3 @@ class Tick : TimerTask() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class ApiFetchTick : TimerTask() {
|
|
||||||
override fun run() {
|
|
||||||
if (PlayerStore.instance.playbackState.value == PlaybackStateCompat.STATE_STOPPED)
|
|
||||||
{
|
|
||||||
PlayerStore.instance.fetchApi()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -31,10 +31,8 @@ class PlayerStore {
|
||||||
val isLpUpdated: MutableLiveData<Boolean> = MutableLiveData()
|
val isLpUpdated: MutableLiveData<Boolean> = MutableLiveData()
|
||||||
val isMuted : MutableLiveData<Boolean> = MutableLiveData()
|
val isMuted : MutableLiveData<Boolean> = MutableLiveData()
|
||||||
val listenersCount: MutableLiveData<Int> = MutableLiveData()
|
val listenersCount: MutableLiveData<Int> = MutableLiveData()
|
||||||
private val urlToScrape = "https://r-a-d.io/api"
|
|
||||||
var latencyCompensator : Long = 0
|
var latencyCompensator : Long = 0
|
||||||
var isInitialized: Boolean = false
|
var isInitialized: Boolean = false
|
||||||
var isStreamDown: Boolean = false
|
|
||||||
|
|
||||||
init {
|
init {
|
||||||
playbackState.value = PlaybackStateCompat.STATE_STOPPED
|
playbackState.value = PlaybackStateCompat.STATE_STOPPED
|
||||||
|
|
@ -47,6 +45,7 @@ class PlayerStore {
|
||||||
isLpUpdated.value = false
|
isLpUpdated.value = false
|
||||||
isMuted.value = false
|
isMuted.value = false
|
||||||
currentSong.title.value = noConnectionValue
|
currentSong.title.value = noConnectionValue
|
||||||
|
currentSongBackup.title.value = noConnectionValue
|
||||||
listenersCount.value = 0
|
listenersCount.value = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -54,6 +53,7 @@ class PlayerStore {
|
||||||
// ################# API FUNCTIONS ##################
|
// ################# API FUNCTIONS ##################
|
||||||
// ##################################################
|
// ##################################################
|
||||||
|
|
||||||
|
/*
|
||||||
private fun updateApi(resMain: JSONObject, isCompensatingLatency : Boolean = false) {
|
private fun updateApi(resMain: JSONObject, isCompensatingLatency : Boolean = false) {
|
||||||
// If we're not in PLAYING state, update title / artist metadata. If we're playing, the ICY will take care of that.
|
// If we're not in PLAYING state, update title / artist metadata. If we're playing, the ICY will take care of that.
|
||||||
if (playbackState.value != PlaybackStateCompat.STATE_PLAYING || currentSong.title.value.isNullOrEmpty()
|
if (playbackState.value != PlaybackStateCompat.STATE_PLAYING || currentSong.title.value.isNullOrEmpty()
|
||||||
|
|
@ -154,24 +154,26 @@ class PlayerStore {
|
||||||
}
|
}
|
||||||
Async(scrape, post)
|
Async(scrape, post)
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
// ##################################################
|
// ##################################################
|
||||||
// ############## QUEUE / LP FUNCTIONS ##############
|
// ############## QUEUE / LP FUNCTIONS ##############
|
||||||
// ##################################################
|
// ##################################################
|
||||||
|
|
||||||
fun updateLp() {
|
fun updateLp() {
|
||||||
// note : lp must never be empty. There should always be some songs "last played".
|
// note : lp is empty at initialization. This check was needed when we used the R/a/dio API.
|
||||||
// if not, then the function has been called before initialization. No need to do anything.
|
//if (lp.isNotEmpty()){
|
||||||
if (lp.isNotEmpty()){
|
|
||||||
val n = Song()
|
val n = Song()
|
||||||
n.copy(currentSongBackup)
|
n.copy(currentSongBackup)
|
||||||
|
if (n.title.value != noConnectionValue)
|
||||||
lp.add(0, n)
|
lp.add(0, n)
|
||||||
currentSongBackup.copy(currentSong)
|
currentSongBackup.copy(currentSong)
|
||||||
isLpUpdated.value = true
|
isLpUpdated.value = true
|
||||||
Log.d(tag, playerStoreTag + lp.toString())
|
Log.d(tag, playerStoreTag + lp.toString())
|
||||||
}
|
//}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
fun updateQueue() {
|
fun updateQueue() {
|
||||||
if (queue.isNotEmpty()) {
|
if (queue.isNotEmpty()) {
|
||||||
queue.remove(queue.first())
|
queue.remove(queue.first())
|
||||||
|
|
@ -257,11 +259,13 @@ class PlayerStore {
|
||||||
song.type.value = songJSON.getInt("type")
|
song.type.value = songJSON.getInt("type")
|
||||||
return song
|
return song
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
// ##################################################
|
// ##################################################
|
||||||
// ############## PICTURE FUNCTIONS #################
|
// ############## PICTURE FUNCTIONS #################
|
||||||
// ##################################################
|
// ##################################################
|
||||||
|
|
||||||
|
/*
|
||||||
private fun fetchPicture(fileUrl: String)
|
private fun fetchPicture(fileUrl: String)
|
||||||
{
|
{
|
||||||
val scrape: (Any?) -> Bitmap? = {
|
val scrape: (Any?) -> Bitmap? = {
|
||||||
|
|
@ -287,10 +291,11 @@ class PlayerStore {
|
||||||
}
|
}
|
||||||
Async(scrape, post)
|
Async(scrape, post)
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
fun initPicture(c: Context) {
|
fun initPicture(c: Context) {
|
||||||
streamerPicture.value = BitmapFactory.decodeResource(c.resources,
|
streamerPicture.value = BitmapFactory.decodeResource(c.resources,
|
||||||
R.drawable.actionbar_logo
|
R.drawable.logo_roundsquare
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,19 +8,11 @@ import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.preference.*
|
import androidx.preference.*
|
||||||
import fr.forum_thalie.tsumugi.R
|
import fr.forum_thalie.tsumugi.R
|
||||||
import fr.forum_thalie.tsumugi.preferenceStore
|
import fr.forum_thalie.tsumugi.preferenceStore
|
||||||
import fr.forum_thalie.tsumugi.ui.songs.request.Requestor
|
|
||||||
|
|
||||||
class CustomizeFragment : PreferenceFragmentCompat() {
|
class CustomizeFragment : PreferenceFragmentCompat() {
|
||||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||||
setPreferencesFromResource(R.xml.customize_preferences, rootKey)
|
setPreferencesFromResource(R.xml.customize_preferences, rootKey)
|
||||||
|
|
||||||
val userNamePref = preferenceScreen.findPreference<EditTextPreference>("userName")
|
|
||||||
userNamePref?.summaryProvider = EditTextPreference.SimpleSummaryProvider.getInstance()
|
|
||||||
userNamePref?.setOnPreferenceChangeListener { _, newValue ->
|
|
||||||
val name = newValue as String
|
|
||||||
Requestor.instance.initFavorites(name) // need to be as parameter cause the callback is called BEFORE PARAMETER SET
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
val snackbarPersistent = preferenceScreen.findPreference<SwitchPreferenceCompat>("snackbarPersistent")
|
val snackbarPersistent = preferenceScreen.findPreference<SwitchPreferenceCompat>("snackbarPersistent")
|
||||||
snackbarPersistent!!.summary = if (preferenceStore.getBoolean("snackbarPersistent", true))
|
snackbarPersistent!!.summary = if (preferenceStore.getBoolean("snackbarPersistent", true))
|
||||||
|
|
@ -48,33 +40,5 @@ class CustomizeFragment : PreferenceFragmentCompat() {
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
val helpFavorites = preferenceScreen.findPreference<Preference>("helpFavorites")
|
|
||||||
helpFavorites?.setOnPreferenceClickListener { _ ->
|
|
||||||
val url = getString(R.string.github_url_wiki_irc_for_favorites)
|
|
||||||
val i = Intent(Intent.ACTION_VIEW)
|
|
||||||
i.data = Uri.parse(url)
|
|
||||||
startActivity(i)
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
val fetchPeriod = preferenceScreen.findPreference<ListPreference>("fetchPeriod")
|
|
||||||
fetchPeriod?.summaryProvider = ListPreference.SimpleSummaryProvider.getInstance()
|
|
||||||
fetchPeriod?.setOnPreferenceChangeListener { _, newValue ->
|
|
||||||
val builder1 = AlertDialog.Builder(context!!)
|
|
||||||
if (Integer.parseInt(newValue as String) == 0)
|
|
||||||
builder1.setMessage(R.string.fetch_disabled_restart_the_app)
|
|
||||||
else
|
|
||||||
builder1.setMessage(R.string.restart_the_app)
|
|
||||||
builder1.setCancelable(true)
|
|
||||||
|
|
||||||
builder1.setPositiveButton("Close" ) { dialog, _ ->
|
|
||||||
dialog.cancel()
|
|
||||||
}
|
|
||||||
|
|
||||||
val alert11 = builder1.create()
|
|
||||||
alert11.show()
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,63 +0,0 @@
|
||||||
package fr.forum_thalie.tsumugi.preferences
|
|
||||||
|
|
||||||
import android.os.Bundle
|
|
||||||
import androidx.appcompat.app.AlertDialog
|
|
||||||
import androidx.preference.*
|
|
||||||
import fr.forum_thalie.tsumugi.R
|
|
||||||
import fr.forum_thalie.tsumugi.preferenceStore
|
|
||||||
import fr.forum_thalie.tsumugi.streamerNotificationService.WorkerStore
|
|
||||||
import fr.forum_thalie.tsumugi.streamerNotificationService.startStreamerMonitor
|
|
||||||
import fr.forum_thalie.tsumugi.streamerNotificationService.stopStreamerMonitor
|
|
||||||
|
|
||||||
class StreamerNotifServiceFragment : PreferenceFragmentCompat() {
|
|
||||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
|
||||||
setPreferencesFromResource(R.xml.streamer_notif_service_preferences, rootKey)
|
|
||||||
|
|
||||||
|
|
||||||
val streamerPeriod = preferenceScreen.findPreference<Preference>("streamerMonitorPeriodPref")
|
|
||||||
|
|
||||||
val streamerNotification = preferenceScreen.findPreference<Preference>("newStreamerNotification")
|
|
||||||
streamerNotification?.setOnPreferenceChangeListener { _, newValue ->
|
|
||||||
if ((newValue as Boolean)) {
|
|
||||||
val builder1 = AlertDialog.Builder(context!!)
|
|
||||||
builder1.setMessage(R.string.warningStreamerNotif)
|
|
||||||
builder1.setCancelable(false)
|
|
||||||
builder1.setPositiveButton(
|
|
||||||
"Yes"
|
|
||||||
) { dialog, _ ->
|
|
||||||
startStreamerMonitor(context!!, force = true) // force enabled because the preference value is not yet set when running this callback.
|
|
||||||
streamerPeriod?.isEnabled = true
|
|
||||||
dialog.cancel()
|
|
||||||
}
|
|
||||||
|
|
||||||
builder1.setNegativeButton(
|
|
||||||
"No"
|
|
||||||
) { dialog, _ ->
|
|
||||||
|
|
||||||
stopStreamerMonitor(context!!)
|
|
||||||
(streamerNotification as SwitchPreferenceCompat).isChecked = false
|
|
||||||
dialog.cancel()
|
|
||||||
}
|
|
||||||
|
|
||||||
val alert11 = builder1.create()
|
|
||||||
alert11.show()
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
stopStreamerMonitor(context!!)
|
|
||||||
streamerPeriod?.isEnabled = false
|
|
||||||
WorkerStore.instance.isServiceStarted = false
|
|
||||||
}
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
streamerPeriod?.summaryProvider = ListPreference.SimpleSummaryProvider.getInstance()
|
|
||||||
streamerPeriod?.isEnabled = preferenceStore.getBoolean("newStreamerNotification", true)
|
|
||||||
streamerPeriod?.setOnPreferenceChangeListener { _, newValue ->
|
|
||||||
WorkerStore.instance.tickerPeriod = (Integer.parseInt(newValue as String)).toLong() * 60
|
|
||||||
// this should be sufficient, the next alarm schedule should take the new tickerPeriod.
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,41 +0,0 @@
|
||||||
package fr.forum_thalie.tsumugi.streamerNotificationService
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import fr.forum_thalie.tsumugi.BaseNotification
|
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
class ServiceNotification(
|
|
||||||
notificationChannelId: String,
|
|
||||||
notificationChannel : Int,
|
|
||||||
notificationId: Int,
|
|
||||||
notificationImportance: Int
|
|
||||||
|
|
||||||
) : BaseNotification
|
|
||||||
(
|
|
||||||
notificationChannelId,
|
|
||||||
notificationChannel,
|
|
||||||
notificationId,
|
|
||||||
notificationImportance
|
|
||||||
){
|
|
||||||
|
|
||||||
override fun create(c: Context)
|
|
||||||
{
|
|
||||||
super.create(c)
|
|
||||||
update()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun update()
|
|
||||||
{
|
|
||||||
val date = Date() // given date
|
|
||||||
val calendar = Calendar.getInstance() // creates a new calendar instance
|
|
||||||
calendar.time = date // assigns calendar to given date
|
|
||||||
val hours = calendar.get(Calendar.HOUR_OF_DAY) // gets hour in 24h format
|
|
||||||
//val hours_american = calendar.get(Calendar.HOUR) // gets hour in 12h format
|
|
||||||
val minutes = calendar.get(Calendar.MINUTE)
|
|
||||||
val seconds = calendar.get(Calendar.SECOND)
|
|
||||||
|
|
||||||
builder.setContentTitle("Never miss a stream! Current: ${WorkerStore.instance.streamerName.value}")
|
|
||||||
builder.setContentText("Last update: ${hours}:${minutes}:${seconds}")
|
|
||||||
notification = builder.build()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,114 +0,0 @@
|
||||||
package fr.forum_thalie.tsumugi.streamerNotificationService
|
|
||||||
|
|
||||||
import android.app.AlarmManager
|
|
||||||
import android.app.PendingIntent
|
|
||||||
import android.content.BroadcastReceiver
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
|
||||||
import android.os.Build
|
|
||||||
import android.os.SystemClock
|
|
||||||
import android.util.Log
|
|
||||||
import androidx.core.app.NotificationCompat
|
|
||||||
import androidx.preference.PreferenceManager
|
|
||||||
import fr.forum_thalie.tsumugi.*
|
|
||||||
import fr.forum_thalie.tsumugi.alarm.RadioAlarm
|
|
||||||
import fr.forum_thalie.tsumugi.playerstore.PlayerStore
|
|
||||||
import org.json.JSONObject
|
|
||||||
import java.net.URL
|
|
||||||
|
|
||||||
fun startNextAlarmStreamer(c: Context){
|
|
||||||
// the notification works with an alarm re-scheduled at fixed rate.
|
|
||||||
// if the service stopped, the alarm is not re-scheduled.
|
|
||||||
if (WorkerStore.instance.isServiceStarted)
|
|
||||||
{
|
|
||||||
val alarmIntent = Intent(c, StreamerMonitorService::class.java).let { intent ->
|
|
||||||
intent.putExtra("action", Actions.NOTIFY.name)
|
|
||||||
PendingIntent.getService(c, 0, intent, 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
val alarmMgr = c.getSystemService(Context.ALARM_SERVICE) as AlarmManager
|
|
||||||
when {
|
|
||||||
Build.VERSION.SDK_INT >= Build.VERSION_CODES.M -> alarmMgr.setExactAndAllowWhileIdle(
|
|
||||||
AlarmManager.ELAPSED_REALTIME_WAKEUP,
|
|
||||||
SystemClock.elapsedRealtime() + WorkerStore.instance.tickerPeriod * 1000,
|
|
||||||
alarmIntent
|
|
||||||
)
|
|
||||||
Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT -> alarmMgr.setExact(
|
|
||||||
AlarmManager.ELAPSED_REALTIME_WAKEUP,
|
|
||||||
SystemClock.elapsedRealtime() + WorkerStore.instance.tickerPeriod * 1000,
|
|
||||||
alarmIntent
|
|
||||||
)
|
|
||||||
else -> alarmMgr.set(
|
|
||||||
AlarmManager.ELAPSED_REALTIME_WAKEUP,
|
|
||||||
SystemClock.elapsedRealtime() + WorkerStore.instance.tickerPeriod * 1000,
|
|
||||||
alarmIntent
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Log.d(tag, "alarm called while service is dead - skipped.")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun stopStreamerMonitor(context: Context)
|
|
||||||
{
|
|
||||||
val intent = Intent(context, StreamerMonitorService::class.java)
|
|
||||||
intent.putExtra("action", Actions.KILL.name)
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
||||||
context.startService(intent)
|
|
||||||
} else {
|
|
||||||
context.startService(intent)
|
|
||||||
}
|
|
||||||
|
|
||||||
Log.i(tag, "Service stopped")
|
|
||||||
}
|
|
||||||
|
|
||||||
fun startStreamerMonitor(context: Context, force: Boolean = false)
|
|
||||||
{
|
|
||||||
if (!force)
|
|
||||||
{
|
|
||||||
val isNotifyingForNewStreamer = PreferenceManager.getDefaultSharedPreferences(context).getBoolean("newStreamerNotification", false)
|
|
||||||
if (!isNotifyingForNewStreamer)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
val intent = Intent(context, StreamerMonitorService::class.java)
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
||||||
context.startForegroundService(intent)
|
|
||||||
} else {
|
|
||||||
context.startService(intent)
|
|
||||||
}
|
|
||||||
|
|
||||||
Log.i(tag, "Service started on boot")
|
|
||||||
}
|
|
||||||
|
|
||||||
fun fetchStreamer(applicationContext: Context) {
|
|
||||||
val urlToScrape = "https://r-a-d.io/api"
|
|
||||||
val scrape : (Any?) -> String =
|
|
||||||
{
|
|
||||||
URL(urlToScrape).readText()
|
|
||||||
}
|
|
||||||
val post: (parameter: Any?) -> Unit = {
|
|
||||||
val result = JSONObject(it as String)
|
|
||||||
if (!result.isNull("main"))
|
|
||||||
{
|
|
||||||
val name = result.getJSONObject("main").getJSONObject("dj").getString("djname")
|
|
||||||
WorkerStore.instance.streamerName.value = name
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// notify
|
|
||||||
val t = ServiceNotification(
|
|
||||||
notificationChannelId = applicationContext.getString(R.string.streamerServiceChannelId),
|
|
||||||
notificationChannel = R.string.streamerServiceChannel,
|
|
||||||
notificationId = 2,
|
|
||||||
notificationImportance = NotificationCompat.PRIORITY_LOW
|
|
||||||
)
|
|
||||||
t.create(applicationContext)
|
|
||||||
t.show()
|
|
||||||
|
|
||||||
try{
|
|
||||||
Async(scrape, post)
|
|
||||||
Log.d(tag, "enqueue next work in ${WorkerStore.instance.tickerPeriod} seconds")
|
|
||||||
} catch (e: Exception) {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,123 +0,0 @@
|
||||||
package fr.forum_thalie.tsumugi.streamerNotificationService
|
|
||||||
|
|
||||||
|
|
||||||
import android.app.Service
|
|
||||||
import android.content.Intent
|
|
||||||
import android.os.Build
|
|
||||||
import android.os.IBinder
|
|
||||||
import android.util.Log
|
|
||||||
import androidx.core.app.NotificationCompat
|
|
||||||
import androidx.lifecycle.Observer
|
|
||||||
import androidx.preference.PreferenceManager
|
|
||||||
import fr.forum_thalie.tsumugi.Actions
|
|
||||||
import fr.forum_thalie.tsumugi.R
|
|
||||||
import fr.forum_thalie.tsumugi.tag
|
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
class StreamerMonitorService : Service() {
|
|
||||||
override fun onBind(intent: Intent): IBinder? {
|
|
||||||
return null // no binding allowed nor needed
|
|
||||||
}
|
|
||||||
private val streamerNameObserver: Observer<String> = Observer {
|
|
||||||
val previousStreamer: String
|
|
||||||
if (PreferenceManager.getDefaultSharedPreferences(this).contains("streamerName"))
|
|
||||||
{
|
|
||||||
previousStreamer = PreferenceManager.getDefaultSharedPreferences(this).getString("streamerName", "") ?: ""
|
|
||||||
/* 3 conditions:
|
|
||||||
- the streamer changed from previously
|
|
||||||
- there is a previous non-empty streamer (at least second time running it)
|
|
||||||
- the current streamer is non-empty (this can happen at Activity start where init() is called)
|
|
||||||
*/
|
|
||||||
if (previousStreamer != it && previousStreamer != "" && it != "")
|
|
||||||
{
|
|
||||||
// notify
|
|
||||||
val newStreamer = StreamerNotification(
|
|
||||||
notificationChannelId = this.getString(R.string.streamerNotificationChannelId),
|
|
||||||
notificationChannel = R.string.streamerNotificationChannel,
|
|
||||||
notificationId = 3,
|
|
||||||
notificationImportance = NotificationCompat.PRIORITY_DEFAULT
|
|
||||||
)
|
|
||||||
newStreamer.create(this)
|
|
||||||
newStreamer.show()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
with(PreferenceManager.getDefaultSharedPreferences(this).edit()){
|
|
||||||
putString("streamerName", it)
|
|
||||||
commit()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreate() {
|
|
||||||
super.onCreate()
|
|
||||||
val streamerMonitorNotification = ServiceNotification(
|
|
||||||
notificationChannelId = this.getString(R.string.streamerServiceChannelId),
|
|
||||||
notificationChannel = R.string.streamerServiceChannel,
|
|
||||||
notificationId = 2,
|
|
||||||
notificationImportance = NotificationCompat.PRIORITY_LOW
|
|
||||||
)
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
|
|
||||||
{
|
|
||||||
streamerMonitorNotification.create(this)
|
|
||||||
streamerMonitorNotification.update()
|
|
||||||
streamerMonitorNotification.show()
|
|
||||||
startForeground(2, streamerMonitorNotification.notification)
|
|
||||||
}
|
|
||||||
|
|
||||||
WorkerStore.instance.tickerPeriod = 60 *
|
|
||||||
(if (PreferenceManager.getDefaultSharedPreferences(this).contains("streamerMonitorPeriodPref"))
|
|
||||||
Integer.parseInt(PreferenceManager.getDefaultSharedPreferences(this).getString("streamerMonitorPeriodPref", "15")!!).toLong()
|
|
||||||
else
|
|
||||||
15)
|
|
||||||
Log.d(tag, "tickerPeriod = ${WorkerStore.instance.tickerPeriod}")
|
|
||||||
|
|
||||||
with(PreferenceManager.getDefaultSharedPreferences(this).edit()){
|
|
||||||
remove("streamerName")
|
|
||||||
commit() // I commit on main thread to be sure it's been updated before continuing.
|
|
||||||
}
|
|
||||||
WorkerStore.instance.streamerName.observeForever(streamerNameObserver)
|
|
||||||
WorkerStore.instance.isServiceStarted = true
|
|
||||||
startNextAlarmStreamer(this)
|
|
||||||
Log.d(tag, "streamerMonitor created")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
|
||||||
val isNotifyingForNewStreamer = PreferenceManager.getDefaultSharedPreferences(this).getBoolean("newStreamerNotification", false)
|
|
||||||
|
|
||||||
// it's probably redundant but it shouldn't hurt
|
|
||||||
if (!isNotifyingForNewStreamer || !WorkerStore.instance.isServiceStarted)
|
|
||||||
{
|
|
||||||
stopForeground(true)
|
|
||||||
stopSelf()
|
|
||||||
return START_NOT_STICKY
|
|
||||||
}
|
|
||||||
when (intent?.getStringExtra("action")) {
|
|
||||||
Actions.NOTIFY.name -> {
|
|
||||||
val date = Date() // given date
|
|
||||||
val calendar = Calendar.getInstance() // creates a new calendar instance
|
|
||||||
calendar.time = date // assigns calendar to given date
|
|
||||||
val hours = calendar.get(Calendar.HOUR_OF_DAY) // gets hour in 24h format
|
|
||||||
//val hours_american = calendar.get(Calendar.HOUR) // gets hour in 12h format
|
|
||||||
val minutes = calendar.get(Calendar.MINUTE) // gets month number, NOTE this is zero based!
|
|
||||||
|
|
||||||
Log.d(tag, "Fetched streamer name at ${hours}:${if (minutes < 10) "0" else ""}${minutes}")
|
|
||||||
fetchStreamer(this)
|
|
||||||
startNextAlarmStreamer(this) // schedule next alarm
|
|
||||||
return START_STICKY
|
|
||||||
}
|
|
||||||
Actions.KILL.name -> {
|
|
||||||
stopForeground(true)
|
|
||||||
stopSelf()
|
|
||||||
return START_NOT_STICKY
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return START_STICKY
|
|
||||||
//super.onStartCommand(intent, flags, startId)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDestroy() {
|
|
||||||
WorkerStore.instance.streamerName.removeObserver(streamerNameObserver)
|
|
||||||
WorkerStore.instance.isServiceStarted = false
|
|
||||||
super.onDestroy()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,35 +0,0 @@
|
||||||
package fr.forum_thalie.tsumugi.streamerNotificationService
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import fr.forum_thalie.tsumugi.BaseNotification
|
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
class StreamerNotification(
|
|
||||||
notificationChannelId: String,
|
|
||||||
notificationChannel : Int,
|
|
||||||
notificationId: Int,
|
|
||||||
notificationImportance: Int
|
|
||||||
|
|
||||||
) : BaseNotification
|
|
||||||
(
|
|
||||||
notificationChannelId,
|
|
||||||
notificationChannel,
|
|
||||||
notificationId,
|
|
||||||
notificationImportance
|
|
||||||
){
|
|
||||||
override fun create(c: Context) {
|
|
||||||
super.create(c)
|
|
||||||
val date = Date() // given date
|
|
||||||
val calendar = Calendar.getInstance() // creates a new calendar instance
|
|
||||||
calendar.time = date // assigns calendar to given date
|
|
||||||
val hours = calendar.get(Calendar.HOUR_OF_DAY) // gets hour in 24h format
|
|
||||||
//val hours_american = calendar.get(Calendar.HOUR) // gets hour in 12h format
|
|
||||||
val minutes = calendar.get(Calendar.MINUTE) // gets month number, NOTE this is zero based!
|
|
||||||
|
|
||||||
builder.setContentTitle("${WorkerStore.instance.streamerName.value} started streaming!")
|
|
||||||
builder.setContentText("Started at ${hours}:${if (minutes < 10) "0" else ""}${minutes}")
|
|
||||||
builder.setAutoCancel(true)
|
|
||||||
super.show()
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -1,46 +0,0 @@
|
||||||
package fr.forum_thalie.tsumugi.streamerNotificationService
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.util.Log
|
|
||||||
import androidx.core.app.NotificationCompat
|
|
||||||
import androidx.lifecycle.MutableLiveData
|
|
||||||
import androidx.lifecycle.Observer
|
|
||||||
import androidx.preference.PreferenceManager
|
|
||||||
import fr.forum_thalie.tsumugi.R
|
|
||||||
import fr.forum_thalie.tsumugi.tag
|
|
||||||
|
|
||||||
class WorkerStore {
|
|
||||||
companion object {
|
|
||||||
val instance = WorkerStore()
|
|
||||||
}
|
|
||||||
|
|
||||||
val streamerName = MutableLiveData<String>()
|
|
||||||
var isServiceStarted : Boolean = false
|
|
||||||
var tickerPeriod : Long = 45 // seconds
|
|
||||||
|
|
||||||
init {
|
|
||||||
tickerPeriod = 45
|
|
||||||
streamerName.value = ""
|
|
||||||
isServiceStarted = false
|
|
||||||
}
|
|
||||||
|
|
||||||
fun init(c: Context)
|
|
||||||
{
|
|
||||||
tickerPeriod = 45
|
|
||||||
streamerName.value = ""
|
|
||||||
val tickerPeriod = 60 *
|
|
||||||
(if (PreferenceManager.getDefaultSharedPreferences(c).contains("streamerMonitorPeriodPref"))
|
|
||||||
Integer.parseInt(PreferenceManager.getDefaultSharedPreferences(c).getString("streamerMonitorPeriodPref", "15")!!).toLong()
|
|
||||||
else
|
|
||||||
15
|
|
||||||
)
|
|
||||||
instance.tickerPeriod = tickerPeriod
|
|
||||||
Log.d(tag, "tickerPeriod = $tickerPeriod")
|
|
||||||
|
|
||||||
with(PreferenceManager.getDefaultSharedPreferences(c).edit()){
|
|
||||||
remove("streamerName")
|
|
||||||
commit() // I commit on main thread to be sure it's been updated before continuing.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -1,52 +0,0 @@
|
||||||
package fr.forum_thalie.tsumugi.ui.chat
|
|
||||||
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.util.Log
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import android.view.WindowManager
|
|
||||||
import android.webkit.WebView
|
|
||||||
import androidx.fragment.app.Fragment
|
|
||||||
import androidx.lifecycle.ViewModelProviders
|
|
||||||
import fr.forum_thalie.tsumugi.R
|
|
||||||
|
|
||||||
|
|
||||||
class ChatFragment : Fragment() {
|
|
||||||
|
|
||||||
private lateinit var chatViewModel: ChatViewModel
|
|
||||||
|
|
||||||
|
|
||||||
override fun onCreateView(
|
|
||||||
inflater: LayoutInflater,
|
|
||||||
container: ViewGroup?,
|
|
||||||
savedInstanceState: Bundle?
|
|
||||||
): View? {
|
|
||||||
activity?.window?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_PAN)
|
|
||||||
|
|
||||||
chatViewModel =
|
|
||||||
ViewModelProviders.of(this).get(ChatViewModel::class.java)
|
|
||||||
|
|
||||||
if (!chatViewModel.isChatLoaded)
|
|
||||||
{
|
|
||||||
|
|
||||||
try {
|
|
||||||
chatViewModel.root = inflater.inflate(R.layout.fragment_chat, container, false)
|
|
||||||
chatViewModel.webView = chatViewModel.root.findViewById<WebView>(R.id.chat_webview)
|
|
||||||
chatViewModel.webViewChat = WebViewChat(chatViewModel.webView as WebView)
|
|
||||||
chatViewModel.webViewChat!!.start()
|
|
||||||
} catch (e: Exception) {
|
|
||||||
chatViewModel.root = inflater.inflate(R.layout.fragment_error_chat, container, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
chatViewModel.isChatLoaded = true
|
|
||||||
Log.d(tag, "webview created")
|
|
||||||
} else {
|
|
||||||
Log.d(tag, "webview already created!?")
|
|
||||||
}
|
|
||||||
|
|
||||||
return chatViewModel.root
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -1,12 +0,0 @@
|
||||||
package fr.forum_thalie.tsumugi.ui.chat
|
|
||||||
|
|
||||||
import android.view.View
|
|
||||||
import android.webkit.WebView
|
|
||||||
import androidx.lifecycle.ViewModel
|
|
||||||
|
|
||||||
class ChatViewModel : ViewModel() {
|
|
||||||
lateinit var root: View
|
|
||||||
var webView: WebView? = null
|
|
||||||
var isChatLoaded = false
|
|
||||||
var webViewChat: WebViewChat? = null
|
|
||||||
}
|
|
||||||
|
|
@ -1,47 +0,0 @@
|
||||||
package fr.forum_thalie.tsumugi.ui.chat
|
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.content.Intent
|
|
||||||
import android.net.Uri
|
|
||||||
import android.webkit.WebChromeClient
|
|
||||||
import android.webkit.WebView
|
|
||||||
|
|
||||||
class WebViewChat(private val webView: WebView) {
|
|
||||||
|
|
||||||
@SuppressLint("SetJavaScriptEnabled")
|
|
||||||
fun start() {
|
|
||||||
|
|
||||||
val webSetting = this.webView.settings
|
|
||||||
webSetting.javaScriptEnabled = true
|
|
||||||
webSetting.setSupportZoom(false)
|
|
||||||
|
|
||||||
/* TODO: in the future, it could be nice to have a parameters screen where you can:
|
|
||||||
- Set the text zoom
|
|
||||||
- Set your username (to not type it every time, would it be possible?)
|
|
||||||
- Hide the chat?
|
|
||||||
- do more? */
|
|
||||||
webSetting.textZoom = 90
|
|
||||||
|
|
||||||
webSetting.setSupportMultipleWindows(true)
|
|
||||||
// needs to open target="_blank" links as KiwiIRC links have this attribute.
|
|
||||||
// shamelessly ripped off https://stackoverflow.com/questions/18187714/android-open-target-blank-links-in-webview-with-external-browser
|
|
||||||
this.webView.webChromeClient = object : WebChromeClient() {
|
|
||||||
override fun onCreateWindow(
|
|
||||||
view: WebView,
|
|
||||||
dialog: Boolean,
|
|
||||||
userGesture: Boolean,
|
|
||||||
resultMsg: android.os.Message
|
|
||||||
): Boolean {
|
|
||||||
val result = view.hitTestResult
|
|
||||||
val data = result.extra
|
|
||||||
val context = view.context
|
|
||||||
val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse(data))
|
|
||||||
context.startActivity(browserIntent)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
webView.loadUrl("file:///android_asset/chat.html")
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -5,62 +5,19 @@ import android.util.Log
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.TextView
|
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.fragment.app.FragmentPagerAdapter
|
import androidx.fragment.app.FragmentPagerAdapter
|
||||||
import androidx.lifecycle.Observer
|
|
||||||
import androidx.lifecycle.ViewModelProviders
|
|
||||||
import androidx.viewpager.widget.ViewPager
|
import androidx.viewpager.widget.ViewPager
|
||||||
import com.google.android.material.snackbar.BaseTransientBottomBar
|
|
||||||
import com.google.android.material.snackbar.Snackbar
|
|
||||||
import com.google.android.material.tabs.TabLayout
|
import com.google.android.material.tabs.TabLayout
|
||||||
import fr.forum_thalie.tsumugi.R
|
import fr.forum_thalie.tsumugi.R
|
||||||
import fr.forum_thalie.tsumugi.preferenceStore
|
|
||||||
import fr.forum_thalie.tsumugi.ui.songs.queuelp.LastPlayedFragment
|
import fr.forum_thalie.tsumugi.ui.songs.queuelp.LastPlayedFragment
|
||||||
import fr.forum_thalie.tsumugi.ui.songs.queuelp.QueueFragment
|
|
||||||
import fr.forum_thalie.tsumugi.ui.songs.request.FavoritesFragment
|
|
||||||
import fr.forum_thalie.tsumugi.ui.songs.request.RequestFragment
|
|
||||||
import fr.forum_thalie.tsumugi.ui.songs.request.Requestor
|
|
||||||
|
|
||||||
class SongsFragment : Fragment() {
|
class SongsFragment : Fragment() {
|
||||||
|
|
||||||
private lateinit var adapter : SongsPagerAdapter
|
private lateinit var adapter : SongsPagerAdapter
|
||||||
private lateinit var snackBar : Snackbar
|
|
||||||
private lateinit var root: View
|
private lateinit var root: View
|
||||||
private lateinit var viewPager: ViewPager
|
private lateinit var viewPager: ViewPager
|
||||||
|
|
||||||
|
|
||||||
private val snackBarTextObserver: Observer<String?> = Observer {
|
|
||||||
if (Requestor.instance.snackBarText.value != "")
|
|
||||||
{
|
|
||||||
val snackBarLength = if (preferenceStore.getBoolean("snackbarPersistent", true))
|
|
||||||
Snackbar.LENGTH_INDEFINITE
|
|
||||||
else Snackbar.LENGTH_LONG
|
|
||||||
snackBar = Snackbar.make(viewPager, "", snackBarLength)
|
|
||||||
|
|
||||||
if (snackBarLength == Snackbar.LENGTH_INDEFINITE)
|
|
||||||
snackBar.setAction("OK") {
|
|
||||||
snackBar.dismiss()
|
|
||||||
}
|
|
||||||
|
|
||||||
snackBar.behavior = BaseTransientBottomBar.Behavior().apply {
|
|
||||||
setSwipeDirection(BaseTransientBottomBar.Behavior.SWIPE_DIRECTION_ANY)
|
|
||||||
}
|
|
||||||
|
|
||||||
val snackBarView = snackBar.view
|
|
||||||
val textView =
|
|
||||||
snackBarView.findViewById(com.google.android.material.R.id.snackbar_text) as TextView
|
|
||||||
if (Requestor.instance.addRequestMeta != "")
|
|
||||||
textView.maxLines = 4
|
|
||||||
else
|
|
||||||
textView.maxLines = 2
|
|
||||||
snackBar.setText((it as CharSequence))
|
|
||||||
snackBar.show()
|
|
||||||
Requestor.instance.snackBarText.value = "" // resetting afterwards to avoid re-triggering it when we enter again the fragment
|
|
||||||
Requestor.instance.addRequestMeta = ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreateView(
|
override fun onCreateView(
|
||||||
inflater: LayoutInflater,
|
inflater: LayoutInflater,
|
||||||
container: ViewGroup?,
|
container: ViewGroup?,
|
||||||
|
|
@ -71,9 +28,8 @@ class SongsFragment : Fragment() {
|
||||||
viewPager = root.findViewById(R.id.tabPager)
|
viewPager = root.findViewById(R.id.tabPager)
|
||||||
adapter = SongsPagerAdapter(childFragmentManager, FragmentPagerAdapter.BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT)
|
adapter = SongsPagerAdapter(childFragmentManager, FragmentPagerAdapter.BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT)
|
||||||
adapter.addFragment(LastPlayedFragment.newInstance(), "last played")
|
adapter.addFragment(LastPlayedFragment.newInstance(), "last played")
|
||||||
adapter.addFragment(QueueFragment.newInstance(), "queue")
|
|
||||||
adapter.addFragment(RequestFragment.newInstance(), "request")
|
//adapter.addFragment(QueueFragment.newInstance(), "queue")
|
||||||
adapter.addFragment(FavoritesFragment.newInstance(), "Favorites")
|
|
||||||
|
|
||||||
viewPager.adapter = adapter
|
viewPager.adapter = adapter
|
||||||
|
|
||||||
|
|
@ -81,14 +37,8 @@ class SongsFragment : Fragment() {
|
||||||
tabLayout.setupWithViewPager(viewPager)
|
tabLayout.setupWithViewPager(viewPager)
|
||||||
Log.d(tag, "SongFragment view created")
|
Log.d(tag, "SongFragment view created")
|
||||||
|
|
||||||
Requestor.instance.snackBarText.observeForever(snackBarTextObserver)
|
|
||||||
|
|
||||||
return root
|
return root
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroyView() {
|
|
||||||
Requestor.instance.snackBarText.removeObserver(snackBarTextObserver)
|
|
||||||
super.onDestroyView()
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
@ -1,70 +0,0 @@
|
||||||
package fr.forum_thalie.tsumugi.ui.songs.queuelp
|
|
||||||
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.util.Log
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import androidx.fragment.app.Fragment
|
|
||||||
import androidx.lifecycle.Observer
|
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
|
||||||
import fr.forum_thalie.tsumugi.R
|
|
||||||
import fr.forum_thalie.tsumugi.playerstore.PlayerStore
|
|
||||||
import fr.forum_thalie.tsumugi.playerstore.Song
|
|
||||||
|
|
||||||
class QueueFragment : Fragment(){
|
|
||||||
private val lastPlayedFragmentTag = this::class.java.name
|
|
||||||
|
|
||||||
private lateinit var recyclerView: RecyclerView
|
|
||||||
private lateinit var viewAdapter: RecyclerView.Adapter<*>
|
|
||||||
private lateinit var viewManager: RecyclerView.LayoutManager
|
|
||||||
|
|
||||||
|
|
||||||
private val queueObserver = Observer<Boolean> {
|
|
||||||
Log.d(tag, lastPlayedFragmentTag + "queue changed")
|
|
||||||
viewAdapter.notifyDataSetChanged()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreateView(
|
|
||||||
inflater: LayoutInflater, container: ViewGroup?,
|
|
||||||
savedInstanceState: Bundle?
|
|
||||||
): View? {
|
|
||||||
// Inflate the layout for this fragment
|
|
||||||
val root = inflater.inflate(R.layout.fragment_last_played, container, false)
|
|
||||||
|
|
||||||
viewManager = LinearLayoutManager(context)
|
|
||||||
viewAdapter = SongAdaptater(
|
|
||||||
if (PlayerStore.instance.queue.isEmpty())
|
|
||||||
ArrayList<Song>(listOf((Song("No queue - "))))
|
|
||||||
else
|
|
||||||
PlayerStore.instance.queue
|
|
||||||
)
|
|
||||||
|
|
||||||
recyclerView = root.findViewById<RecyclerView>(R.id.queue_lp_recycler).apply {
|
|
||||||
// use this setting to improve performance if you know that changes
|
|
||||||
// in content do not change the layout size of the RecyclerView
|
|
||||||
setHasFixedSize(true)
|
|
||||||
|
|
||||||
// use a linear layout manager
|
|
||||||
layoutManager = viewManager
|
|
||||||
|
|
||||||
// specify an viewAdapter (see also next example)
|
|
||||||
adapter = viewAdapter
|
|
||||||
}
|
|
||||||
|
|
||||||
PlayerStore.instance.isQueueUpdated.observeForever(queueObserver)
|
|
||||||
|
|
||||||
return root
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDestroyView() {
|
|
||||||
PlayerStore.instance.isQueueUpdated.removeObserver(queueObserver)
|
|
||||||
super.onDestroyView()
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
@JvmStatic
|
|
||||||
fun newInstance() = QueueFragment()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,69 +0,0 @@
|
||||||
package fr.forum_thalie.tsumugi.ui.songs.request
|
|
||||||
|
|
||||||
import kotlin.math.exp
|
|
||||||
import kotlin.math.max
|
|
||||||
|
|
||||||
/*
|
|
||||||
//PHP cooldown calculator
|
|
||||||
|
|
||||||
function pretty_cooldown($lp, $lr, $rc) {
|
|
||||||
$delay = delay($rc);
|
|
||||||
$now = time();
|
|
||||||
$cd = intval(max($lp + $delay - $now, $lr + $delay - $now));
|
|
||||||
if ($cd <= 0)
|
|
||||||
return "Request";
|
|
||||||
$days = intdiv_1($cd, 86400);
|
|
||||||
$cd = $cd % 86400;
|
|
||||||
$hours = intdiv_1($cd, 3600);
|
|
||||||
$cd = $cd % 3600;
|
|
||||||
$minutes = intdiv_1($cd, 60);
|
|
||||||
$seconds = $cd % 60;
|
|
||||||
if ($days > 0)
|
|
||||||
return "Requestable in ".$days."d".$hours."h";
|
|
||||||
else if ($hours > 0)
|
|
||||||
return "Requestable in ".$hours."h".$minutes."m";
|
|
||||||
else if ($minutes > 0)
|
|
||||||
return "Requestable in ".$minutes."m".$seconds."s";
|
|
||||||
return "Request";
|
|
||||||
}
|
|
||||||
function requestable($lastplayed, $requests) {
|
|
||||||
$delay = delay($requests);
|
|
||||||
return (time() - $lastplayed) > $delay;
|
|
||||||
}
|
|
||||||
function delay($priority) {
|
|
||||||
// priority is 30 max
|
|
||||||
if ($priority > 30)
|
|
||||||
$priority = 30;
|
|
||||||
// between 0 and 7 return magic
|
|
||||||
if ($priority >= 0 and $priority <= 7)
|
|
||||||
$cd = -11057 * $priority * $priority + 172954 * $priority + 81720;
|
|
||||||
// if above that, return magic crazy numbers
|
|
||||||
else
|
|
||||||
$cd = (int) (599955 * exp(0.0372 * $priority) + 0.5);
|
|
||||||
return $cd / 2;
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|
||||||
// this function implements the magic delay used on R/a/dio website:
|
|
||||||
// https://github.com/R-a-dio/site/blob/develop/app/start/global.php#L125
|
|
||||||
// (Seriously guys, what were you thinking with these crazy magic numbers...)
|
|
||||||
fun delay(rawPriority: Int) : Int {
|
|
||||||
val priority = if (rawPriority > 30) 30 else rawPriority
|
|
||||||
val coolDown : Int =
|
|
||||||
if (priority in 0..7)
|
|
||||||
-11057 * priority * priority + 172954 * priority + 81720
|
|
||||||
else
|
|
||||||
(599955 * exp(0.0372 * (priority.toDouble()) + 0.5)).toInt()
|
|
||||||
return coolDown/2
|
|
||||||
}
|
|
||||||
|
|
||||||
// I tweaked this to report in a single point whether the song is requestable or not
|
|
||||||
fun coolDown(lastPlayed: Int?, lastRequest: Int?, requestsNbr: Int?) : Long {
|
|
||||||
if (requestsNbr == null || lastPlayed == null || lastRequest == null)
|
|
||||||
return Long.MAX_VALUE // maximum positive value : the song won't be requestable
|
|
||||||
|
|
||||||
val delay = delay(requestsNbr)
|
|
||||||
val now = (System.currentTimeMillis() / 1000)
|
|
||||||
return max(lastPlayed, lastRequest) + delay - now
|
|
||||||
// if coolDown < 0, the song is requestable.
|
|
||||||
}
|
|
||||||
|
|
@ -1,132 +0,0 @@
|
||||||
package fr.forum_thalie.tsumugi.ui.songs.request
|
|
||||||
|
|
||||||
import android.os.Build
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.util.Log
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import android.widget.SearchView
|
|
||||||
import android.widget.TextView
|
|
||||||
import androidx.appcompat.widget.AppCompatButton
|
|
||||||
import androidx.fragment.app.Fragment
|
|
||||||
import androidx.lifecycle.Observer
|
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
|
||||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
|
||||||
import fr.forum_thalie.tsumugi.*
|
|
||||||
|
|
||||||
|
|
||||||
class FavoritesFragment : Fragment() {
|
|
||||||
|
|
||||||
private lateinit var recyclerView: RecyclerView
|
|
||||||
private lateinit var viewAdapter: RecyclerView.Adapter<*>
|
|
||||||
private lateinit var viewManager: RecyclerView.LayoutManager
|
|
||||||
private lateinit var searchView: SearchView
|
|
||||||
private lateinit var root: View
|
|
||||||
private lateinit var recyclerSwipe: SwipeRefreshLayout
|
|
||||||
|
|
||||||
private val favoritesSongObserver : Observer<Boolean> = Observer {
|
|
||||||
viewAdapter.notifyDataSetChanged()
|
|
||||||
createView(isCallback = true) // force-re-create the view, but do not call again the initFavorites (avoid callback loop)
|
|
||||||
recyclerSwipe.isRefreshing = false // disable refreshing animation. Needs to be done manually...
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreateView(
|
|
||||||
inflater: LayoutInflater,
|
|
||||||
container: ViewGroup?,
|
|
||||||
savedInstanceState: Bundle?
|
|
||||||
): View? {
|
|
||||||
super.onCreateView(inflater, container, savedInstanceState)
|
|
||||||
root = inflater.inflate(R.layout.fragment_request, container, false)
|
|
||||||
|
|
||||||
return createView()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun createView(isCallback: Boolean = false) : View?
|
|
||||||
{
|
|
||||||
|
|
||||||
viewAdapter = RequestSongAdapter(Requestor.instance.favoritesSongArray)
|
|
||||||
|
|
||||||
val listener : SearchView.OnQueryTextListener = object : SearchView.OnQueryTextListener{
|
|
||||||
override fun onQueryTextSubmit(query: String?): Boolean {
|
|
||||||
// do nothing
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
override fun onQueryTextChange(newText: String?): Boolean {
|
|
||||||
(viewAdapter as RequestSongAdapter).filter(newText ?: "")
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
searchView = root.findViewById(R.id.searchBox)
|
|
||||||
searchView.queryHint = "Search filter..."
|
|
||||||
searchView.setOnQueryTextListener(listener)
|
|
||||||
viewManager = LinearLayoutManager(context)
|
|
||||||
recyclerView = root.findViewById<RecyclerView>(R.id.request_recycler).apply {
|
|
||||||
// use this setting to improve performance if you know that changes
|
|
||||||
// in content do not change the layout size of the RecyclerView
|
|
||||||
setHasFixedSize(true)
|
|
||||||
|
|
||||||
// use a linear layout manager
|
|
||||||
layoutManager = viewManager
|
|
||||||
|
|
||||||
// specify an viewAdapter (see also next example)
|
|
||||||
adapter = viewAdapter
|
|
||||||
}
|
|
||||||
|
|
||||||
val noUserNameText : TextView = root.findViewById(R.id.noUserNameText)
|
|
||||||
|
|
||||||
recyclerSwipe = root.findViewById(R.id.recyclerSwipe) as SwipeRefreshLayout
|
|
||||||
recyclerSwipe.setOnRefreshListener {
|
|
||||||
val userName1 = preferenceStore.getString("userName", null)
|
|
||||||
Log.d(tag,"userName = $userName1")
|
|
||||||
if (userName1 != null && !userName1.isBlank())
|
|
||||||
{
|
|
||||||
noUserNameText.visibility = View.GONE
|
|
||||||
Requestor.instance.initFavorites()
|
|
||||||
} else {
|
|
||||||
noUserNameText.visibility = View.VISIBLE
|
|
||||||
recyclerSwipe.isRefreshing = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
val userName1 = preferenceStore.getString("userName", null)
|
|
||||||
Log.d(tag,"userName = $userName1")
|
|
||||||
if (userName1 != null && !userName1.isBlank())
|
|
||||||
{
|
|
||||||
noUserNameText.visibility = View.GONE
|
|
||||||
if (!isCallback) // avoid callback loop if called from the Observer.
|
|
||||||
Requestor.instance.initFavorites()
|
|
||||||
} else {
|
|
||||||
noUserNameText.visibility = View.VISIBLE
|
|
||||||
recyclerSwipe.isRefreshing = false
|
|
||||||
}
|
|
||||||
|
|
||||||
val raFButton : AppCompatButton = root.findViewById(R.id.ra_f_button)
|
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) // for API21+ Material Design makes ripples on the button.
|
|
||||||
raFButton.supportBackgroundTintList = colorGreenList
|
|
||||||
else // But on API20- no Material Design support, so we add some more color when clicked
|
|
||||||
raFButton.supportBackgroundTintList = colorGreenListCompat
|
|
||||||
raFButton.isEnabled = true
|
|
||||||
raFButton.isClickable = true
|
|
||||||
raFButton.setOnClickListener {
|
|
||||||
val s = Requestor.instance.raF()
|
|
||||||
Requestor.instance.snackBarText.value = ""
|
|
||||||
Requestor.instance.addRequestMeta = "Request: ${s.artist.value} - ${s.title.value}\n"
|
|
||||||
Requestor.instance.request(s.id)
|
|
||||||
}
|
|
||||||
raFButton.visibility = View.VISIBLE
|
|
||||||
|
|
||||||
Requestor.instance.isFavoritesUpdated.observe(viewLifecycleOwner, favoritesSongObserver)
|
|
||||||
return root
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
@JvmStatic
|
|
||||||
fun newInstance() = FavoritesFragment()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,81 +0,0 @@
|
||||||
package fr.forum_thalie.tsumugi.ui.songs.request
|
|
||||||
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.util.Log
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import android.widget.SearchView
|
|
||||||
import androidx.fragment.app.Fragment
|
|
||||||
import androidx.lifecycle.Observer
|
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
|
||||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
|
||||||
import fr.forum_thalie.tsumugi.R
|
|
||||||
|
|
||||||
class RequestFragment : Fragment() {
|
|
||||||
|
|
||||||
private lateinit var recyclerView: RecyclerView
|
|
||||||
private lateinit var viewAdapter: RecyclerView.Adapter<*>
|
|
||||||
private lateinit var viewManager: RecyclerView.LayoutManager
|
|
||||||
private lateinit var searchView: SearchView
|
|
||||||
|
|
||||||
private val listener : SearchView.OnQueryTextListener = object : SearchView.OnQueryTextListener{
|
|
||||||
override fun onQueryTextSubmit(query: String?): Boolean {
|
|
||||||
if (query == null || query.isEmpty())
|
|
||||||
Requestor.instance.snackBarText.value = "Field is empty, no search possible."
|
|
||||||
else
|
|
||||||
Requestor.instance.search(query)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
override fun onQueryTextChange(newText: String?): Boolean {
|
|
||||||
if (newText == "")
|
|
||||||
{
|
|
||||||
Requestor.instance.reset()
|
|
||||||
viewAdapter.notifyDataSetChanged() // this is to remove the "Load more" button
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private val requestSongObserver = Observer<Boolean> {
|
|
||||||
Log.d(tag, "request song list changed")
|
|
||||||
viewAdapter.notifyDataSetChanged()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreateView(
|
|
||||||
inflater: LayoutInflater, container: ViewGroup?,
|
|
||||||
savedInstanceState: Bundle?
|
|
||||||
): View? {
|
|
||||||
// Inflate the layout for this fragment
|
|
||||||
val root = inflater.inflate(R.layout.fragment_request, container, false)
|
|
||||||
|
|
||||||
val recyclerSwipe = root.findViewById(R.id.recyclerSwipe) as SwipeRefreshLayout
|
|
||||||
recyclerSwipe.isEnabled = false // don't need to pull-to-refresh for Request
|
|
||||||
|
|
||||||
searchView = root.findViewById(R.id.searchBox)
|
|
||||||
searchView.setOnQueryTextListener(listener)
|
|
||||||
|
|
||||||
viewManager = LinearLayoutManager(context)
|
|
||||||
viewAdapter = RequestSongAdapter(Requestor.instance.requestSongArray)
|
|
||||||
|
|
||||||
recyclerView = root.findViewById<RecyclerView>(R.id.request_recycler).apply {
|
|
||||||
// use this setting to improve performance if you know that changes
|
|
||||||
// in content do not change the layout size of the RecyclerView
|
|
||||||
setHasFixedSize(true)
|
|
||||||
|
|
||||||
// use a linear layout manager
|
|
||||||
layoutManager = viewManager
|
|
||||||
|
|
||||||
// specify an viewAdapter (see also next example)
|
|
||||||
adapter = viewAdapter
|
|
||||||
}
|
|
||||||
Requestor.instance.isRequestResultUpdated.observe(viewLifecycleOwner, requestSongObserver)
|
|
||||||
return root
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
@JvmStatic
|
|
||||||
fun newInstance() = RequestFragment()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,42 +0,0 @@
|
||||||
package fr.forum_thalie.tsumugi.ui.songs.request
|
|
||||||
|
|
||||||
import fr.forum_thalie.tsumugi.playerstore.Song
|
|
||||||
import org.json.JSONObject
|
|
||||||
|
|
||||||
class RequestResponse(jsonResponse: JSONObject) {
|
|
||||||
//val total: Int = jsonResponse.getInt("total")
|
|
||||||
//val perPage: Int = jsonResponse.getInt("per_page") // should stay 20 but in any case...
|
|
||||||
val currentPage: Int = jsonResponse.getInt("current_page")
|
|
||||||
val lastPage: Int = jsonResponse.getInt("last_page")
|
|
||||||
//val fromNbr: Int = jsonResponse.getInt("from")
|
|
||||||
//val toNbr: Int = jsonResponse.getInt("to")
|
|
||||||
var songs : ArrayList<Song> = ArrayList()
|
|
||||||
|
|
||||||
init {
|
|
||||||
val songList = jsonResponse.getJSONArray("data")
|
|
||||||
for (i in 0 until songList.length())
|
|
||||||
{
|
|
||||||
|
|
||||||
val title = (songList[i] as JSONObject).getString("title")
|
|
||||||
val artist = (songList[i] as JSONObject).getString("artist")
|
|
||||||
val id = (songList[i] as JSONObject).getInt("id")
|
|
||||||
|
|
||||||
val s = Song("", id)
|
|
||||||
s.title.value = title
|
|
||||||
s.artist.value = artist
|
|
||||||
s.isRequestable = (songList[i] as JSONObject).getBoolean("requestable")
|
|
||||||
// TODO add the time before being requestable.
|
|
||||||
songs.add(s)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun toString(): String {
|
|
||||||
var s = ""
|
|
||||||
for (i in 0 until songs.size)
|
|
||||||
{
|
|
||||||
s += (songs[i].artist.value + " - " + songs[i].title.value + " | ")
|
|
||||||
}
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -1,148 +0,0 @@
|
||||||
package fr.forum_thalie.tsumugi.ui.songs.request
|
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.os.Build
|
|
||||||
import android.util.TypedValue
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import android.widget.TextView
|
|
||||||
import androidx.constraintlayout.widget.ConstraintLayout
|
|
||||||
import androidx.core.widget.TextViewCompat
|
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
|
||||||
import fr.forum_thalie.tsumugi.*
|
|
||||||
import fr.forum_thalie.tsumugi.playerstore.Song
|
|
||||||
import kotlinx.android.synthetic.main.request_song_view.view.*
|
|
||||||
import android.view.View
|
|
||||||
import kotlinx.android.synthetic.main.button_load_more.view.*
|
|
||||||
import android.R.attr.name
|
|
||||||
import android.text.method.TextKeyListener.clear
|
|
||||||
import android.util.Log
|
|
||||||
import java.util.*
|
|
||||||
import kotlin.collections.ArrayList
|
|
||||||
|
|
||||||
|
|
||||||
class RequestSongAdapter(private val dataSet: ArrayList<Song>
|
|
||||||
/*,
|
|
||||||
context: Context,
|
|
||||||
resource: Int,
|
|
||||||
objects: Array<out Song>*/
|
|
||||||
) : RecyclerView.Adapter<RequestSongAdapter.MyViewHolder>() /*ArrayAdapter<Song>(context, resource, objects)*/ {
|
|
||||||
|
|
||||||
private val viewTypeCell = 1 // normal cell with song and request button
|
|
||||||
private val viewTypeFooter = 2 // the bottom cell should be the "load more" button whenever needed
|
|
||||||
|
|
||||||
// Provide a reference to the views for each data item
|
|
||||||
// Complex data items may need more than one view per item, and
|
|
||||||
// you provide access to all the views for a data item in a view holder.
|
|
||||||
// Each data item is just a string in this case that is shown in a TextView.
|
|
||||||
class MyViewHolder(view: ConstraintLayout) : RecyclerView.ViewHolder(view)
|
|
||||||
|
|
||||||
|
|
||||||
// Create new views (invoked by the layout manager)
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup,
|
|
||||||
viewType: Int): MyViewHolder {
|
|
||||||
// create a new view
|
|
||||||
|
|
||||||
val view =
|
|
||||||
if (viewType == viewTypeCell)
|
|
||||||
LayoutInflater.from(parent.context).inflate(R.layout.request_song_view, parent, false) as ConstraintLayout
|
|
||||||
else
|
|
||||||
LayoutInflater.from(parent.context).inflate(R.layout.button_load_more, parent, false) as ConstraintLayout
|
|
||||||
|
|
||||||
// set the view's size, margins, paddings and layout parameters
|
|
||||||
//...
|
|
||||||
return MyViewHolder(view)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Replace the contents of a view (invoked by the layout manager)
|
|
||||||
@SuppressLint("SetTextI18n")
|
|
||||||
override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
|
|
||||||
|
|
||||||
if (itemCount <= 1)
|
|
||||||
{
|
|
||||||
// in any case, if there's nothing, don't display the loadMore button!!
|
|
||||||
holder.itemView.loadMoreButton.visibility = View.GONE
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (holder.itemViewType == viewTypeFooter)
|
|
||||||
{
|
|
||||||
if (Requestor.instance.isLoadMoreVisible)
|
|
||||||
holder.itemView.loadMoreButton.visibility = View.VISIBLE
|
|
||||||
else
|
|
||||||
holder.itemView.loadMoreButton.visibility = View.GONE
|
|
||||||
holder.itemView.loadMoreButton.text = "Load more results"
|
|
||||||
holder.itemView.loadMoreButton.setOnClickListener{
|
|
||||||
Requestor.instance.loadMore()
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
val artist = holder.itemView.findViewById<TextView>(R.id.request_song_artist)
|
|
||||||
val title = holder.itemView.findViewById<TextView>(R.id.request_song_title)
|
|
||||||
val button = holder.itemView.request_button
|
|
||||||
|
|
||||||
if (dataSet[position].isRequestable) {
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) // for API21+ Material Design makes ripples on the button.
|
|
||||||
button.supportBackgroundTintList = colorGreenList
|
|
||||||
else // But on API20- no Material Design support, so we add some more color when clicked
|
|
||||||
button.supportBackgroundTintList = colorGreenListCompat
|
|
||||||
button.isEnabled = true
|
|
||||||
button.isClickable = true
|
|
||||||
button.setOnClickListener {
|
|
||||||
Requestor.instance.request(dataSet[position].id)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
button.supportBackgroundTintList = colorRedList
|
|
||||||
button.isEnabled = false
|
|
||||||
button.isClickable = false
|
|
||||||
}
|
|
||||||
|
|
||||||
TextViewCompat.setAutoSizeTextTypeUniformWithConfiguration(
|
|
||||||
button,2, 24, 2, TypedValue.COMPLEX_UNIT_SP)
|
|
||||||
artist.text = dataSet[position].artist.value
|
|
||||||
title.text = dataSet[position].title.value
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return the size of your dataset (invoked by the layout manager)
|
|
||||||
override fun getItemCount() = dataSet.size + 1 // add 1 for the "Load more results" button
|
|
||||||
|
|
||||||
override fun getItemViewType(position: Int): Int {
|
|
||||||
return if (position == dataSet.size) viewTypeFooter else viewTypeCell
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
|
|
||||||
// create a new view
|
|
||||||
val view = LayoutInflater.from(parent.context)
|
|
||||||
.inflate(R.layout.song_view, parent, false) as ConstraintLayout
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|
||||||
// a filtering function. As naive as it could be, but it should work.
|
|
||||||
private val dataSetOrig = ArrayList<Song>()
|
|
||||||
init {
|
|
||||||
dataSetOrig.addAll(dataSet)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun filter(entry: String) {
|
|
||||||
var text = entry
|
|
||||||
dataSet.clear()
|
|
||||||
Log.d(tag, "entering filter")
|
|
||||||
if (text.isEmpty()) {
|
|
||||||
dataSet.addAll(dataSetOrig)
|
|
||||||
} else {
|
|
||||||
text = text.toLowerCase(locale = Locale.ROOT)
|
|
||||||
for (item in dataSetOrig) {
|
|
||||||
Log.d(tag, "$text, ${item.artist.value!!.toLowerCase(locale = Locale.ROOT)}, ${item.title.value!!.toLowerCase(locale = Locale.ROOT)}")
|
|
||||||
if (item.artist.value!!.toLowerCase(locale = Locale.ROOT).contains(text) ||
|
|
||||||
item.title.value!!.toLowerCase(locale = Locale.ROOT).contains(text)) {
|
|
||||||
dataSet.add(item)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
notifyDataSetChanged()
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
@ -1,301 +0,0 @@
|
||||||
package fr.forum_thalie.tsumugi.ui.songs.request
|
|
||||||
|
|
||||||
import android.util.Log
|
|
||||||
import androidx.lifecycle.MutableLiveData
|
|
||||||
import fr.forum_thalie.tsumugi.ActionOnError
|
|
||||||
import fr.forum_thalie.tsumugi.Async
|
|
||||||
import fr.forum_thalie.tsumugi.playerstore.Song
|
|
||||||
import fr.forum_thalie.tsumugi.preferenceStore
|
|
||||||
import fr.forum_thalie.tsumugi.tag
|
|
||||||
import org.json.JSONArray
|
|
||||||
import org.json.JSONException
|
|
||||||
import org.json.JSONObject
|
|
||||||
import java.io.BufferedReader
|
|
||||||
import java.io.IOException
|
|
||||||
import java.io.InputStreamReader
|
|
||||||
import java.net.CookieHandler
|
|
||||||
import java.net.CookieManager
|
|
||||||
import java.net.MalformedURLException
|
|
||||||
import java.net.URL
|
|
||||||
import java.util.*
|
|
||||||
import java.util.regex.Pattern
|
|
||||||
|
|
||||||
import javax.net.ssl.HttpsURLConnection
|
|
||||||
import kotlin.collections.ArrayList
|
|
||||||
import kotlin.random.Random
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Requests a song via the website's API
|
|
||||||
*
|
|
||||||
* We scrape the website for a CSRF token and POST it to /request/ endpoint with
|
|
||||||
* the song id
|
|
||||||
*
|
|
||||||
* Created by Kethsar on 1/2/2017.
|
|
||||||
* Converted to Kotlin and adapted by Yattoz on 05 Nov. 2019
|
|
||||||
*/
|
|
||||||
|
|
||||||
class Requestor {
|
|
||||||
var addRequestMeta: String = ""
|
|
||||||
private val cookieManager: CookieManager = CookieManager()
|
|
||||||
private val requestUrl = "https://r-a-d.io/request/%1\$d"
|
|
||||||
private val searchUrl = "https://r-a-d.io/api/search/%1s?page=%2\$d"
|
|
||||||
private val favoritesUrl = "https://r-a-d.io/faves/%1s?dl=true"
|
|
||||||
private val songThresholdStep = 50
|
|
||||||
private var songThreshold = songThresholdStep
|
|
||||||
private var localQuery = ""
|
|
||||||
|
|
||||||
private var token: String? = null
|
|
||||||
val snackBarText : MutableLiveData<String?> = MutableLiveData()
|
|
||||||
private var responseArray : ArrayList<RequestResponse> = ArrayList()
|
|
||||||
val requestSongArray : ArrayList<Song> = ArrayList()
|
|
||||||
val favoritesSongArray : ArrayList<Song> = ArrayList()
|
|
||||||
val isRequestResultUpdated : MutableLiveData<Boolean> = MutableLiveData()
|
|
||||||
val isFavoritesUpdated : MutableLiveData<Boolean> = MutableLiveData()
|
|
||||||
var isLoadMoreVisible: Boolean = false
|
|
||||||
|
|
||||||
|
|
||||||
init {
|
|
||||||
snackBarText.value = ""
|
|
||||||
isRequestResultUpdated.value = false
|
|
||||||
isFavoritesUpdated.value = false
|
|
||||||
isLoadMoreVisible = false
|
|
||||||
}
|
|
||||||
|
|
||||||
fun initFavorites(userName : String? = preferenceStore.getString("userName", null)){
|
|
||||||
Log.d(tag, "initializing favorites")
|
|
||||||
favoritesSongArray.clear()
|
|
||||||
if (userName == null)
|
|
||||||
{
|
|
||||||
// Display is done by default in the XML.
|
|
||||||
Log.d(tag, "no user name set for favorites")
|
|
||||||
isFavoritesUpdated.value = true
|
|
||||||
return
|
|
||||||
}
|
|
||||||
val favoritesUserUrl = String.format(Locale.getDefault(), favoritesUrl, userName)
|
|
||||||
val scrapeFavorites : (Any?) -> JSONArray = {
|
|
||||||
JSONArray(URL(favoritesUserUrl).readText())
|
|
||||||
}
|
|
||||||
val postFavorites : (Any?) -> Unit = {
|
|
||||||
val res = it as JSONArray
|
|
||||||
for (i in 0 until (res).length())
|
|
||||||
{
|
|
||||||
val item = res.getJSONObject(i)
|
|
||||||
val artistTitle = item.getString("meta")
|
|
||||||
val id : Int? = if (item.isNull("tracks_id"))
|
|
||||||
null
|
|
||||||
else
|
|
||||||
item.getInt("tracks_id")
|
|
||||||
|
|
||||||
val lastRequested : Int? = if (item.isNull("lastrequested")) null else item.getInt("lastrequested")
|
|
||||||
val lastPlayed : Int? = if (item.isNull("lastplayed")) null else item.getInt("lastplayed")
|
|
||||||
val requestCount : Int? = if (item.isNull("requestcount")) null else item.getInt("requestcount")
|
|
||||||
val isRequestable = (coolDown(lastPlayed, lastRequested, requestCount) < 0)
|
|
||||||
//Log.d(tag, "val : $id")
|
|
||||||
favoritesSongArray.add(Song(artistTitle, id ?: 0, isRequestable))
|
|
||||||
}
|
|
||||||
Log.d(tag, "favorites : $favoritesSongArray")
|
|
||||||
isFavoritesUpdated.value = true
|
|
||||||
}
|
|
||||||
Async(scrapeFavorites, postFavorites, ActionOnError.NOTIFY)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun search(query: String)
|
|
||||||
{
|
|
||||||
responseArray.clear()
|
|
||||||
requestSongArray.clear()
|
|
||||||
localQuery = query
|
|
||||||
searchPage(query, 1) // the searchPage function is recursive to get all pages.
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun searchPage(query: String, pageNumber : Int)
|
|
||||||
{
|
|
||||||
val searchURL = String.format(Locale.getDefault(), searchUrl, query, pageNumber)
|
|
||||||
val scrape : (Any?) -> JSONObject = {
|
|
||||||
val res = URL(searchURL).readText()
|
|
||||||
val json = JSONObject(res)
|
|
||||||
json
|
|
||||||
}
|
|
||||||
val post : (Any?) -> Unit = {
|
|
||||||
val response = RequestResponse(it as JSONObject)
|
|
||||||
|
|
||||||
responseArray.add(response)
|
|
||||||
for (i in 0 until response.songs.size)
|
|
||||||
{
|
|
||||||
requestSongArray.add(response.songs[i])
|
|
||||||
}
|
|
||||||
isRequestResultUpdated.value = true
|
|
||||||
if (requestSongArray.size >= songThreshold)
|
|
||||||
{
|
|
||||||
isLoadMoreVisible = true
|
|
||||||
|
|
||||||
} else {
|
|
||||||
if (response.currentPage < response.lastPage)
|
|
||||||
searchPage(query, pageNumber + 1) // recursive call to get the next page
|
|
||||||
else
|
|
||||||
finishSearch()
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
Async(scrape, post, ActionOnError.NOTIFY)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun finishSearch()
|
|
||||||
{
|
|
||||||
isLoadMoreVisible = false
|
|
||||||
}
|
|
||||||
|
|
||||||
fun reset()
|
|
||||||
{
|
|
||||||
requestSongArray.clear()
|
|
||||||
responseArray.clear()
|
|
||||||
isRequestResultUpdated.value = false
|
|
||||||
songThreshold = songThresholdStep
|
|
||||||
}
|
|
||||||
|
|
||||||
fun loadMore()
|
|
||||||
{
|
|
||||||
songThreshold += songThresholdStep
|
|
||||||
searchPage(localQuery, responseArray.last().currentPage + 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Scrape the website for the CSRF token required for requesting
|
|
||||||
* scrapeToken and postToken are the two lambas run by the Async() class.
|
|
||||||
*/
|
|
||||||
|
|
||||||
private val scrapeToken : (Any?) -> Any? = {
|
|
||||||
val radioSearchUrl = "https://r-a-d.io/search"
|
|
||||||
var searchURL: URL? = null
|
|
||||||
var retVal: String? = null
|
|
||||||
var reader: BufferedReader? = null
|
|
||||||
|
|
||||||
CookieHandler.setDefault(cookieManager) // it[0] ??
|
|
||||||
|
|
||||||
try {
|
|
||||||
searchURL = URL(radioSearchUrl)
|
|
||||||
} catch (e: MalformedURLException) {
|
|
||||||
e.printStackTrace()
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
reader = BufferedReader(InputStreamReader(searchURL!!.openStream(), "UTF-8"))
|
|
||||||
var line: String?
|
|
||||||
line = reader.readLine()
|
|
||||||
while (line != null)
|
|
||||||
{
|
|
||||||
line = line.trim { it <= ' ' }
|
|
||||||
val p = Pattern.compile("value=\"(\\w+)\"")
|
|
||||||
val m = p.matcher(line)
|
|
||||||
|
|
||||||
if (line.startsWith("<form")) {
|
|
||||||
if (m.find()) {
|
|
||||||
retVal = m.group(1)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
line = reader.readLine()
|
|
||||||
}
|
|
||||||
} catch (e: IOException) {
|
|
||||||
e.printStackTrace()
|
|
||||||
} finally {
|
|
||||||
if (reader != null) try {
|
|
||||||
reader.close()
|
|
||||||
} catch (ignored: IOException) {
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
retVal
|
|
||||||
}
|
|
||||||
|
|
||||||
private val postToken : (Any?) -> (Unit) = {
|
|
||||||
token = it as String?
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Request the song with the CSRF token that was scraped
|
|
||||||
*/
|
|
||||||
private val requestSong: (Any?) -> Any? = {
|
|
||||||
val reqString = it as String
|
|
||||||
var response = ""
|
|
||||||
|
|
||||||
try {
|
|
||||||
val reqURL = URL(reqString)
|
|
||||||
val conn = reqURL.openConnection() as HttpsURLConnection
|
|
||||||
val tokenObject = JSONObject()
|
|
||||||
|
|
||||||
tokenObject.put("_token", token)
|
|
||||||
val requestBytes = tokenObject.toString().toByteArray()
|
|
||||||
|
|
||||||
conn.requestMethod = "POST"
|
|
||||||
conn.doOutput = true
|
|
||||||
conn.doInput = true
|
|
||||||
conn.setChunkedStreamingMode(0)
|
|
||||||
conn.setRequestProperty("Content-Type", "application/json")
|
|
||||||
|
|
||||||
val os = conn.outputStream
|
|
||||||
os.write(requestBytes)
|
|
||||||
|
|
||||||
val responseCode = conn.responseCode
|
|
||||||
|
|
||||||
if (responseCode == HttpsURLConnection.HTTP_OK) {
|
|
||||||
var line: String?
|
|
||||||
val br = BufferedReader(InputStreamReader(
|
|
||||||
conn.inputStream))
|
|
||||||
line = br.readLine()
|
|
||||||
while (line != null) {
|
|
||||||
response += line
|
|
||||||
line = br.readLine()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
response += ""
|
|
||||||
}
|
|
||||||
} catch (ex: IOException) {
|
|
||||||
ex.printStackTrace()
|
|
||||||
} catch (ex: JSONException) {
|
|
||||||
ex.printStackTrace()
|
|
||||||
}
|
|
||||||
|
|
||||||
response
|
|
||||||
}
|
|
||||||
|
|
||||||
private val postSong : (Any?) -> (Unit) = {
|
|
||||||
val response = JSONObject(it as String)
|
|
||||||
val key = response.names()!!.get(0) as String
|
|
||||||
val value = response.getString(key)
|
|
||||||
|
|
||||||
snackBarText.postValue(addRequestMeta + value)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
fun request(songID: Int?) {
|
|
||||||
val requestSongUrl = String.format(requestUrl, songID!!)
|
|
||||||
if (token == null) {
|
|
||||||
Async(scrapeToken, postToken, ActionOnError.NOTIFY)
|
|
||||||
}
|
|
||||||
Async(requestSong, postSong, ActionOnError.NOTIFY, requestSongUrl)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun raF() : Song {
|
|
||||||
// request a random favorite song. HELL YEAH
|
|
||||||
val requestableSongArray = ArrayList<Song>()
|
|
||||||
for (i in 0 until favoritesSongArray.size)
|
|
||||||
{
|
|
||||||
if (favoritesSongArray[i].isRequestable && (favoritesSongArray[i].id ?: 0) > 0)
|
|
||||||
requestableSongArray.add(favoritesSongArray[i])
|
|
||||||
}
|
|
||||||
return if (requestableSongArray.isNotEmpty()) {
|
|
||||||
val songNbr = Random(System.currentTimeMillis()).nextInt(1, requestableSongArray.size)
|
|
||||||
requestableSongArray[songNbr]
|
|
||||||
} else {
|
|
||||||
Song("No song requestable - ")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
val instance by lazy {
|
|
||||||
Requestor()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -1,13 +0,0 @@
|
||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:width="24dp"
|
|
||||||
android:height="24dp"
|
|
||||||
android:viewportWidth="26.086956"
|
|
||||||
android:viewportHeight="26.086956"
|
|
||||||
android:tint="#FFFFFF">
|
|
||||||
<group android:translateX="1.0434783"
|
|
||||||
android:translateY="1.0434783">
|
|
||||||
<path
|
|
||||||
android:fillColor="#FF000000"
|
|
||||||
android:pathData="M3.24,6.15C2.51,6.43 2,7.17 2,8v12c0,1.1 0.89,2 2,2h16c1.11,0 2,-0.9 2,-2L22,8c0,-1.11 -0.89,-2 -2,-2L8.3,6l8.26,-3.34L15.88,1 3.24,6.15zM7,20c-1.66,0 -3,-1.34 -3,-3s1.34,-3 3,-3 3,1.34 3,3 -1.34,3 -3,3zM20,12h-2v-2h-2v2L4,12L4,8h16v4z"/>
|
|
||||||
</group>
|
|
||||||
</vector>
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:width="24dp"
|
|
||||||
android:height="24dp"
|
|
||||||
android:viewportWidth="24"
|
|
||||||
android:viewportHeight="24"
|
|
||||||
android:tint="#FFFFFF"
|
|
||||||
android:alpha="0.8">
|
|
||||||
<path
|
|
||||||
android:fillColor="#FF000000"
|
|
||||||
android:pathData="M20,2L4,2c-1.1,0 -1.99,0.9 -1.99,2L2,22l4,-4h14c1.1,0 2,-0.9 2,-2L22,4c0,-1.1 -0.9,-2 -2,-2zM9,11L7,11L7,9h2v2zM13,11h-2L11,9h2v2zM17,11h-2L15,9h2v2z"/>
|
|
||||||
</vector>
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:width="24dp"
|
|
||||||
android:height="24dp"
|
|
||||||
android:viewportWidth="24"
|
|
||||||
android:viewportHeight="24"
|
|
||||||
android:tint="#FFFFFF"
|
|
||||||
android:alpha="0.8">
|
|
||||||
<path
|
|
||||||
android:fillColor="#FF000000"
|
|
||||||
android:pathData="M19,4L5,4c-1.11,0 -2,0.9 -2,2v12c0,1.1 0.89,2 2,2h4v-2L5,18L5,8h14v10h-4v2h4c1.1,0 2,-0.9 2,-2L21,6c0,-1.1 -0.89,-2 -2,-2zM12,10l-4,4h3v6h2v-6h3l-4,-4z"/>
|
|
||||||
</vector>
|
|
||||||
|
Before Width: | Height: | Size: 204 B |
|
Before Width: | Height: | Size: 213 B |
|
Before Width: | Height: | Size: 402 B After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 143 B |
|
Before Width: | Height: | Size: 165 B |
|
Before Width: | Height: | Size: 318 B After Width: | Height: | Size: 659 B |
|
|
@ -1,11 +1,11 @@
|
||||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android"
|
<layer-list xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
android:opacity="opaque">
|
android:opacity="opaque">
|
||||||
<!-- The background color, preferably the same as your normal theme -->
|
<!-- The background color, preferably the same as your normal theme -->
|
||||||
<item android:drawable="@color/colorPrimaryDark"/>
|
<item android:drawable="@color/colorAccent"/>
|
||||||
<!-- Your product logo - 144dp color version of your app icon -->
|
<!-- Your product logo - 144dp color version of your app icon -->
|
||||||
|
|
||||||
<item
|
<item
|
||||||
android:drawable="@drawable/ic_radio_logo_round"
|
android:drawable="@drawable/ic_tsumugi_lineart"
|
||||||
android:gravity="center"
|
android:gravity="center"
|
||||||
android:height="144dp"
|
android:height="144dp"
|
||||||
android:width="144dp"
|
android:width="144dp"
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 236 B |
|
Before Width: | Height: | Size: 249 B |
|
Before Width: | Height: | Size: 494 B After Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 321 B |
|
Before Width: | Height: | Size: 353 B |
|
Before Width: | Height: | Size: 698 B After Width: | Height: | Size: 2.7 KiB |
|
After Width: | Height: | Size: 4.1 KiB |
|
Before Width: | Height: | Size: 13 KiB |
|
|
@ -1,18 +0,0 @@
|
||||||
<vector xmlns:tools="http://schemas.android.com/tools"
|
|
||||||
android:height="144dp" android:viewportHeight="130"
|
|
||||||
android:viewportWidth="130" android:width="144dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
|
||||||
<path android:fillAlpha="1" android:fillColor="#f5f5f5"
|
|
||||||
android:pathData="m56.497,129.184c-3.016,-0.459 -7.77,-1.474 -8.229,-1.758 -0.303,-0.187 -0.37,-1.586 -0.37,-7.713 0,-7.406 0.225,-10.317 1.041,-13.463 0.204,-0.786 0.324,-1.476 0.266,-1.534 -0.057,-0.057 -0.956,0.699 -1.996,1.68 -4.188,3.95 -10.293,6.952 -18.619,9.154l-2.904,0.768 -1.065,-0.722C22.693,114.289 15.627,107.008 13.48,104.118 11.15,100.982 8.714,97.085 8.941,96.858c0.081,-0.081 1.319,0.293 2.751,0.832 3.752,1.411 9.219,2.726 12.833,3.088 13.515,1.351 23.624,-5.425 28.35,-19.003 0.84,-2.414 1.934,-6.922 1.725,-7.106 -0.048,-0.043 -0.624,-0.347 -1.279,-0.677 -3.497,-1.761 -8.052,-5.635 -10.005,-8.508 -2.694,-3.963 -4.527,-9.389 -4.971,-14.717l-0.175,-2.103 -1.686,-0.135c-4.393,-0.353 -7.049,-3.319 -7.431,-8.299 -0.346,-4.51 2.339,-8.89 6.315,-10.301 1.915,-0.68 4.271,-0.79 5.647,-0.264 1.357,0.518 3.329,2.354 3.832,3.568 0.185,0.447 0.446,0.813 0.579,0.813 0.133,0 0.744,-0.491 1.357,-1.091 1.998,-1.957 7.303,-5.111 7.786,-4.629 0.173,0.173 1.626,14.106 1.652,15.84 0.009,0.597 -0.104,0.728 -0.631,0.728 -1.316,0 -1.474,0.502 -1.474,4.685l0,3.853l-1.815,1.91c-2.167,2.282 -3.229,4.344 -3.412,6.629 -0.396,4.93 3.516,9.928 8.596,10.982 1.828,0.38 4.104,0.065 6.033,-0.833 1.765,-0.822 4.084,-3.186 5.036,-5.133 0.701,-1.433 0.776,-1.844 0.776,-4.233 0,-2.386 -0.075,-2.8 -0.77,-4.215 -0.451,-0.918 -1.47,-2.268 -2.456,-3.254l-1.685,-1.685 0.031,-4.077c0.017,-2.242 -0.046,-4.201 -0.14,-4.352 -0.094,-0.152 -0.382,-0.276 -0.64,-0.276 -1.013,0 -1.046,0.149 2.346,-10.487l2.206,-6.916 1.745,0.594c6.586,2.243 11.574,6.705 14.448,12.924 0.484,1.048 1.306,3.357 1.827,5.132 1.222,4.166 1.877,5.897 2.68,7.079 0.357,0.527 0.65,1.068 0.65,1.203 0,0.228 -0.637,0.442 -4.101,1.376 -1.002,0.27 -1.323,0.48 -1.323,0.863 0,0.697 2.512,1.989 3.867,1.989 0.962,0 1.028,0.05 1.028,0.78 0,0.429 -0.298,1.34 -0.661,2.024 -0.535,1.007 -0.661,1.621 -0.661,3.217 0,1.718 -0.085,2.074 -0.661,2.759 -0.674,0.802 -0.874,1.906 -0.397,2.201 0.146,0.09 0.265,0.507 0.265,0.926 0,0.419 -0.119,0.836 -0.265,0.926 -0.153,0.095 -0.265,1.544 -0.265,3.449 0,3.198 -0.017,3.297 -0.651,3.712 -0.688,0.451 -0.912,0.434 -4.153,-0.316 -1.321,-0.306 -1.897,-0.335 -2.304,-0.117 -0.563,0.301 -1.09,1.742 -1.093,2.987 -0.003,1.693 -2.224,6.44 -2.678,5.724 -0.124,-0.196 -0.228,-1.214 -0.23,-2.262 -0.002,-1.048 -0.13,-2.493 -0.284,-3.212 -0.292,-1.361 -0.875,-2.415 -1.172,-2.118 -0.513,0.513 -1.992,10.728 -1.72,11.88 0.12,0.508 0.466,0.691 2.034,1.077 4.671,1.148 7.063,3.567 7.805,7.892 0.539,3.142 -0.341,10.284 -1.758,14.267 -0.226,0.637 -0.41,1.351 -0.408,1.587 0.002,0.236 1.487,2.567 3.301,5.179l3.298,4.749 -0.854,0.364c-1.188,0.507 -7.225,2.031 -10.246,2.587 -3.3,0.608 -15.425,0.783 -18.785,0.272z"
|
|
||||||
android:strokeAlpha="1" android:strokeColor="#00000000"
|
|
||||||
android:strokeLineCap="butt" android:strokeLineJoin="miter" android:strokeWidth="1.33333325"
|
|
||||||
tools:ignore="VectorPath" />
|
|
||||||
<path android:fillAlpha="1" android:fillColor="#f5f5f5"
|
|
||||||
android:pathData="m91.686,122.295c0,-1.147 -0.159,-1.729 -0.707,-2.598 -2.015,-3.188 -4.277,-9.369 -4.617,-12.616 -0.276,-2.633 -0.587,-3.682 -2.237,-7.553 -0.811,-1.902 -1.037,-2.794 -1.121,-4.432 -0.082,-1.603 -0.027,-2.049 0.254,-2.045 0.619,0.01 1.489,0.881 1.91,1.912l0.406,0.992l2.115,0c1.163,0 2.19,-0.122 2.282,-0.271 0.186,-0.3 -1.685,-3.435 -2.701,-4.525 -0.356,-0.382 -1.591,-1.332 -2.744,-2.111 -2.079,-1.404 -2.894,-2.431 -2.894,-3.65 0,-0.305 0.045,-0.555 0.099,-0.555 0.055,0 0.772,0.298 1.595,0.661 2.106,0.931 2.274,0.892 2.274,-0.529 0,-0.655 0.109,-1.191 0.241,-1.191 0.249,0 3.206,3.628 4.851,5.953 2.423,3.424 6.277,12.163 7.702,17.462 0.886,3.297 2.382,11.144 2.21,11.593 -0.208,0.542 -6.74,4.305 -8.323,4.795 -0.568,0.176 -0.595,0.117 -0.595,-1.294zM90.315,99.671c0.098,-1.376 -0.346,-1.862 -1.703,-1.862 -1.129,0 -1.739,0.618 -1.521,1.54 0.289,1.22 0.876,1.669 2.068,1.581 1.067,-0.078 1.072,-0.085 1.156,-1.26z"
|
|
||||||
android:strokeAlpha="1" android:strokeColor="#00000000"
|
|
||||||
android:strokeLineCap="butt" android:strokeLineJoin="miter" android:strokeWidth="1.33333325"
|
|
||||||
tools:ignore="VectorPath" />
|
|
||||||
<path android:fillAlpha="1" android:fillColor="#f5f5f5"
|
|
||||||
android:pathData="m57.486,69.12c-5.937,-1.578 -6.761,-9.516 -1.285,-12.376 2.399,-1.253 5.309,-0.938 7.213,0.782 1.654,1.494 2.199,2.757 2.199,5.094 0,2.319 -0.56,3.636 -2.136,5.019 -1.739,1.526 -3.854,2.05 -5.991,1.481z"
|
|
||||||
android:strokeAlpha="1" android:strokeColor="#00000000"
|
|
||||||
android:strokeLineCap="butt" android:strokeLineJoin="miter" android:strokeWidth="1.33333325"/>
|
|
||||||
</vector>
|
|
||||||
|
|
@ -1,18 +0,0 @@
|
||||||
<vector xmlns:tools="http://schemas.android.com/tools"
|
|
||||||
android:height="144dp" android:viewportHeight="130"
|
|
||||||
android:viewportWidth="130" android:width="144dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
|
||||||
<path android:fillAlpha="1" android:fillColor="#f5f5f5"
|
|
||||||
android:pathData="m48.088,128.692c-0.323,-2.725 -0.477,-9.598 -0.297,-13.223 0.227,-4.572 0.798,-8.774 1.407,-10.35 0.223,-0.577 -0.398,-0.371 -1.022,0.34 -4.345,4.949 -13.03,8.963 -24.242,11.205 -6.35,1.27 -12.392,1.943 -20.346,2.268l-3.597,0.194l0,-8.227c0,-4.525 -0.016,-10.434 -0.007,-13.234L-0,92.494 3.744,94.173c7.594,3.405 14.078,5.826 20.089,6.514 2.987,0.342 7.839,0.132 10.296,-0.446 6.157,-1.449 11.139,-4.971 14.864,-10.51 2.054,-3.054 4.272,-8.386 5.16,-12.402 0.571,-2.581 0.552,-2.656 -0.799,-3.297 -3.588,-1.702 -8.251,-5.744 -10.333,-8.955 -1.72,-2.654 -3.083,-5.859 -3.914,-9.209 -0.397,-1.6 -0.862,-4.916 -0.862,-6.151l0,-1.052l-0.889,-0.003c-1.226,-0.004 -3.218,-0.45 -4.161,-0.932 -2.634,-1.344 -4.159,-4.346 -4.181,-8.233 -0.028,-5.06 3.09,-9.007 7.921,-10.028 0.832,-0.176 1.845,-0.258 2.489,-0.202 2.09,0.181 4.191,1.661 5.402,3.806 0.31,0.549 0.609,0.999 0.664,0.999 0.055,0 0.727,-0.595 1.493,-1.323 1.566,-1.488 3.611,-2.887 5.566,-3.809 2.064,-0.973 2.12,-0.979 2.24,-0.256 0.092,0.556 1.352,13.901 1.463,15.489 0.044,0.637 0.025,0.656 -0.753,0.761 -0.439,0.06 -0.902,0.213 -1.029,0.341 -0.171,0.173 -0.216,1.242 -0.175,4.174l0.056,3.941 -1.404,1.294c-1.729,1.593 -3.081,3.576 -3.655,5.361 -0.369,1.149 -0.42,1.612 -0.338,3.078 0.191,3.417 1.991,6.404 5.05,8.381 3.174,2.051 7.386,2.06 10.539,0.023 2.898,-1.873 4.94,-5.486 4.938,-8.737 -0.002,-2.817 -1.027,-5.057 -3.418,-7.465l-1.653,-1.665 0.05,-4.11c0.047,-3.847 0.028,-4.126 -0.304,-4.369 -0.195,-0.142 -0.548,-0.259 -0.786,-0.259 -0.327,0 -0.432,-0.105 -0.432,-0.433 0,-0.747 5.15,-16.966 5.387,-16.966 0.465,0 3.837,1.283 5.198,1.977 4.423,2.257 8.254,6.142 10.53,10.679 1.084,2.161 1.265,2.651 2.288,6.208 1.002,3.48 2.078,6.197 2.862,7.226 0.268,0.351 0.487,0.685 0.487,0.74 0,0.218 -1.128,0.665 -2.632,1.044 -3.138,0.791 -2.98,0.726 -2.98,1.216 0,0.707 2.113,1.815 3.728,1.954l1.23,0.106 0.06,0.626c0.042,0.438 -0.155,1.057 -0.655,2.058 -0.699,1.4 -0.714,1.473 -0.681,3.336l0.034,1.905 -0.642,0.681c-0.5,0.53 -0.642,0.839 -0.642,1.393 0,0.405 0.121,0.813 0.281,0.945 0.359,0.298 0.375,1.607 0.021,1.743 -0.204,0.078 -0.274,0.822 -0.33,3.5 -0.069,3.301 -0.083,3.411 -0.51,3.756 -0.586,0.474 -1.312,0.449 -3.878,-0.136 -3.071,-0.7 -3.444,-0.461 -3.826,2.456 -0.121,0.926 -0.388,2.135 -0.594,2.686 -0.432,1.161 -1.79,3.582 -2.009,3.582 -0.263,0 -0.375,-0.796 -0.377,-2.686 -0.003,-2.42 -0.666,-5.172 -1.246,-5.172 -0.329,0 -0.538,0.553 -0.705,1.871 -0.072,0.566 -0.246,1.45 -0.387,1.964 -0.141,0.514 -0.311,1.609 -0.377,2.432 -0.067,0.823 -0.235,2.33 -0.373,3.347 -0.368,2.701 -0.306,2.793 2.244,3.371 2.834,0.642 5.471,2.475 6.509,4.523 0.952,1.878 1.16,2.95 1.145,5.877 -0.022,4.194 -0.502,7.048 -2.157,12.816l-0.215,0.748 4.492,6.482c2.47,3.565 4.557,7.11 4.628,7.224 0.093,0.151 -5.43,0.123 -20.266,0.081l-20.332,-0.059z"
|
|
||||||
android:strokeAlpha="1" android:strokeColor="#00000000"
|
|
||||||
android:strokeLineCap="butt" android:strokeLineJoin="miter" android:strokeWidth="0.94280899"
|
|
||||||
tools:ignore="VectorPath" />
|
|
||||||
<path android:fillAlpha="1" android:fillColor="#f5f5f5"
|
|
||||||
android:pathData="m57.115,69.085c-5.74,-1.898 -6.229,-9.968 -0.759,-12.531 0.921,-0.432 1.308,-0.5 2.846,-0.5 1.636,0 1.882,0.05 3.035,0.617 1.391,0.684 2.307,1.647 2.987,3.139 0.344,0.754 0.42,1.259 0.425,2.795 0.005,1.747 -0.037,1.959 -0.612,3.129 -1.395,2.834 -4.943,4.335 -7.921,3.35z"
|
|
||||||
android:strokeAlpha="1" android:strokeColor="#00000000"
|
|
||||||
android:strokeLineCap="butt" android:strokeLineJoin="miter" android:strokeWidth="0.94280899"/>
|
|
||||||
<path android:fillAlpha="1" android:fillColor="#f5f5f5"
|
|
||||||
android:pathData="m92.411,130.001c-0.259,-0.42 -0.681,-3.216 -0.775,-5.939l-0.106,-3.087 -1.564,-3.181c-0.86,-1.749 -1.997,-4.401 -2.527,-5.893 -0.836,-2.354 -0.986,-2.997 -1.135,-4.864 -0.203,-2.548 -0.481,-3.596 -1.593,-6.023 -1.397,-3.05 -1.751,-4.298 -1.751,-6.176 0,-1.327 0.058,-1.679 0.291,-1.769 0.442,-0.17 1.605,0.877 1.89,1.702 0.418,1.207 0.464,1.228 2.711,1.228 2.313,0 2.522,-0.13 1.87,-1.166 -0.211,-0.336 -0.719,-1.158 -1.127,-1.827 -0.925,-1.516 -1.892,-2.446 -4.058,-3.905 -2.056,-1.385 -2.39,-1.732 -2.775,-2.883 -0.572,-1.71 -0.609,-1.699 1.995,-0.588 0.902,0.385 1.695,0.644 1.763,0.576 0.068,-0.068 0.108,-0.565 0.09,-1.105 -0.052,-1.516 0.156,-1.626 1.087,-0.574 0.439,0.496 1.718,2.123 2.841,3.615 1.719,2.284 2.361,3.349 4.06,6.735 3.051,6.08 4.741,10.916 5.82,16.651 0.319,1.698 0.7,3.634 0.845,4.303 0.502,2.316 0.669,4.231 0.784,9.027l0.116,5.144 -4.401,0.004c-2.427,0.002 -4.303,0.073 -4.351,-0.004zM90.081,100.856c0.287,-0.154 0.361,-0.394 0.359,-1.169 -0.002,-0.537 -0.065,-1.068 -0.14,-1.181 -0.413,-0.618 -1.845,-0.83 -2.768,-0.41 -0.458,0.209 -0.49,0.296 -0.41,1.124 0.093,0.967 0.501,1.575 1.143,1.7 0.776,0.152 1.459,0.128 1.817,-0.064z"
|
|
||||||
android:strokeAlpha="1" android:strokeColor="#00000000"
|
|
||||||
android:strokeLineCap="butt" android:strokeLineJoin="miter" android:strokeWidth="0.94280899"
|
|
||||||
tools:ignore="VectorPath" />
|
|
||||||
</vector>
|
|
||||||
|
After Width: | Height: | Size: 110 KiB |
|
Before Width: | Height: | Size: 4.2 KiB |
|
|
@ -1,42 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<!-- shamelessly ripped off https://stackoverflow.com/questions/32212259/android-how-to-change-default-seekbar-thickness -->
|
|
||||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
|
||||||
|
|
||||||
<item android:id="@android:id/background">
|
|
||||||
<shape>
|
|
||||||
|
|
||||||
<corners
|
|
||||||
android:radius="4dp"
|
|
||||||
/>
|
|
||||||
<solid
|
|
||||||
android:color="@color/seek_bar_background"
|
|
||||||
/>
|
|
||||||
</shape>
|
|
||||||
</item>
|
|
||||||
|
|
||||||
<item android:id="@android:id/secondaryProgress">
|
|
||||||
<clip>
|
|
||||||
<shape>
|
|
||||||
<corners
|
|
||||||
android:radius="4dp"
|
|
||||||
/>
|
|
||||||
<solid
|
|
||||||
android:color="@color/seek_bar_secondary_progress"
|
|
||||||
/>
|
|
||||||
</shape>
|
|
||||||
</clip>
|
|
||||||
</item>
|
|
||||||
|
|
||||||
<item android:id="@android:id/progress">
|
|
||||||
<clip>
|
|
||||||
<shape>
|
|
||||||
<corners
|
|
||||||
android:radius="4dp"
|
|
||||||
/>
|
|
||||||
<solid
|
|
||||||
android:color="@color/seek_bar_progress"
|
|
||||||
/>
|
|
||||||
</shape>
|
|
||||||
</clip>
|
|
||||||
</item>
|
|
||||||
</layer-list>
|
|
||||||
|
|
@ -1,25 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<!-- shamelessly ripped off https://stackoverflow.com/questions/32212259/android-how-to-change-default-seekbar-thickness -->
|
|
||||||
<!--
|
|
||||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:shape="oval"
|
|
||||||
android:innerRadius="@dimen/seek_bar_thumb_size">
|
|
||||||
<size
|
|
||||||
android:height="@dimen/seek_bar_thumb_size"
|
|
||||||
android:width="@dimen/seek_bar_thumb_size"
|
|
||||||
/>
|
|
||||||
<solid android:color="@color/seek_bar_progress"/>
|
|
||||||
</shape>
|
|
||||||
-->
|
|
||||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:shape="rectangle"
|
|
||||||
>
|
|
||||||
<corners
|
|
||||||
android:radius="2dp"
|
|
||||||
/>
|
|
||||||
<size
|
|
||||||
android:height="@dimen/seek_bar_thumb_height"
|
|
||||||
android:width="@dimen/seek_bar_thumb_size"
|
|
||||||
/>
|
|
||||||
<solid android:color="@color/whited2"/>
|
|
||||||
</shape>
|
|
||||||
|
|
@ -37,15 +37,17 @@
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
>
|
>
|
||||||
|
|
||||||
|
<!-- REMOVE VISIBILITY GONE TO ADD AN ICON IN THE TOP BAR -->
|
||||||
<ImageView
|
<ImageView
|
||||||
android:id="@+id/logo_persistent"
|
android:id="@+id/logo_persistent"
|
||||||
android:layout_width="?attr/actionBarSize"
|
android:layout_width="?attr/actionBarSize"
|
||||||
android:layout_height="?attr/actionBarSize"
|
android:layout_height="?attr/actionBarSize"
|
||||||
app:srcCompat="@drawable/actionbar_logo"
|
app:srcCompat="@drawable/logo_roundsquare"
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
android:padding="12dp"
|
android:padding="12dp"
|
||||||
android:contentDescription="@string/r_a_dio_icon" />
|
android:contentDescription="@string/r_a_dio_icon"
|
||||||
|
android:visibility="gone"/>
|
||||||
|
|
||||||
<androidx.appcompat.widget.Toolbar
|
<androidx.appcompat.widget.Toolbar
|
||||||
android:id="@+id/toolbar"
|
android:id="@+id/toolbar"
|
||||||
|
|
@ -54,7 +56,7 @@
|
||||||
android:background="?attr/colorPrimary"
|
android:background="?attr/colorPrimary"
|
||||||
android:theme="@style/ThemeOverlay.AppCompat.ActionBar"
|
android:theme="@style/ThemeOverlay.AppCompat.ActionBar"
|
||||||
app:menu="@menu/toolbar_menu"
|
app:menu="@menu/toolbar_menu"
|
||||||
app:layout_constraintStart_toEndOf="@id/logo_persistent"
|
app:layout_constraintStart_toEndOf="@+id/logo_persistent"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
android:layout_marginLeft="-4dp"
|
android:layout_marginLeft="-4dp"
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,7 @@
|
||||||
|
|
||||||
app:layout_constraintStart_toStartOf="@id/imageGuideline"
|
app:layout_constraintStart_toStartOf="@id/imageGuideline"
|
||||||
app:layout_constraintTop_toTopOf="@id/seek_bar_volume"
|
app:layout_constraintTop_toTopOf="@id/seek_bar_volume"
|
||||||
app:srcCompat="@drawable/actionbar_logo"
|
app:srcCompat="@drawable/logo_roundsquare"
|
||||||
tools:ignore="HardcodedText" />
|
tools:ignore="HardcodedText" />
|
||||||
|
|
||||||
<androidx.constraintlayout.widget.Guideline
|
<androidx.constraintlayout.widget.Guideline
|
||||||
|
|
@ -72,7 +72,8 @@
|
||||||
app:layout_constraintHorizontal_bias="1.0"
|
app:layout_constraintHorizontal_bias="1.0"
|
||||||
app:layout_constraintVertical_weight="3"
|
app:layout_constraintVertical_weight="3"
|
||||||
app:layout_constraintStart_toStartOf="@id/streamerPicture"
|
app:layout_constraintStart_toStartOf="@id/streamerPicture"
|
||||||
app:layout_constraintTop_toBottomOf="@id/streamerPicture" />
|
app:layout_constraintTop_toBottomOf="@id/streamerPicture"
|
||||||
|
android:visibility="gone"/>
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/listenersCount"
|
android:id="@+id/listenersCount"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
|
|
@ -87,7 +88,9 @@
|
||||||
app:layout_constraintBottom_toTopOf="@id/topInfoGuideline"
|
app:layout_constraintBottom_toTopOf="@id/topInfoGuideline"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintStart_toStartOf="@id/streamerPicture"
|
app:layout_constraintStart_toStartOf="@id/streamerPicture"
|
||||||
app:layout_constraintTop_toBottomOf="@id/streamerName" />
|
app:layout_constraintTop_toBottomOf="@id/streamerName"
|
||||||
|
android:visibility="gone"
|
||||||
|
/>
|
||||||
|
|
||||||
<androidx.constraintlayout.widget.Guideline
|
<androidx.constraintlayout.widget.Guideline
|
||||||
android:id="@+id/topInfoGuideline"
|
android:id="@+id/topInfoGuideline"
|
||||||
|
|
@ -170,6 +173,7 @@
|
||||||
android:orientation="vertical"
|
android:orientation="vertical"
|
||||||
android:layout_marginEnd="8dp"
|
android:layout_marginEnd="8dp"
|
||||||
android:layout_marginRight="8dp"
|
android:layout_marginRight="8dp"
|
||||||
|
android:visibility="gone"
|
||||||
>
|
>
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
|
|
@ -287,6 +291,7 @@
|
||||||
|
|
||||||
<!-- android:progressDrawable="@drawable/progress_bar_progress" -->
|
<!-- android:progressDrawable="@drawable/progress_bar_progress" -->
|
||||||
|
|
||||||
|
<!-- REMOVE VISIBILITY GONE IF YOU HAVE TIME VALUES TO DISPLAY THE PROGRESS BAR -->
|
||||||
<ProgressBar
|
<ProgressBar
|
||||||
android:id="@+id/progressBar"
|
android:id="@+id/progressBar"
|
||||||
style="@style/Widget.AppCompat.ProgressBar.Horizontal"
|
style="@style/Widget.AppCompat.ProgressBar.Horizontal"
|
||||||
|
|
@ -297,8 +302,10 @@
|
||||||
android:progress="70"
|
android:progress="70"
|
||||||
android:progressDrawable="@drawable/progress_bar_progress"
|
android:progressDrawable="@drawable/progress_bar_progress"
|
||||||
app:layout_constraintBottom_toTopOf="@id/play_pause"
|
app:layout_constraintBottom_toTopOf="@id/play_pause"
|
||||||
tools:layout_editor_absoluteX="0dp" />
|
tools:layout_editor_absoluteX="0dp"
|
||||||
|
android:visibility="gone"/>
|
||||||
|
|
||||||
|
<!-- REMOVE VISIBILITY GONE IF YOU HAVE TIME VALUES TO DISPLAY THE PROGRESS BAR -->
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/endTime"
|
android:id="@+id/endTime"
|
||||||
android:layout_width="44sp"
|
android:layout_width="44sp"
|
||||||
|
|
@ -308,8 +315,10 @@
|
||||||
android:text=""
|
android:text=""
|
||||||
android:textAlignment="textEnd"
|
android:textAlignment="textEnd"
|
||||||
app:layout_constraintEnd_toEndOf="@id/progressBar"
|
app:layout_constraintEnd_toEndOf="@id/progressBar"
|
||||||
app:layout_constraintTop_toBottomOf="@id/progressBar" />
|
app:layout_constraintTop_toBottomOf="@id/progressBar"
|
||||||
|
android:visibility="gone"/>
|
||||||
|
|
||||||
|
<!-- REMOVE VISIBILITY GONE IF YOU HAVE TIME VALUES TO DISPLAY THE PROGRESS BAR -->
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/currentTime"
|
android:id="@+id/currentTime"
|
||||||
android:layout_width="44sp"
|
android:layout_width="44sp"
|
||||||
|
|
@ -319,7 +328,8 @@
|
||||||
android:text=""
|
android:text=""
|
||||||
android:textAlignment="textStart"
|
android:textAlignment="textStart"
|
||||||
app:layout_constraintStart_toStartOf="@id/progressBar"
|
app:layout_constraintStart_toStartOf="@id/progressBar"
|
||||||
app:layout_constraintTop_toBottomOf="@id/progressBar" />
|
app:layout_constraintTop_toBottomOf="@id/progressBar"
|
||||||
|
android:visibility="gone"/>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,9 +16,5 @@
|
||||||
android:icon="@drawable/ic_newspaper"
|
android:icon="@drawable/ic_newspaper"
|
||||||
android:title="@string/title_news"/>
|
android:title="@string/title_news"/>
|
||||||
|
|
||||||
<item
|
|
||||||
android:id="@+id/navigation_chat"
|
|
||||||
android:icon="@drawable/ic_chat_processing"
|
|
||||||
android:title="@string/title_chat"/>
|
|
||||||
|
|
||||||
</menu>
|
</menu>
|
||||||
|
|
|
||||||
|
|
@ -14,11 +14,12 @@
|
||||||
android:icon="@drawable/ic_alarm"
|
android:icon="@drawable/ic_alarm"
|
||||||
app:showAsAction="ifRoom"/>
|
app:showAsAction="ifRoom"/>
|
||||||
|
|
||||||
|
<!--
|
||||||
<item
|
<item
|
||||||
android:id="@+id/action_refresh"
|
android:id="@+id/action_refresh"
|
||||||
android:title="@string/action_refresh"
|
android:title="@string/action_refresh"
|
||||||
app:showAsAction="never"/>
|
app:showAsAction="never"/>
|
||||||
|
-->
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
<item
|
<item
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<background android:drawable="@drawable/ic_launcher_background"/>
|
<background android:drawable="@color/ic_launcher_background"/>
|
||||||
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
||||||
</adaptive-icon>
|
</adaptive-icon>
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<background android:drawable="@drawable/ic_launcher_background"/>
|
<background android:drawable="@color/ic_launcher_background"/>
|
||||||
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
||||||
</adaptive-icon>
|
</adaptive-icon>
|
||||||
|
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 3.7 KiB |
|
Before Width: | Height: | Size: 3.0 KiB |
|
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 5.7 KiB |
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 2.4 KiB |
|
Before Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 3.4 KiB |
|
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 5.3 KiB |
|
Before Width: | Height: | Size: 4.2 KiB |
|
Before Width: | Height: | Size: 5.9 KiB After Width: | Height: | Size: 8.1 KiB |
|
Before Width: | Height: | Size: 5.1 KiB After Width: | Height: | Size: 8.5 KiB |
|
Before Width: | Height: | Size: 6.9 KiB |
|
Before Width: | Height: | Size: 9.1 KiB After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 6.9 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 9.8 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 19 KiB |
|
|
@ -1,13 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
|
||||||
android:id="@+id/navigation_chat"
|
|
||||||
app:startDestination="@id/fragment_chat"
|
|
||||||
tools:ignore="UnusedNavigation">
|
|
||||||
<fragment
|
|
||||||
android:id="@+id/fragment_chat"
|
|
||||||
android:name="fr.forum_thalie.tsumugi.ui.chat.ChatFragment"
|
|
||||||
android:label="@string/title_chat"
|
|
||||||
tools:layout="@layout/fragment_chat" />
|
|
||||||
</navigation>
|
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<color name="ic_launcher_background">#F58B01</color>
|
||||||
|
</resources>
|
||||||
|
|
@ -29,7 +29,7 @@
|
||||||
<string name="action_settings">Settings</string>
|
<string name="action_settings">Settings</string>
|
||||||
<string name="action_refresh">Force data refresh</string>
|
<string name="action_refresh">Force data refresh</string>
|
||||||
<string name="action_bug_submit">Submit a bug (GitHub)</string>
|
<string name="action_bug_submit">Submit a bug (GitHub)</string>
|
||||||
<string name="github_url_new_issue">https://github.com/yattoz/Radio2/issues/</string>
|
<string name="github_url_new_issue">https://github.com/yattoz/Tsumugi-app/issues/</string>
|
||||||
<string name="no_user_name">To display your favorite, set you IRC user name in Settings → Customize app behavior.</string>
|
<string name="no_user_name">To display your favorite, set you IRC user name in Settings → Customize app behavior.</string>
|
||||||
<string name="ra_f">.ra f</string>
|
<string name="ra_f">.ra f</string>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,13 +6,13 @@
|
||||||
<item name="colorPrimary">@color/colorPrimary</item>
|
<item name="colorPrimary">@color/colorPrimary</item>
|
||||||
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
|
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
|
||||||
<item name="colorAccent">@color/colorAccent</item>
|
<item name="colorAccent">@color/colorAccent</item>
|
||||||
<item name="icon">@drawable/actionbar_logo</item>
|
<item name="icon">@drawable/logo_roundsquare</item>
|
||||||
<item name="android:textColorHighlight">@color/rblue</item>
|
<item name="android:textColorHighlight">@color/rblue</item>
|
||||||
<item name="android:colorBackground">@color/colorPrimaryDark</item>
|
<item name="android:colorBackground">@color/colorPrimaryDark</item>
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<style name="AppTheme.Parameters" parent="AppTheme">
|
<style name="AppTheme.Parameters" parent="AppTheme">
|
||||||
<item name="icon" />
|
<item name="icon"/>
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<style name="AppTheme.BottomBar" parent="AppTheme">
|
<style name="AppTheme.BottomBar" parent="AppTheme">
|
||||||
|
|
|
||||||
|
|
@ -2,14 +2,6 @@
|
||||||
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
|
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<EditTextPreference
|
|
||||||
app:iconSpaceReserved="false"
|
|
||||||
app:key="userName"
|
|
||||||
app:title="User name for favorite songs"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<SeekBarPreference
|
<SeekBarPreference
|
||||||
app:key="volume"
|
app:key="volume"
|
||||||
app:iconSpaceReserved="false"
|
app:iconSpaceReserved="false"
|
||||||
|
|
@ -34,24 +26,7 @@
|
||||||
app:iconSpaceReserved="false"
|
app:iconSpaceReserved="false"
|
||||||
app:title="Make Snackbars persistent"
|
app:title="Make Snackbars persistent"
|
||||||
app:singleLineTitle="false"
|
app:singleLineTitle="false"
|
||||||
app:defaultValue="true"
|
app:defaultValue="false"
|
||||||
/>
|
|
||||||
|
|
||||||
<ListPreference
|
|
||||||
app:key="fetchPeriod"
|
|
||||||
app:iconSpaceReserved="false"
|
|
||||||
android:title="Set update period when stopped"
|
|
||||||
app:singleLineTitle="false"
|
|
||||||
android:entries="@array/fetchPeriodString"
|
|
||||||
android:entryValues="@array/fetchPeriodValues"
|
|
||||||
android:defaultValue="10"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Preference
|
|
||||||
app:key="helpFavorites"
|
|
||||||
app:title="Help to use favorites with IRC"
|
|
||||||
app:summary="Opens the wiki in your web browser"
|
|
||||||
app:icon="@drawable/ic_open_in_browser"
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
</PreferenceScreen>
|
</PreferenceScreen>
|
||||||
|
|
@ -25,13 +25,6 @@
|
||||||
app:fragment="fr.forum_thalie.tsumugi.preferences.CustomizeFragment"
|
app:fragment="fr.forum_thalie.tsumugi.preferences.CustomizeFragment"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Preference
|
|
||||||
app:icon="@drawable/ic_notification"
|
|
||||||
app:key="streamerNotifServiceFragment"
|
|
||||||
app:title="Streamer Notification Service"
|
|
||||||
app:fragment="fr.forum_thalie.tsumugi.preferences.StreamerNotifServiceFragment"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Preference
|
<Preference
|
||||||
app:key="submitBug"
|
app:key="submitBug"
|
||||||
app:title="Submit a bug (opens GitHub in a web browser)"
|
app:title="Submit a bug (opens GitHub in a web browser)"
|
||||||
|
|
|
||||||
|
|
@ -1,22 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
|
||||||
|
|
||||||
<SwitchPreferenceCompat
|
|
||||||
app:key="newStreamerNotification"
|
|
||||||
app:iconSpaceReserved="false"
|
|
||||||
app:title="New streamer notification (drains battery!)"
|
|
||||||
app:singleLineTitle="false"
|
|
||||||
app:defaultValue="false"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<ListPreference
|
|
||||||
app:iconSpaceReserved="false"
|
|
||||||
app:key="streamerMonitorPeriodPref"
|
|
||||||
app:title="Streamer monitor frequency"
|
|
||||||
android:entries="@array/streamerFetchPeriodString"
|
|
||||||
android:entryValues="@array/streamerFetchPeriod"
|
|
||||||
android:defaultValue="10"
|
|
||||||
/>
|
|
||||||
|
|
||||||
</PreferenceScreen>
|
|
||||||