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>
|
||||
|
Före Bredd: | Höjd: | Storlek: 22 KiB Efter Bredd: | Höjd: | Storlek: 44 KiB |
|
|
@ -71,7 +71,7 @@ abstract class BaseNotification(private val notificationChannelId: String,
|
|||
builder.setSmallIcon(R.drawable.lollipop_logo)
|
||||
builder.color = -0xf58b01 // same color as Accent. Can't use c.getColor since it's API23+
|
||||
} else {
|
||||
builder.setSmallIcon(R.drawable.normal_logo)
|
||||
builder.setSmallIcon(R.drawable.logo_roundsquare)
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
|
|
|
|||
|
|
@ -8,8 +8,6 @@ import android.util.Log
|
|||
import androidx.preference.PreferenceManager
|
||||
import fr.forum_thalie.tsumugi.alarm.RadioAlarm
|
||||
import fr.forum_thalie.tsumugi.playerstore.PlayerStore
|
||||
import fr.forum_thalie.tsumugi.streamerNotificationService.WorkerStore
|
||||
import fr.forum_thalie.tsumugi.streamerNotificationService.startStreamerMonitor
|
||||
|
||||
class BootBroadcastReceiver : BroadcastReceiver(){
|
||||
|
||||
|
|
@ -19,8 +17,6 @@ class BootBroadcastReceiver : BroadcastReceiver(){
|
|||
preferenceStore = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
|
|
@ -29,8 +25,6 @@ class BootBroadcastReceiver : BroadcastReceiver(){
|
|||
RadioAlarm.instance.setNextAlarm(context) // schedule next alarm
|
||||
if (PlayerStore.instance.streamerName.value.isNullOrBlank())
|
||||
PlayerStore.instance.initPicture(context)
|
||||
if (!PlayerStore.instance.isInitialized)
|
||||
PlayerStore.instance.initApi()
|
||||
|
||||
val i = Intent(context, RadioService::class.java)
|
||||
i.putExtra("action", Actions.PLAY_OR_FALLBACK.name)
|
||||
|
|
|
|||
|
|
@ -15,12 +15,8 @@ import fr.forum_thalie.tsumugi.playerstore.PlayerStore
|
|||
|
||||
import java.util.Timer
|
||||
import android.view.MenuItem
|
||||
import androidx.preference.PreferenceManager
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
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
|
||||
|
|
@ -39,11 +35,11 @@ class MainActivity : BaseActivity() {
|
|||
* Called on first creation and when restoring state.
|
||||
*/
|
||||
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.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
|
||||
val controller = bottomNavigationView.setupWithNavController(
|
||||
|
|
@ -77,15 +73,15 @@ class MainActivity : BaseActivity() {
|
|||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
// Handle item selection
|
||||
return when (item.itemId) {
|
||||
/*
|
||||
R.id.action_refresh -> {
|
||||
PlayerStore.instance.queue.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)
|
||||
s.show()
|
||||
true
|
||||
}
|
||||
*/
|
||||
R.id.action_settings -> {
|
||||
val i = Intent(this, ParametersActivity::class.java)
|
||||
startActivity(i)
|
||||
|
|
@ -118,10 +114,6 @@ class MainActivity : BaseActivity() {
|
|||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
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.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))
|
||||
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
|
||||
if (PlayerStore.instance.isInitialized)
|
||||
{
|
||||
|
|
@ -149,9 +139,6 @@ class MainActivity : BaseActivity() {
|
|||
// initialize some API data
|
||||
PlayerStore.instance.initPicture(this)
|
||||
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)
|
||||
|
|
|
|||
|
|
@ -29,7 +29,6 @@ class ParametersActivity : BaseActivity() {
|
|||
ActionOpenParam.ALARM.name -> AlarmFragment()
|
||||
ActionOpenParam.SLEEP.name -> SleepFragment()
|
||||
ActionOpenParam.CUSTOMIZE.name -> CustomizeFragment()
|
||||
ActionOpenParam.STREAMER_NOTIFICATION_SERVICE.name -> StreamerNotifServiceFragment()
|
||||
else -> MainPreferenceFragment()
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -107,29 +107,6 @@ class RadioService : MediaBrowserServiceCompat() {
|
|||
// ################### 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 {
|
||||
setVolume(it)
|
||||
}
|
||||
|
|
@ -145,20 +122,14 @@ class RadioService : MediaBrowserServiceCompat() {
|
|||
stopPlaying()
|
||||
}
|
||||
|
||||
private val startTimeObserver = Observer<Long> {
|
||||
// We're listening to startTime to determine if we have to update Queue and Lp.
|
||||
// this is because startTime is set by the API and never by the ICY, so both cases are covered (playing and stopped)
|
||||
// should be OK even when a new streamer comes in.
|
||||
if (it != PlayerStore.instance.currentSongBackup.startTime.value) // we have a new song
|
||||
private val titleObserver = Observer<String> {
|
||||
// We're checking if a new song arrives. If so, we put the currentSong in Lp and update the backup.
|
||||
if (PlayerStore.instance.currentSong != PlayerStore.instance.currentSongBackup
|
||||
&& it != noConnectionValue)
|
||||
{
|
||||
PlayerStore.instance.updateLp()
|
||||
PlayerStore.instance.updateQueue()
|
||||
}
|
||||
}
|
||||
|
||||
private val streamerObserver = Observer<String> {
|
||||
PlayerStore.instance.initApi()
|
||||
nowPlayingNotification.update(this) // should update the streamer icon
|
||||
nowPlayingNotification.update(this)
|
||||
}
|
||||
|
||||
private val streamerPictureObserver = Observer<Bitmap> {
|
||||
|
|
@ -224,9 +195,7 @@ class RadioService : MediaBrowserServiceCompat() {
|
|||
nowPlayingNotification.create(this, mediaSession)
|
||||
|
||||
|
||||
PlayerStore.instance.streamerName.observeForever(streamerObserver)
|
||||
PlayerStore.instance.currentSong.title.observeForever(titleObserver)
|
||||
PlayerStore.instance.currentSong.startTime.observeForever(startTimeObserver)
|
||||
PlayerStore.instance.volume.observeForever(volumeObserver)
|
||||
PlayerStore.instance.isPlaying.observeForever(isPlayingObserver)
|
||||
PlayerStore.instance.isMuted.observeForever(isMutedObserver)
|
||||
|
|
@ -234,12 +203,6 @@ class RadioService : MediaBrowserServiceCompat() {
|
|||
|
||||
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
|
||||
Log.d(tag, radioTag + "created")
|
||||
}
|
||||
|
|
@ -303,7 +266,6 @@ class RadioService : MediaBrowserServiceCompat() {
|
|||
player.release()
|
||||
unregisterReceiver(receiver)
|
||||
PlayerStore.instance.currentSong.title.removeObserver(titleObserver)
|
||||
PlayerStore.instance.currentSong.startTime.removeObserver(startTimeObserver)
|
||||
PlayerStore.instance.volume.removeObserver(volumeObserver)
|
||||
PlayerStore.instance.isPlaying.removeObserver(isPlayingObserver)
|
||||
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 isMuted : MutableLiveData<Boolean> = MutableLiveData()
|
||||
val listenersCount: MutableLiveData<Int> = MutableLiveData()
|
||||
private val urlToScrape = "https://r-a-d.io/api"
|
||||
var latencyCompensator : Long = 0
|
||||
var isInitialized: Boolean = false
|
||||
var isStreamDown: Boolean = false
|
||||
|
||||
init {
|
||||
playbackState.value = PlaybackStateCompat.STATE_STOPPED
|
||||
|
|
@ -47,6 +45,7 @@ class PlayerStore {
|
|||
isLpUpdated.value = false
|
||||
isMuted.value = false
|
||||
currentSong.title.value = noConnectionValue
|
||||
currentSongBackup.title.value = noConnectionValue
|
||||
listenersCount.value = 0
|
||||
}
|
||||
|
||||
|
|
@ -54,6 +53,7 @@ class PlayerStore {
|
|||
// ################# API FUNCTIONS ##################
|
||||
// ##################################################
|
||||
|
||||
/*
|
||||
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 (playbackState.value != PlaybackStateCompat.STATE_PLAYING || currentSong.title.value.isNullOrEmpty()
|
||||
|
|
@ -154,24 +154,26 @@ class PlayerStore {
|
|||
}
|
||||
Async(scrape, post)
|
||||
}
|
||||
*/
|
||||
|
||||
// ##################################################
|
||||
// ############## QUEUE / LP FUNCTIONS ##############
|
||||
// ##################################################
|
||||
|
||||
fun updateLp() {
|
||||
// note : lp must never be empty. There should always be some songs "last played".
|
||||
// if not, then the function has been called before initialization. No need to do anything.
|
||||
if (lp.isNotEmpty()){
|
||||
// note : lp is empty at initialization. This check was needed when we used the R/a/dio API.
|
||||
//if (lp.isNotEmpty()){
|
||||
val n = Song()
|
||||
n.copy(currentSongBackup)
|
||||
lp.add(0, n)
|
||||
if (n.title.value != noConnectionValue)
|
||||
lp.add(0, n)
|
||||
currentSongBackup.copy(currentSong)
|
||||
isLpUpdated.value = true
|
||||
Log.d(tag, playerStoreTag + lp.toString())
|
||||
}
|
||||
//}
|
||||
}
|
||||
|
||||
/*
|
||||
fun updateQueue() {
|
||||
if (queue.isNotEmpty()) {
|
||||
queue.remove(queue.first())
|
||||
|
|
@ -257,11 +259,13 @@ class PlayerStore {
|
|||
song.type.value = songJSON.getInt("type")
|
||||
return song
|
||||
}
|
||||
*/
|
||||
|
||||
// ##################################################
|
||||
// ############## PICTURE FUNCTIONS #################
|
||||
// ##################################################
|
||||
|
||||
/*
|
||||
private fun fetchPicture(fileUrl: String)
|
||||
{
|
||||
val scrape: (Any?) -> Bitmap? = {
|
||||
|
|
@ -287,10 +291,11 @@ class PlayerStore {
|
|||
}
|
||||
Async(scrape, post)
|
||||
}
|
||||
*/
|
||||
|
||||
fun initPicture(c: Context) {
|
||||
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 fr.forum_thalie.tsumugi.R
|
||||
import fr.forum_thalie.tsumugi.preferenceStore
|
||||
import fr.forum_thalie.tsumugi.ui.songs.request.Requestor
|
||||
|
||||
class CustomizeFragment : PreferenceFragmentCompat() {
|
||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||
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")
|
||||
snackbarPersistent!!.summary = if (preferenceStore.getBoolean("snackbarPersistent", true))
|
||||
|
|
@ -48,33 +40,5 @@ class CustomizeFragment : PreferenceFragmentCompat() {
|
|||
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.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.TextView
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.FragmentPagerAdapter
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.lifecycle.ViewModelProviders
|
||||
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 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.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() {
|
||||
|
||||
private lateinit var adapter : SongsPagerAdapter
|
||||
private lateinit var snackBar : Snackbar
|
||||
private lateinit var root: View
|
||||
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(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
|
|
@ -71,9 +28,8 @@ class SongsFragment : Fragment() {
|
|||
viewPager = root.findViewById(R.id.tabPager)
|
||||
adapter = SongsPagerAdapter(childFragmentManager, FragmentPagerAdapter.BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT)
|
||||
adapter.addFragment(LastPlayedFragment.newInstance(), "last played")
|
||||
adapter.addFragment(QueueFragment.newInstance(), "queue")
|
||||
adapter.addFragment(RequestFragment.newInstance(), "request")
|
||||
adapter.addFragment(FavoritesFragment.newInstance(), "Favorites")
|
||||
|
||||
//adapter.addFragment(QueueFragment.newInstance(), "queue")
|
||||
|
||||
viewPager.adapter = adapter
|
||||
|
||||
|
|
@ -81,14 +37,8 @@ class SongsFragment : Fragment() {
|
|||
tabLayout.setupWithViewPager(viewPager)
|
||||
Log.d(tag, "SongFragment view created")
|
||||
|
||||
Requestor.instance.snackBarText.observeForever(snackBarTextObserver)
|
||||
|
||||
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>
|
||||
|
Före Bredd: | Höjd: | Storlek: 204 B |
|
Före Bredd: | Höjd: | Storlek: 213 B |
|
Före Bredd: | Höjd: | Storlek: 402 B Efter Bredd: | Höjd: | Storlek: 1.1 KiB |
|
Före Bredd: | Höjd: | Storlek: 143 B |
|
Före Bredd: | Höjd: | Storlek: 165 B |
|
Före Bredd: | Höjd: | Storlek: 318 B Efter Bredd: | Höjd: | Storlek: 659 B |
|
|
@ -1,11 +1,11 @@
|
|||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:opacity="opaque">
|
||||
<!-- 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 -->
|
||||
|
||||
<item
|
||||
android:drawable="@drawable/ic_radio_logo_round"
|
||||
android:drawable="@drawable/ic_tsumugi_lineart"
|
||||
android:gravity="center"
|
||||
android:height="144dp"
|
||||
android:width="144dp"
|
||||
|
|
|
|||
|
Före Bredd: | Höjd: | Storlek: 236 B |
|
Före Bredd: | Höjd: | Storlek: 249 B |
|
Före Bredd: | Höjd: | Storlek: 494 B Efter Bredd: | Höjd: | Storlek: 1.6 KiB |
|
Före Bredd: | Höjd: | Storlek: 321 B |
|
Före Bredd: | Höjd: | Storlek: 353 B |
|
Före Bredd: | Höjd: | Storlek: 698 B Efter Bredd: | Höjd: | Storlek: 2.7 KiB |
|
Efter Bredd: | Höjd: | Storlek: 4.1 KiB |
|
Före Bredd: | Höjd: | Storlek: 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>
|
||||
|
Efter Bredd: | Höjd: | Storlek: 110 KiB |
|
Före Bredd: | Höjd: | Storlek: 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"
|
||||
>
|
||||
|
||||
<!-- REMOVE VISIBILITY GONE TO ADD AN ICON IN THE TOP BAR -->
|
||||
<ImageView
|
||||
android:id="@+id/logo_persistent"
|
||||
android:layout_width="?attr/actionBarSize"
|
||||
android:layout_height="?attr/actionBarSize"
|
||||
app:srcCompat="@drawable/actionbar_logo"
|
||||
app:srcCompat="@drawable/logo_roundsquare"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
android:padding="12dp"
|
||||
android:contentDescription="@string/r_a_dio_icon" />
|
||||
android:contentDescription="@string/r_a_dio_icon"
|
||||
android:visibility="gone"/>
|
||||
|
||||
<androidx.appcompat.widget.Toolbar
|
||||
android:id="@+id/toolbar"
|
||||
|
|
@ -54,7 +56,7 @@
|
|||
android:background="?attr/colorPrimary"
|
||||
android:theme="@style/ThemeOverlay.AppCompat.ActionBar"
|
||||
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_constraintTop_toTopOf="parent"
|
||||
android:layout_marginLeft="-4dp"
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@
|
|||
|
||||
app:layout_constraintStart_toStartOf="@id/imageGuideline"
|
||||
app:layout_constraintTop_toTopOf="@id/seek_bar_volume"
|
||||
app:srcCompat="@drawable/actionbar_logo"
|
||||
app:srcCompat="@drawable/logo_roundsquare"
|
||||
tools:ignore="HardcodedText" />
|
||||
|
||||
<androidx.constraintlayout.widget.Guideline
|
||||
|
|
@ -72,7 +72,8 @@
|
|||
app:layout_constraintHorizontal_bias="1.0"
|
||||
app:layout_constraintVertical_weight="3"
|
||||
app:layout_constraintStart_toStartOf="@id/streamerPicture"
|
||||
app:layout_constraintTop_toBottomOf="@id/streamerPicture" />
|
||||
app:layout_constraintTop_toBottomOf="@id/streamerPicture"
|
||||
android:visibility="gone"/>
|
||||
<TextView
|
||||
android:id="@+id/listenersCount"
|
||||
android:layout_width="0dp"
|
||||
|
|
@ -87,7 +88,9 @@
|
|||
app:layout_constraintBottom_toTopOf="@id/topInfoGuideline"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
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
|
||||
android:id="@+id/topInfoGuideline"
|
||||
|
|
@ -170,6 +173,7 @@
|
|||
android:orientation="vertical"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:layout_marginRight="8dp"
|
||||
android:visibility="gone"
|
||||
>
|
||||
|
||||
<TextView
|
||||
|
|
@ -287,6 +291,7 @@
|
|||
|
||||
<!-- android:progressDrawable="@drawable/progress_bar_progress" -->
|
||||
|
||||
<!-- REMOVE VISIBILITY GONE IF YOU HAVE TIME VALUES TO DISPLAY THE PROGRESS BAR -->
|
||||
<ProgressBar
|
||||
android:id="@+id/progressBar"
|
||||
style="@style/Widget.AppCompat.ProgressBar.Horizontal"
|
||||
|
|
@ -297,8 +302,10 @@
|
|||
android:progress="70"
|
||||
android:progressDrawable="@drawable/progress_bar_progress"
|
||||
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
|
||||
android:id="@+id/endTime"
|
||||
android:layout_width="44sp"
|
||||
|
|
@ -308,8 +315,10 @@
|
|||
android:text=""
|
||||
android:textAlignment="textEnd"
|
||||
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
|
||||
android:id="@+id/currentTime"
|
||||
android:layout_width="44sp"
|
||||
|
|
@ -319,7 +328,8 @@
|
|||
android:text=""
|
||||
android:textAlignment="textStart"
|
||||
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:title="@string/title_news"/>
|
||||
|
||||
<item
|
||||
android:id="@+id/navigation_chat"
|
||||
android:icon="@drawable/ic_chat_processing"
|
||||
android:title="@string/title_chat"/>
|
||||
|
||||
</menu>
|
||||
|
|
|
|||
|
|
@ -14,11 +14,12 @@
|
|||
android:icon="@drawable/ic_alarm"
|
||||
app:showAsAction="ifRoom"/>
|
||||
|
||||
|
||||
<!--
|
||||
<item
|
||||
android:id="@+id/action_refresh"
|
||||
android:title="@string/action_refresh"
|
||||
app:showAsAction="never"/>
|
||||
-->
|
||||
|
||||
<!--
|
||||
<item
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@drawable/ic_launcher_background"/>
|
||||
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||
<background android:drawable="@color/ic_launcher_background"/>
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
||||
</adaptive-icon>
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@drawable/ic_launcher_background"/>
|
||||
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||
<background android:drawable="@color/ic_launcher_background"/>
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
||||
</adaptive-icon>
|
||||
|
Före Bredd: | Höjd: | Storlek: 2.4 KiB Efter Bredd: | Höjd: | Storlek: 3.7 KiB |
|
Före Bredd: | Höjd: | Storlek: 3.0 KiB |
|
Före Bredd: | Höjd: | Storlek: 4.2 KiB Efter Bredd: | Höjd: | Storlek: 5.7 KiB |
|
Före Bredd: | Höjd: | Storlek: 1.7 KiB Efter Bredd: | Höjd: | Storlek: 2.4 KiB |
|
Före Bredd: | Höjd: | Storlek: 1.9 KiB |
|
Före Bredd: | Höjd: | Storlek: 2.6 KiB Efter Bredd: | Höjd: | Storlek: 3.4 KiB |
|
Före Bredd: | Höjd: | Storlek: 3.3 KiB Efter Bredd: | Höjd: | Storlek: 5.3 KiB |
|
Före Bredd: | Höjd: | Storlek: 4.2 KiB |
|
Före Bredd: | Höjd: | Storlek: 5.9 KiB Efter Bredd: | Höjd: | Storlek: 8.1 KiB |
|
Före Bredd: | Höjd: | Storlek: 5.1 KiB Efter Bredd: | Höjd: | Storlek: 8.5 KiB |
|
Före Bredd: | Höjd: | Storlek: 6.9 KiB |
|
Före Bredd: | Höjd: | Storlek: 9.1 KiB Efter Bredd: | Höjd: | Storlek: 13 KiB |
|
Före Bredd: | Höjd: | Storlek: 6.9 KiB Efter Bredd: | Höjd: | Storlek: 12 KiB |
|
Före Bredd: | Höjd: | Storlek: 9.8 KiB |
|
Före Bredd: | Höjd: | Storlek: 13 KiB Efter Bredd: | Höjd: | Storlek: 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_refresh">Force data refresh</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="ra_f">.ra f</string>
|
||||
|
||||
|
|
|
|||
|
|
@ -6,13 +6,13 @@
|
|||
<item name="colorPrimary">@color/colorPrimary</item>
|
||||
<item name="colorPrimaryDark">@color/colorPrimaryDark</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:colorBackground">@color/colorPrimaryDark</item>
|
||||
</style>
|
||||
|
||||
<style name="AppTheme.Parameters" parent="AppTheme">
|
||||
<item name="icon" />
|
||||
<item name="icon"/>
|
||||
</style>
|
||||
|
||||
<style name="AppTheme.BottomBar" parent="AppTheme">
|
||||
|
|
|
|||
|
|
@ -2,14 +2,6 @@
|
|||
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
|
||||
|
||||
<EditTextPreference
|
||||
app:iconSpaceReserved="false"
|
||||
app:key="userName"
|
||||
app:title="User name for favorite songs"
|
||||
/>
|
||||
|
||||
<SeekBarPreference
|
||||
app:key="volume"
|
||||
app:iconSpaceReserved="false"
|
||||
|
|
@ -34,24 +26,7 @@
|
|||
app:iconSpaceReserved="false"
|
||||
app:title="Make Snackbars persistent"
|
||||
app:singleLineTitle="false"
|
||||
app:defaultValue="true"
|
||||
/>
|
||||
|
||||
<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"
|
||||
app:defaultValue="false"
|
||||
/>
|
||||
|
||||
</PreferenceScreen>
|
||||
|
|
@ -25,13 +25,6 @@
|
|||
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
|
||||
app:key="submitBug"
|
||||
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>
|
||||