removed many R/a/dio specifc code. Hid some UI that could be used later.

remotes/origin/devel
yattoz 2019-12-07 22:10:52 +01:00
parent d1fb81f615
commit 6b1c018318
81 changed files with 163 additions and 1800 deletions

View File

@ -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>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 44 KiB

View File

@ -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) {

View File

@ -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)

View File

@ -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)

View File

@ -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()
}

View File

@ -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)

View File

@ -11,11 +11,3 @@ class Tick : TimerTask() {
}
}
class ApiFetchTick : TimerTask() {
override fun run() {
if (PlayerStore.instance.playbackState.value == PlaybackStateCompat.STATE_STOPPED)
{
PlayerStore.instance.fetchApi()
}
}
}

View File

@ -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
)
}

View File

@ -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
}
}
}

View File

@ -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
}
}
}

View File

@ -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()
}
}

View File

@ -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) {
}
}

View File

@ -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()
}
}

View File

@ -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()
}
}

View File

@ -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.
}
}
}

View File

@ -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
}
}

View File

@ -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
}

View File

@ -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")
}
}

View File

@ -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()
}
}

View File

@ -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()
}
}

View File

@ -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.
}

View File

@ -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()
}
}

View File

@ -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()
}
}

View File

@ -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
}
}

View File

@ -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()
}
}

View File

@ -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()
}
}
}

View File

@ -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>

View File

@ -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>

View File

@ -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>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 204 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 213 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 402 B

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 143 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 165 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 318 B

After

Width:  |  Height:  |  Size: 659 B

View File

@ -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"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 236 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 249 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 494 B

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 321 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 353 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 698 B

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

File diff suppressed because one or more lines are too long

View File

@ -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>

View File

@ -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>

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 110 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

View File

@ -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>

View File

@ -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>

View File

@ -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"

View File

@ -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"/>

View File

@ -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>

View File

@ -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

View File

@ -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>

View File

@ -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>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.9 KiB

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.1 KiB

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.1 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.9 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 19 KiB

View File

@ -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>

View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">#F58B01</color>
</resources>

View File

@ -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>

View File

@ -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">

View File

@ -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>

View File

@ -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)"

View File

@ -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>