| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314 |
- package com.musicweb.player
- import android.annotation.SuppressLint
- import android.content.BroadcastReceiver
- import android.content.Context
- import android.content.Intent
- import android.content.IntentFilter
- import android.os.Bundle
- import android.view.ViewGroup
- import android.webkit.JavascriptInterface
- import android.webkit.WebChromeClient
- import android.webkit.WebResourceRequest
- import android.webkit.WebSettings
- import android.webkit.WebView
- import android.webkit.WebViewClient
- import android.widget.EditText
- import android.widget.LinearLayout
- import androidx.appcompat.app.AppCompatActivity
- import androidx.core.content.ContextCompat
- import com.google.android.material.dialog.MaterialAlertDialogBuilder
- import com.musicweb.player.player.PlayerService
- class MainActivity : AppCompatActivity() {
- private lateinit var webView: WebView
- private val prefs by lazy { getSharedPreferences("musicweb_apk", MODE_PRIVATE) }
- private val playbackReceiver = object : BroadcastReceiver() {
- override fun onReceive(context: Context?, intent: Intent?) {
- if (intent?.action != PlayerService.ACTION_PLAYBACK_STATE || !this@MainActivity::webView.isInitialized) return
- val isPlaying = intent.getBooleanExtra(PlayerService.EXTRA_IS_PLAYING, false)
- val position = intent.getLongExtra(PlayerService.EXTRA_POSITION, 0L)
- val duration = intent.getLongExtra(PlayerService.EXTRA_DURATION, 0L)
- val index = intent.getIntExtra(PlayerService.EXTRA_CURRENT_INDEX, -1)
- val name = intent.getStringExtra(PlayerService.EXTRA_TRACK_NAME).orEmpty()
- val path = intent.getStringExtra(PlayerService.EXTRA_TRACK_PATH).orEmpty()
- val error = intent.getStringExtra(PlayerService.EXTRA_ERROR).orEmpty()
- runOnUiThread {
- val script = """
- window.__musicwebNativeUpdate && window.__musicwebNativeUpdate({
- isPlaying: $isPlaying,
- position: $position,
- duration: $duration,
- index: $index,
- name: ${name.quoteForJs()},
- path: ${path.quoteForJs()},
- error: ${error.quoteForJs()}
- });
- """.trimIndent()
- webView.evaluateJavascript(script, null)
- }
- }
- }
- @SuppressLint("SetJavaScriptEnabled")
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- webView = WebView(this).apply {
- layoutParams = ViewGroup.LayoutParams(
- ViewGroup.LayoutParams.MATCH_PARENT,
- ViewGroup.LayoutParams.MATCH_PARENT
- )
- settings.apply {
- javaScriptEnabled = true
- domStorageEnabled = true
- cacheMode = WebSettings.LOAD_DEFAULT
- mediaPlaybackRequiresUserGesture = false
- mixedContentMode = WebSettings.MIXED_CONTENT_ALWAYS_ALLOW
- allowFileAccess = true
- allowContentAccess = true
- }
- WebView.setWebContentsDebuggingEnabled(true)
- addJavascriptInterface(Bridge(), "AndroidPlayer")
- webViewClient = object : WebViewClient() {
- override fun shouldOverrideUrlLoading(view: WebView?, request: WebResourceRequest?) = false
- override fun onPageFinished(view: WebView?, url: String?) {
- super.onPageFinished(view, url)
- injectBridge()
- }
- }
- webChromeClient = WebChromeClient()
- }
- setContentView(webView)
- if (prefs.getString(KEY_BASE_URL, null).isNullOrBlank()) {
- promptForUrl()
- } else {
- webView.loadUrl(requireBaseUrl())
- }
- }
- override fun onBackPressed() {
- if (this::webView.isInitialized && webView.canGoBack()) webView.goBack() else super.onBackPressed()
- }
- override fun onStart() {
- super.onStart()
- ContextCompat.registerReceiver(
- this,
- playbackReceiver,
- IntentFilter(PlayerService.ACTION_PLAYBACK_STATE),
- ContextCompat.RECEIVER_NOT_EXPORTED
- )
- }
- override fun onStop() {
- unregisterReceiver(playbackReceiver)
- super.onStop()
- }
- private fun requireBaseUrl(): String {
- return prefs.getString(KEY_BASE_URL, DEFAULT_BASE_URL)?.trim().orEmpty()
- .ifBlank { DEFAULT_BASE_URL }
- .trimEnd('/')
- }
- private fun promptForUrl() {
- val input = EditText(this).apply {
- setText(DEFAULT_BASE_URL)
- setSelection(text.length)
- }
- val container = LinearLayout(this).apply {
- setPadding(48, 32, 48, 0)
- addView(input)
- }
- MaterialAlertDialogBuilder(this)
- .setTitle(R.string.set_server_title)
- .setMessage(R.string.set_server_message)
- .setView(container)
- .setCancelable(false)
- .setPositiveButton(android.R.string.ok) { _, _ ->
- val url = input.text?.toString()?.trim().orEmpty().ifBlank { DEFAULT_BASE_URL }
- prefs.edit().putString(KEY_BASE_URL, url).apply()
- webView.loadUrl(url.trimEnd('/'))
- }
- .show()
- }
- private fun injectBridge() {
- val script = """
- (function() {
- if (window.__musicwebNativeBridgeInstalled) return;
- window.__musicwebNativeBridgeInstalled = true;
- const absoluteUrl = (url) => {
- try {
- const parsed = new URL(url || "", window.location.href);
- parsed.pathname = parsed.pathname
- .split('/')
- .map(part => encodeURIComponent(decodeURIComponent(part)))
- .join('/');
- return parsed.toString();
- } catch (e) {
- return url || "";
- }
- };
- const toQueueJson = () => JSON.stringify((state.currentQueue || []).map(t => ({
- path: t.path || "",
- url: absoluteUrl(t.url || ""),
- name: t.name || "",
- folder: t.folder || ""
- })));
- const updateNativeUi = (index) => {
- if (!state.currentQueue.length) return;
- state.currentTrackIndex = index;
- const track = state.currentQueue[index];
- if (typeof updateDock === 'function') updateDock(track);
- if (typeof renderQueue === 'function') renderQueue();
- if (typeof savePlaybackState === 'function') savePlaybackState();
- };
- const sync = () => {
- if (window.AndroidPlayer && state.currentQueue && state.currentQueue.length) {
- AndroidPlayer.setQueue(toQueueJson(), Number(state.currentTrackIndex || 0));
- }
- };
- const formatNativeTime = (ms) => {
- const total = Math.max(0, Math.floor((Number(ms) || 0) / 1000));
- const minutes = Math.floor(total / 60);
- const seconds = total % 60;
- return String(minutes).padStart(2, '0') + ':' + String(seconds).padStart(2, '0');
- };
- window.__musicwebNativeUpdate = function(payload) {
- const playing = !!payload.isPlaying;
- const toggle = document.getElementById('playToggleBtn');
- const playIcon = document.querySelector('.icon-play');
- const pauseIcon = document.querySelector('.icon-pause');
- if (toggle) {
- toggle.classList.toggle('is-playing', playing);
- toggle.dataset.tip = playing ? '暂停' : '播放';
- }
- if (playIcon) playIcon.classList.toggle('is-hidden', playing);
- if (pauseIcon) pauseIcon.classList.toggle('is-hidden', !playing);
- if (payload.index >= 0 && state.currentQueue && state.currentQueue[payload.index]) {
- state.currentTrackIndex = payload.index;
- if (typeof renderQueue === 'function') renderQueue();
- }
- const duration = Number(payload.duration || 0);
- const position = Number(payload.position || 0);
- const progress = duration > 0 ? (position / duration) * 100 : 0;
- const bar = document.getElementById('progressBar');
- if (bar) bar.value = String(Math.max(0, Math.min(100, progress)));
- const currentLabel = document.getElementById('currentTimeLabel');
- const durationLabel = document.getElementById('durationLabel');
- if (currentLabel) currentLabel.textContent = formatNativeTime(position);
- if (durationLabel) durationLabel.textContent = formatNativeTime(duration);
- if (payload.name) {
- const title = document.getElementById('dockTitle');
- const dockPath = document.getElementById('dockPath');
- const nowTitle = document.getElementById('nowTitle');
- const nowMeta = document.getElementById('nowMeta');
- if (title) title.textContent = payload.name;
- if (dockPath) dockPath.textContent = payload.path || '';
- if (nowTitle) nowTitle.textContent = payload.name;
- if (nowMeta) nowMeta.textContent = payload.path || '';
- }
- };
- window.startQueue = function(tracks, index) {
- if (!tracks || !tracks.length) return;
- state.currentQueue = [...tracks];
- updateNativeUi(index);
- AndroidPlayer.playQueue(toQueueJson(), Number(state.currentTrackIndex || 0));
- };
- window.playTrack = function(index) {
- if (!state.currentQueue.length) return;
- updateNativeUi(index);
- AndroidPlayer.playQueue(toQueueJson(), Number(state.currentTrackIndex || 0));
- };
- window.playPrevious = function() { AndroidPlayer.previous(); };
- window.playNext = function() { AndroidPlayer.next(); };
- window.nextTrack = function() { AndroidPlayer.next(); };
- const playBtn = document.getElementById('playToggleBtn');
- if (playBtn && !playBtn.dataset.nativeBound) {
- playBtn.dataset.nativeBound = '1';
- playBtn.onclick = function() { AndroidPlayer.toggle(); };
- }
- const prevBtn = document.getElementById('prevBtn');
- if (prevBtn && !prevBtn.dataset.nativeBound) {
- prevBtn.dataset.nativeBound = '1';
- prevBtn.onclick = function() { AndroidPlayer.previous(); };
- }
- const nextBtn = document.getElementById('nextBtn');
- if (nextBtn && !nextBtn.dataset.nativeBound) {
- nextBtn.dataset.nativeBound = '1';
- nextBtn.onclick = function() { AndroidPlayer.next(); };
- }
- sync();
- })();
- """.trimIndent()
- webView.evaluateJavascript(script, null)
- }
- inner class Bridge {
- @JavascriptInterface
- fun setQueue(queueJson: String, index: Int) {
- val intent = Intent(this@MainActivity, PlayerService::class.java).apply {
- action = PlayerService.ACTION_SET_QUEUE
- putExtra(PlayerService.EXTRA_QUEUE_JSON, queueJson)
- putExtra(PlayerService.EXTRA_INDEX, index)
- }
- startService(intent)
- }
- @JavascriptInterface
- fun playQueue(queueJson: String, index: Int) {
- val intent = Intent(this@MainActivity, PlayerService::class.java).apply {
- action = PlayerService.ACTION_PLAY_QUEUE
- putExtra(PlayerService.EXTRA_QUEUE_JSON, queueJson)
- putExtra(PlayerService.EXTRA_INDEX, index)
- }
- ContextCompat.startForegroundService(this@MainActivity, intent)
- }
- @JavascriptInterface
- fun toggle() {
- startService(Intent(this@MainActivity, PlayerService::class.java).apply {
- action = PlayerService.ACTION_TOGGLE
- })
- }
- @JavascriptInterface
- fun next() {
- startService(Intent(this@MainActivity, PlayerService::class.java).apply {
- action = PlayerService.ACTION_NEXT
- })
- }
- @JavascriptInterface
- fun previous() {
- startService(Intent(this@MainActivity, PlayerService::class.java).apply {
- action = PlayerService.ACTION_PREVIOUS
- })
- }
- }
- companion object {
- private const val KEY_BASE_URL = "base_url"
- private const val DEFAULT_BASE_URL = "http://110.42.102.94:8006/"
- }
- }
- private fun String.quoteForJs(): String {
- return buildString {
- append('"')
- this@quoteForJs.forEach { char ->
- when (char) {
- '\\' -> append("\\\\")
- '"' -> append("\\\"")
- '\n' -> append("\\n")
- '\r' -> append("\\r")
- '\t' -> append("\\t")
- else -> append(char)
- }
- }
- append('"')
- }
- }
|