merged api
commit
f133bea0ac
|
|
@ -29,8 +29,8 @@ android {
|
|||
applicationId "fr.forum_thalie.tsumugi"
|
||||
minSdkVersion 16
|
||||
targetSdkVersion 29
|
||||
versionCode 100
|
||||
versionName "1.0.0"
|
||||
versionCode 110
|
||||
versionName "1.1.0"
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
vectorDrawables.useSupportLibrary = true
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,9 +31,6 @@
|
|||
<action android:name="android.media.browse.MediaBrowserService" />
|
||||
</intent-filter>
|
||||
</service>
|
||||
<service android:name=".streamerNotificationService.StreamerMonitorService"
|
||||
android:enabled="true"
|
||||
android:exported="true"/>
|
||||
|
||||
<receiver android:name=".BootBroadcastReceiver"
|
||||
android:directBootAware="true"
|
||||
|
|
|
|||
|
|
@ -22,7 +22,10 @@ class BootBroadcastReceiver : BroadcastReceiver(){
|
|||
|
||||
if (arg1.getStringExtra("action") == "$tag.${Actions.PLAY_OR_FALLBACK.name}" )
|
||||
{
|
||||
|
||||
RadioAlarm.instance.setNextAlarm(context) // schedule next alarm
|
||||
if (!PlayerStore.instance.isInitialized)
|
||||
PlayerStore.instance.initApi()
|
||||
if (PlayerStore.instance.streamerName.value.isNullOrBlank())
|
||||
PlayerStore.instance.initPicture(context)
|
||||
|
||||
|
|
|
|||
|
|
@ -129,6 +129,12 @@ class MainActivity : BaseActivity() {
|
|||
colorGreenListCompat = (ResourcesCompat.getColorStateList(resources, R.color.button_green_compat, null))
|
||||
colorAccent = (ResourcesCompat.getColor(resources, R.color.colorAccent, null))
|
||||
|
||||
// fetch program
|
||||
Planning.instance.parseUrl(/* getString(R.string.planning_url) */ context = this)
|
||||
|
||||
PlayerStore.instance.initUrl(this)
|
||||
PlayerStore.instance.initApi()
|
||||
|
||||
// Post-UI Launch
|
||||
if (PlayerStore.instance.isInitialized)
|
||||
{
|
||||
|
|
@ -160,9 +166,6 @@ class MainActivity : BaseActivity() {
|
|||
isTimerStarted = true
|
||||
}
|
||||
|
||||
// fetch program
|
||||
Planning.instance.parseUrl(/* getString(R.string.planning_url) */ context = this)
|
||||
|
||||
// initialize the UI
|
||||
setTheme(R.style.AppTheme)
|
||||
setContentView(R.layout.activity_main)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package fr.forum_thalie.tsumugi
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.MenuItem
|
||||
import fr.forum_thalie.tsumugi.preferences.*
|
||||
|
||||
|
||||
|
|
@ -38,4 +39,15 @@ class ParametersActivity : BaseActivity() {
|
|||
.replace(R.id.parameters_host_container, fragmentToLoad)
|
||||
.commit()
|
||||
}
|
||||
|
||||
// Make the Up button function as back instead of always bringing us to the main activity
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
return when (item.itemId) {
|
||||
android.R.id.home -> {
|
||||
onBackPressed()
|
||||
true
|
||||
}
|
||||
else -> super.onOptionsItemSelected(item)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -124,10 +124,32 @@ class RadioService : MediaBrowserServiceCompat() {
|
|||
|
||||
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.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 == noConnectionValue -> {
|
||||
PlayerStore.instance.isStreamDown = true
|
||||
}
|
||||
else -> {
|
||||
PlayerStore.instance.fetchApi(numberOfSongs >= 2)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (PlayerStore.instance.currentSong != PlayerStore.instance.currentSongBackup
|
||||
&& it != noConnectionValue)
|
||||
{
|
||||
PlayerStore.instance.updateLp()
|
||||
PlayerStore.instance.updateQueue()
|
||||
}
|
||||
nowPlayingNotification.update(this)
|
||||
Planning.instance.checkProgramme()
|
||||
|
|
@ -163,6 +185,12 @@ class RadioService : MediaBrowserServiceCompat() {
|
|||
|
||||
preferenceStore = PreferenceManager.getDefaultSharedPreferences(this)
|
||||
|
||||
// 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)
|
||||
|
||||
// Define managers
|
||||
telephonyManager = getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManager
|
||||
telephonyManager?.listen(phoneStateListener, PhoneStateListener.LISTEN_CALL_STATE)
|
||||
|
|
|
|||
|
|
@ -1,8 +1,18 @@
|
|||
package fr.forum_thalie.tsumugi
|
||||
|
||||
import android.support.v4.media.session.PlaybackStateCompat
|
||||
import fr.forum_thalie.tsumugi.playerstore.PlayerStore
|
||||
import java.util.*
|
||||
|
||||
class ApiFetchTick : TimerTask() {
|
||||
override fun run() {
|
||||
if (PlayerStore.instance.playbackState.value == PlaybackStateCompat.STATE_STOPPED)
|
||||
{
|
||||
PlayerStore.instance.fetchApi()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class Tick : TimerTask() {
|
||||
override fun run() {
|
||||
PlayerStore.instance.currentTime.postValue(PlayerStore.instance.currentTime.value!! + 500)
|
||||
|
|
|
|||
|
|
@ -7,9 +7,16 @@ import android.support.v4.media.session.PlaybackStateCompat
|
|||
import android.util.Log
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import fr.forum_thalie.tsumugi.*
|
||||
import org.json.JSONObject
|
||||
import java.net.URL
|
||||
import java.text.ParseException
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
import kotlin.collections.ArrayList
|
||||
|
||||
class PlayerStore {
|
||||
|
||||
private lateinit var urlToScrape: String
|
||||
val isPlaying: MutableLiveData<Boolean> = MutableLiveData()
|
||||
val isServiceStarted: MutableLiveData<Boolean> = MutableLiveData()
|
||||
val volume: MutableLiveData<Int> = MutableLiveData()
|
||||
|
|
@ -27,6 +34,7 @@ class PlayerStore {
|
|||
val listenersCount: MutableLiveData<Int> = MutableLiveData()
|
||||
var latencyCompensator : Long = 0
|
||||
var isInitialized: Boolean = false
|
||||
var isStreamDown: Boolean = false
|
||||
|
||||
init {
|
||||
playbackState.value = PlaybackStateCompat.STATE_STOPPED
|
||||
|
|
@ -43,10 +51,131 @@ class PlayerStore {
|
|||
listenersCount.value = 0
|
||||
}
|
||||
|
||||
fun initUrl(c: Context)
|
||||
{
|
||||
urlToScrape = c.getString(R.string.API_URL)
|
||||
}
|
||||
|
||||
private fun getTimestamp(s: String) : Long
|
||||
{
|
||||
val dateFormat = SimpleDateFormat("yyyy-MM-dd hh:mm:ss", Locale.getDefault())
|
||||
try {
|
||||
val t: Date? = dateFormat.parse(s)
|
||||
return t!!.time
|
||||
} catch (e: ParseException) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
|
||||
// ##################################################
|
||||
// ################# API FUNCTIONS ##################
|
||||
// ##################################################
|
||||
|
||||
private fun updateApi(res: 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.
|
||||
|
||||
val resMain = res.getJSONObject("tracks").getJSONObject("current")
|
||||
val s = extractSong(resMain)
|
||||
if (playbackState.value != PlaybackStateCompat.STATE_PLAYING || currentSong.title.value.isNullOrEmpty()
|
||||
|| currentSong.title.value == noConnectionValue)
|
||||
currentSong.setTitleArtist("${s.artist.value} - ${s.title.value}")
|
||||
|
||||
val starts = s.startTime.value
|
||||
val ends = s.stopTime.value
|
||||
|
||||
if (currentSong.startTime.value != starts)
|
||||
currentSong.startTime.value = starts
|
||||
|
||||
currentSong.stopTime.value = ends
|
||||
|
||||
// I noticed that the server has a big (3 to 9 seconds !!) offset for current time.
|
||||
// we can measure it when the player is playing, to compensate it and have our progress bar perfectly timed
|
||||
// latencyCompensator is set to null when beginPlaying() (we can't measure it at the moment we start playing, since we're in the middle of a song),
|
||||
// at this moment, we set it to 0. Then, next time the updateApi is called when we're playing, we measure the latency and we set out latencyComparator.
|
||||
if(isCompensatingLatency)
|
||||
{
|
||||
latencyCompensator = getTimestamp(res.getJSONObject("station").getString("schedulerTime")) - (currentSong.startTime.value ?: getTimestamp(res.getJSONObject("station").getString("schedulerTime")))
|
||||
Log.d(tag, "latency compensator set to ${(latencyCompensator).toFloat()/1000} s")
|
||||
}
|
||||
currentTime.value = getTimestamp(res.getJSONObject("station").getString("schedulerTime")) - (latencyCompensator)
|
||||
|
||||
/*
|
||||
val listeners = resMain.getInt("listeners")
|
||||
listenersCount.value = listeners
|
||||
Log.d(tag, playerStoreTag + "store updated")
|
||||
*/
|
||||
}
|
||||
|
||||
private val scrape : (Any?) -> String =
|
||||
{
|
||||
URL(urlToScrape).readText()
|
||||
}
|
||||
|
||||
/* initApi is called :
|
||||
- at startup
|
||||
- when a streamer changes.
|
||||
the idea is to fetch the queue when a streamer changes (potentially Hanyuu), and at startup.
|
||||
The Last Played is only fetched if it's empty (so, only at startup), not when a streamer changes.
|
||||
*/
|
||||
fun initApi()
|
||||
{
|
||||
val post : (parameter: Any?) -> Unit = {
|
||||
val result = JSONObject(it as String)
|
||||
if (result.has("tracks"))
|
||||
{
|
||||
updateApi(result)
|
||||
currentSongBackup.copy(currentSong)
|
||||
|
||||
isQueueUpdated.value = true
|
||||
|
||||
isLpUpdated.value = true
|
||||
}
|
||||
isInitialized = true
|
||||
}
|
||||
Async(scrape, post)
|
||||
}
|
||||
|
||||
fun fetchApi(isCompensatingLatency: Boolean = false) {
|
||||
val post: (parameter: Any?) -> Unit = {
|
||||
val result = JSONObject(it as String)
|
||||
if (!result.isNull("tracks"))
|
||||
{
|
||||
updateApi(result, isCompensatingLatency)
|
||||
}
|
||||
}
|
||||
Async(scrape, post)
|
||||
}
|
||||
|
||||
private fun extractSong(songJSON: JSONObject) : Song {
|
||||
val song = Song()
|
||||
song.setTitleArtist(songJSON.getString("name"))
|
||||
song.startTime.value = getTimestamp(songJSON.getString("starts"))
|
||||
song.stopTime.value = getTimestamp(songJSON.getString("ends"))
|
||||
song.type.value = 0 // only used for R/a/dio
|
||||
return song
|
||||
}
|
||||
|
||||
|
||||
// ##################################################
|
||||
// ############## QUEUE / LP FUNCTIONS ##############
|
||||
// ##################################################
|
||||
|
||||
fun updateQueue() {
|
||||
if (queue.isNotEmpty()) {
|
||||
queue.remove(queue.first())
|
||||
Log.d(tag, queue.toString())
|
||||
fetchLastRequest()
|
||||
isQueueUpdated.value = true
|
||||
} else if (isInitialized) {
|
||||
fetchLastRequest()
|
||||
} else {
|
||||
Log.d(tag, "queue is empty! fetching anyway !!")
|
||||
fetchLastRequest()
|
||||
}
|
||||
}
|
||||
|
||||
fun updateLp() {
|
||||
// note : lp is empty at initialization. This check was needed when we used the R/a/dio API.
|
||||
//if (lp.isNotEmpty()){
|
||||
|
|
@ -61,6 +190,74 @@ class PlayerStore {
|
|||
}
|
||||
|
||||
|
||||
private fun fetchLastRequest()
|
||||
{
|
||||
val sleepScrape: (Any?) -> String = {
|
||||
/* we can maximize our chances to retrieve the last queued song by specifically waiting for the number of seconds we measure between ICY metadata and API change.
|
||||
we add 2 seconds just to get a higher probability that the API has correctly updated. (the latency compensator can have a jitter of 1 second usually)
|
||||
If, against all odds, the API hasn't updated yet, we will retry in the same amount of seconds. So we'll have the data anyway.
|
||||
This way to fetch at the most probable time is a good compromise between fetch speed and fetch frequency
|
||||
We don't fetch too often, and we start to fetch at the most *probable* time.
|
||||
If there's no latencyCompensator measured yet, we only wait for 3 seconds.
|
||||
If the song is the same, it will be called again. 3 seconds is a good compromise between speed and frequency:
|
||||
it might be called twice, rarely 3 times, and it's only the 2 first songs ; after these, the latencyCompensator is set to fetch at the most probable time.
|
||||
*/
|
||||
val sleepTime: Long = if (latencyCompensator > 0) latencyCompensator + 2000 else 3000
|
||||
Thread.sleep(sleepTime) // we wait a bit (10s) for the API to get updated on R/a/dio side!
|
||||
URL(urlToScrape).readText()
|
||||
}
|
||||
|
||||
lateinit var post: (parameter: Any?) -> Unit
|
||||
|
||||
fun postFun(result: JSONObject)
|
||||
{
|
||||
if (result.has("tracks")) {
|
||||
val resMain = result.getJSONObject("tracks")
|
||||
/*
|
||||
if ((resMain.has("isafkstream") && !resMain.getBoolean("isafkstream")) &&
|
||||
queue.isNotEmpty())
|
||||
{
|
||||
queue.clear() //we're not requesting anything anymore.
|
||||
isQueueUpdated.value = true
|
||||
} else if (resMain.has("isafkstream") && resMain.getBoolean("isafkstream") &&
|
||||
queue.isEmpty())
|
||||
{
|
||||
initApi()
|
||||
} else
|
||||
*/
|
||||
if (resMain.has("next") /*&& queue.isNotEmpty()*/) {
|
||||
val queueJSON =
|
||||
resMain.getJSONObject("next")
|
||||
val t = extractSong(queueJSON)
|
||||
if (queue.isNotEmpty() && t == queue.last())
|
||||
{
|
||||
Log.d(tag, playerStoreTag + "Song already in there: $t")
|
||||
Async(sleepScrape, post)
|
||||
} else {
|
||||
queue.add(queue.size, t)
|
||||
Log.d(tag, playerStoreTag + "added last queue song: $t")
|
||||
isQueueUpdated.value = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
post = {
|
||||
val result = JSONObject(it as String)
|
||||
/* The goal is to pass the result to a function that will process it (postFun).
|
||||
The magic trick is, under circumstances, the last queue song might not have been updated yet when we fetch it.
|
||||
So if this is detected ==> if (t == queue.last() )
|
||||
Then the function re-schedule an Async(sleepScrape, post).
|
||||
To do that, the "post" must be defined BEFORE the function, but the function must be defined BEFORE the "post" value.
|
||||
So I declare "post" as lateinit var, define the function, then define the "post" that calls the function. IT SHOULD WORK.
|
||||
*/
|
||||
postFun(result)
|
||||
}
|
||||
|
||||
Async(sleepScrape, post)
|
||||
}
|
||||
|
||||
|
||||
// ##################################################
|
||||
// ############## PICTURE FUNCTIONS #################
|
||||
// ##################################################
|
||||
|
|
|
|||
|
|
@ -1,9 +1,6 @@
|
|||
package fr.forum_thalie.tsumugi.preferences
|
||||
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.preference.*
|
||||
import fr.forum_thalie.tsumugi.R
|
||||
|
|
@ -40,5 +37,25 @@ class CustomizeFragment : PreferenceFragmentCompat() {
|
|||
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.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
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -49,12 +49,12 @@ class NowPlayingFragment : Fragment() {
|
|||
val volumeText: TextView = root.findViewById(R.id.volume_text)
|
||||
val progressBar: ProgressBar = root.findViewById(R.id.progressBar)
|
||||
val volumeIconImage : ImageView = root.findViewById(R.id.volume_icon)
|
||||
|
||||
val currentProgrammeText: TextView = root.findViewById(R.id.text_current_programme)
|
||||
val streamerPictureImageView: ImageView = root.findViewById(R.id.streamerPicture)
|
||||
|
||||
// Note: these values are not used in the generic app, but if you want to, you can use them.
|
||||
val songTitleNextText: TextView = root.findViewById(R.id.text_song_title_next)
|
||||
//val songArtistNextText: TextView = root.findViewById(R.id.text_song_artist_next)
|
||||
val songArtistNextText: TextView = root.findViewById(R.id.text_song_artist_next)
|
||||
|
||||
/*
|
||||
val streamerNameText : TextView = root.findViewById(R.id.streamerName)
|
||||
|
|
@ -69,15 +69,15 @@ class NowPlayingFragment : Fragment() {
|
|||
listenersText,8, 16, 2, TypedValue.COMPLEX_UNIT_SP)
|
||||
*/
|
||||
|
||||
/*
|
||||
|
||||
// trick : I can't observe the queue because it's an ArrayDeque that doesn't trigger any change...
|
||||
// so I observe a dedicated Mutable that gets set when the queue is updated.
|
||||
PlayerStore.instance.isQueueUpdated.observe(viewLifecycleOwner, Observer {
|
||||
val t = if (PlayerStore.instance.queue.size > 0) PlayerStore.instance.queue[0] else Song("No queue - ") // (it.peekFirst != null ? it.peekFirst : Song() )
|
||||
val t = if (PlayerStore.instance.queue.size > 0) PlayerStore.instance.queue[0] else Song(noConnectionValue) // (it.peekFirst != null ? it.peekFirst : Song() )
|
||||
songTitleNextText.text = t.title.value
|
||||
songArtistNextText.text = t.artist.value
|
||||
})
|
||||
|
||||
/*
|
||||
PlayerStore.instance.streamerName.observe(viewLifecycleOwner, Observer {
|
||||
streamerNameText.text = it
|
||||
})
|
||||
|
|
@ -92,10 +92,12 @@ class NowPlayingFragment : Fragment() {
|
|||
songTitleText.text = it
|
||||
})
|
||||
|
||||
|
||||
Planning.instance.currentProgramme.observe(viewLifecycleOwner, Observer {
|
||||
songTitleNextText.text = it
|
||||
currentProgrammeText.text = it
|
||||
})
|
||||
|
||||
|
||||
PlayerStore.instance.currentSong.artist.observe(viewLifecycleOwner, Observer {
|
||||
songArtistText.text = it
|
||||
})
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ class ProgrammeFragment : Fragment() {
|
|||
|
||||
viewPager.adapter = adapter
|
||||
val todaySundayFirst = Calendar.getInstance(Planning.instance.timeZone).get(Calendar.DAY_OF_WEEK) - 1
|
||||
viewPager.currentItem = (todaySundayFirst - 1)%7
|
||||
viewPager.currentItem = (todaySundayFirst - 1 + 7)%7 // don't do modulos on negative, seems like it's weird
|
||||
|
||||
val tabLayout : TabLayout = root.findViewById(R.id.dayTabLayout)
|
||||
tabLayout.setupWithViewPager(viewPager)
|
||||
|
|
|
|||
|
|
@ -93,13 +93,6 @@
|
|||
android:visibility="gone"
|
||||
/>
|
||||
|
||||
<androidx.constraintlayout.widget.Guideline
|
||||
android:id="@+id/topInfoGuideline"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
app:layout_constraintGuide_percent="0.95" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/sleepInfo"
|
||||
android:layout_width="0dp"
|
||||
|
|
@ -162,7 +155,8 @@
|
|||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:fillViewport="true"
|
||||
app:layout_constraintBottom_toTopOf="@id/topInfoGuideline"
|
||||
android:layout_marginBottom="4dp"
|
||||
app:layout_constraintBottom_toTopOf="@id/text_current_programme"
|
||||
app:layout_constraintEnd_toStartOf="@id/streamerPicture"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/seek_bar_volume">
|
||||
|
|
@ -184,7 +178,7 @@
|
|||
android:layout_gravity="top"
|
||||
android:text="@string/up_next"
|
||||
android:textAlignment="center"
|
||||
android:textColor="@color/whited3"
|
||||
android:textColor="@color/whited5"
|
||||
android:layout_marginTop="8dp"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
|
|
@ -199,10 +193,10 @@
|
|||
android:text=""
|
||||
android:textAlignment="center"
|
||||
android:textColor="@color/whited3"
|
||||
android:textSize="16sp"
|
||||
android:textSize="14sp"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/upNext"
|
||||
android:visibility="gone"/>
|
||||
android:visibility="visible"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_song_title_next"
|
||||
|
|
@ -212,14 +206,50 @@
|
|||
android:text=""
|
||||
android:textAlignment="center"
|
||||
android:textColor="@color/whited"
|
||||
android:textSize="16sp"
|
||||
android:textSize="14sp"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/text_song_artist_next" />
|
||||
app:layout_constraintTop_toBottomOf="@id/text_song_artist_next"
|
||||
android:visibility="visible" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</ScrollView>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/current_programme"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="bottom"
|
||||
android:text="@string/current_programme"
|
||||
android:textAlignment="center"
|
||||
android:textColor="@color/whited3"
|
||||
android:textSize="14sp"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="@id/topInfoGuideline"
|
||||
android:visibility="visible" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_current_programme"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:text=""
|
||||
android:layout_marginStart="8sp"
|
||||
android:layout_marginLeft="8sp"
|
||||
android:gravity="start|bottom"
|
||||
android:textAlignment="textStart"
|
||||
android:textColor="@color/whited"
|
||||
android:textSize="14sp"
|
||||
app:layout_constraintStart_toEndOf="@id/current_programme"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="@id/topInfoGuideline"
|
||||
android:visibility="visible"
|
||||
/>
|
||||
|
||||
<androidx.constraintlayout.widget.Guideline
|
||||
android:id="@+id/topInfoGuideline"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
app:layout_constraintGuide_percent="0.97" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
|
|
@ -268,7 +298,7 @@
|
|||
android:text=""
|
||||
android:textAlignment="center"
|
||||
android:textColor="@color/whited"
|
||||
android:textSize="20sp"
|
||||
android:textSize="18sp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent" />
|
||||
|
||||
|
|
@ -280,7 +310,7 @@
|
|||
android:text=""
|
||||
android:textAlignment="center"
|
||||
android:textColor="@color/whited3"
|
||||
android:textSize="20sp"
|
||||
android:textSize="18sp"
|
||||
app:layout_constraintBottom_toTopOf="@id/text_song_title"
|
||||
app:layout_constraintStart_toStartOf="parent" />
|
||||
|
||||
|
|
@ -305,7 +335,7 @@
|
|||
android:progressDrawable="@drawable/progress_bar_progress"
|
||||
app:layout_constraintBottom_toTopOf="@id/play_pause"
|
||||
tools:layout_editor_absoluteX="0dp"
|
||||
android:visibility="gone"/>
|
||||
android:visibility="visible"/>
|
||||
|
||||
<!-- REMOVE VISIBILITY GONE IF YOU HAVE TIME VALUES TO DISPLAY THE PROGRESS BAR -->
|
||||
<TextView
|
||||
|
|
@ -318,7 +348,7 @@
|
|||
android:textAlignment="textEnd"
|
||||
app:layout_constraintEnd_toEndOf="@id/progressBar"
|
||||
app:layout_constraintTop_toBottomOf="@id/progressBar"
|
||||
android:visibility="gone"/>
|
||||
android:visibility="visible"/>
|
||||
|
||||
<!-- REMOVE VISIBILITY GONE IF YOU HAVE TIME VALUES TO DISPLAY THE PROGRESS BAR -->
|
||||
<TextView
|
||||
|
|
@ -331,7 +361,7 @@
|
|||
android:textAlignment="textStart"
|
||||
app:layout_constraintStart_toStartOf="@id/progressBar"
|
||||
app:layout_constraintTop_toBottomOf="@id/progressBar"
|
||||
android:visibility="gone"/>
|
||||
android:visibility="visible"/>
|
||||
|
||||
|
||||
|
||||
|
|
@ -381,7 +411,7 @@
|
|||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
app:layout_constraintGuide_percent="0.33" />
|
||||
app:layout_constraintGuide_percent="0.38" />
|
||||
|
||||
<androidx.constraintlayout.widget.Guideline
|
||||
android:id="@+id/splitHorizontalLayout"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,31 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
|
||||
<string-array name="snoozeValues">
|
||||
<item name="0">@string/disable</item>
|
||||
<item name="1">1</item>
|
||||
<item name="2">2</item>
|
||||
<item name="5">5</item>
|
||||
<item name="10">10</item>
|
||||
<item name="15">15</item>
|
||||
<item name="20">20</item>
|
||||
<item name="25">25</item>
|
||||
<item name="30">30</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="fetchPeriodString">
|
||||
<item name="5">Toutes les 5 secondes (mise à jour rapide)</item>
|
||||
<item name="10">Toutes les 10 secondes</item>
|
||||
<item name="15">Toutes les 15 secondes</item>
|
||||
<item name="20">Toutes les 20 secondes</item>
|
||||
<item name="30">Toutes les 30 secondes (moins d\'utilisation de batterie)</item>
|
||||
</string-array>
|
||||
<string-array name="fetchPeriodValues">
|
||||
<item name="5">5</item>
|
||||
<item name="10">10</item>
|
||||
<item name="15">15</item>
|
||||
<item name="20">20</item>
|
||||
<item name="30">30</item>
|
||||
</string-array>
|
||||
|
||||
</resources>
|
||||
|
|
@ -10,7 +10,8 @@
|
|||
|
||||
<string name="volume">Volume : </string>
|
||||
|
||||
<string name="up_next">Émission en cours :</string>
|
||||
<string name="up_next">Prochain titre :</string>
|
||||
<string name="current_programme">Émission en cours : </string>
|
||||
<string name="now_streaming">En cours</string>
|
||||
<string name="error_webView">Erreur du chargement de WebView. Téléchargez Google Chrome sur le Play Store, ou activez le si vous l\'avez désactivé.</string>
|
||||
<string name="action_settings">Paramètres</string>
|
||||
|
|
@ -50,5 +51,6 @@
|
|||
<string name="sleepClosesApp">Minuterie avant fermeture de l\'application</string>
|
||||
<string name="setSleepDuration">Choisir une durée (en minutes)</string>
|
||||
<string name="willCloseIn">Extinction dans %1$d minutes</string>
|
||||
<string name="fetchPeriod">Choisir la fréquence de mise à jour quand la radio est stoppée</string>
|
||||
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -13,4 +13,19 @@
|
|||
<item name="30">30</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="fetchPeriodString">
|
||||
<item name="5">Every 5 seconds (faster update)</item>
|
||||
<item name="10">Every 10 seconds</item>
|
||||
<item name="15">Every 15 seconds</item>
|
||||
<item name="20">Every 20 seconds</item>
|
||||
<item name="30">Every 30 seconds (less battery-intensive)</item>
|
||||
</string-array>
|
||||
<string-array name="fetchPeriodValues">
|
||||
<item name="5">5</item>
|
||||
<item name="10">10</item>
|
||||
<item name="15">15</item>
|
||||
<item name="20">20</item>
|
||||
<item name="30">30</item>
|
||||
</string-array>
|
||||
|
||||
</resources>
|
||||
|
|
@ -11,7 +11,7 @@
|
|||
<color name="whited3">#999999</color>
|
||||
<color name="whited4">#999999</color>
|
||||
<color name="whited5">#7F7F7F</color>
|
||||
<color name="rblue">#4FC3F7</color>
|
||||
<color name="rblue">#64B5F6</color>
|
||||
<color name="bluereq">#527a8e</color>
|
||||
<color name="blueTitle">#527a8e</color>
|
||||
<color name="reqButtonPressed">#5cb85c</color>
|
||||
|
|
@ -20,10 +20,10 @@
|
|||
|
||||
<color name="seek_bar_background">#585858</color>
|
||||
<color name="seek_bar_progress">#f58b01</color>
|
||||
<color name="seek_bar_secondary_progress">#4FC3F7</color>
|
||||
<color name="seek_bar_secondary_progress">#64B5F6</color>
|
||||
|
||||
<color name="progress_bar_background">#585858</color>
|
||||
<color name="progress_bar_progress">#4FC3F7</color>
|
||||
<color name="progress_bar_progress">#64B5F6</color>
|
||||
<color name="progress_bar_secondary_progress">#f58b01</color>
|
||||
|
||||
<color name="white">#FFFFFF</color>
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@
|
|||
<string name="github_url_new_issue">https://github.com/yattoz/Tsumugi-app/issues/</string>
|
||||
<string name="website_url">https://tsumugi.forum-thalie.fr/</string>
|
||||
<string name="rss_url">https://tsumugi.forum-thalie.fr/?feed=rss2</string>
|
||||
<string name="API_URL">https://radio.mahoro-net.org/airtime/api/live-info-v2</string>
|
||||
<string name="planning_url">ADD SOME URL HERE</string>
|
||||
|
||||
|
||||
|
|
@ -24,7 +25,8 @@
|
|||
|
||||
<string name="volume">Volume: </string>
|
||||
|
||||
<string name="up_next">Émission en cours :</string>
|
||||
<string name="up_next">Up next :</string>
|
||||
<string name="current_programme">Current programme : </string>
|
||||
<string name="now_streaming">Now streaming</string>
|
||||
<string name="error_webView">Error loading WebView. Try downloading Google Chrome on Google Play, or enabling it if you disabled it.</string>
|
||||
<string name="action_settings">Settings</string>
|
||||
|
|
@ -69,5 +71,6 @@
|
|||
<string name="sleepClosesApp">Sleep - close app after some time</string>
|
||||
<string name="setSleepDuration">Set duration (minutes)</string>
|
||||
<string name="willCloseIn">Will close in %1$d minutes</string>
|
||||
<string name="fetchPeriod">Set update period when stopped</string>
|
||||
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -29,4 +29,14 @@
|
|||
app:defaultValue="false"
|
||||
/>
|
||||
|
||||
<ListPreference
|
||||
app:key="fetchPeriod"
|
||||
app:iconSpaceReserved="false"
|
||||
android:title="@string/fetchPeriod"
|
||||
app:singleLineTitle="false"
|
||||
android:entries="@array/fetchPeriodString"
|
||||
android:entryValues="@array/fetchPeriodValues"
|
||||
android:defaultValue="10"
|
||||
/>
|
||||
|
||||
</PreferenceScreen>
|
||||
Loading…
Reference in New Issue