Import minamp 1.0 source

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
xockoandClaude Opus 5.5 committed 2026-10-14 19:38:37 +02:00
commit 0df8124629
194 files changed
+7319

No files matched your search

+448
View File
@@ -0,0 +1,448 @@
package com.minamp;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import android.app.Activity;
import android.app.AlertDialog;
import android.content.DialogInterface;
import android.content.Intent;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.ProgressBar;
import android.widget.TextView;
import android.widget.Toast;
import com.minamp.plex.Item;
import com.minamp.plex.Library;
import com.minamp.plex.Server;
/** Shared bits: background tasks, mini player, item actions, settings menu. */
public abstract class BaseActivity extends Activity implements PlayerService.Listener, PlayQueue.Listener {
private static final ExecutorService POOL = Executors.newFixedThreadPool(2);
protected App app;
protected boolean destroyed;
protected boolean visible;
private View mini;
private ImageView miniArt;
private TextView miniTitle, miniSub;
private ImageButton miniPlay;
private ProgressBar miniProgress;
private Glyph miniPlayGlyph;
private String miniThumb;
// ------------------------------------------------------------------ tasks
public abstract static class Task<T> {
public abstract T work() throws Exception;
public void done(T result) {}
public void fail(Exception e) {}
}
protected <T> void run(final Task<T> t) {
POOL.execute(new Runnable() {
public void run() {
try {
final T r = t.work();
app.handler.post(new Runnable() { public void run() { if (!destroyed) t.done(r); } });
} catch (final Exception e) {
app.handler.post(new Runnable() { public void run() { if (!destroyed) t.fail(e); } });
} catch (final OutOfMemoryError e) {
app.images.clearMemory();
app.handler.post(new Runnable() { public void run() { if (!destroyed) t.fail(new Exception("Out of memory")); } });
}
}
});
}
protected void toast(String s) { Toast.makeText(this, s, Toast.LENGTH_SHORT).show(); }
protected static String msg(Exception e) { return PlayerService.message(e); }
// ------------------------------------------------------------------ lifecycle
@Override
protected void onCreate(android.os.Bundle b) {
super.onCreate(b);
app = App.get();
}
@Override
protected void onResume() {
super.onResume();
visible = true;
PlayerService.addListener(this);
PlayQueue.get().addListener(this);
bindMini();
app.handler.post(tick);
}
@Override
protected void onPause() {
super.onPause();
visible = false;
PlayerService.removeListener(this);
PlayQueue.get().removeListener(this);
app.handler.removeCallbacks(tick);
}
@Override
protected void onDestroy() {
destroyed = true;
super.onDestroy();
}
private final Runnable tick = new Runnable() {
public void run() {
if (!visible) return;
onTick();
app.handler.postDelayed(this, 500);
}
};
protected void onTick() { updateMiniProgress(); }
public void onPlayerChanged() { updateMini(); }
public void onQueueChanged() { updateMini(); }
// ------------------------------------------------------------------ top bar
protected void setupTopBar(String title, String sub, boolean back) {
TextView t = (TextView) findViewById(R.id.bar_title);
if (t != null) t.setText(title);
setBarSub(sub);
ImageButton b = (ImageButton) findViewById(R.id.btn_back);
if (b != null && back) {
b.setVisibility(View.VISIBLE);
Ui.icon(b, Glyph.BACK, getResources().getColor(R.color.text));
b.setOnClickListener(new View.OnClickListener() { public void onClick(View v) { finish(); } });
}
}
protected void setBarSub(String sub) {
TextView s = (TextView) findViewById(R.id.bar_sub);
if (s == null) return;
s.setVisibility(sub == null || sub.length() == 0 ? View.GONE : View.VISIBLE);
s.setText(sub);
}
protected ImageButton barAction(int id, int glyph, View.OnClickListener l) {
ImageButton b = (ImageButton) findViewById(id);
b.setVisibility(View.VISIBLE);
Ui.icon(b, glyph, getResources().getColor(R.color.text));
b.setOnClickListener(l);
return b;
}
// ------------------------------------------------------------------ status overlay
protected void showStatus(boolean spinning, String text, final Runnable retry) {
View box = findViewById(R.id.status_box);
if (box == null) return;
box.setVisibility(View.VISIBLE);
findViewById(R.id.status_spin).setVisibility(spinning ? View.VISIBLE : View.GONE);
((TextView) findViewById(R.id.status_text)).setText(text);
View r = findViewById(R.id.status_retry);
r.setVisibility(retry != null ? View.VISIBLE : View.GONE);
if (retry != null) r.setOnClickListener(new View.OnClickListener() { public void onClick(View v) { retry.run(); } });
}
protected void hideStatus() {
View box = findViewById(R.id.status_box);
if (box != null) box.setVisibility(View.GONE);
}
// ------------------------------------------------------------------ mini player
private void bindMini() {
if (mini == null) {
mini = findViewById(R.id.mini_root);
if (mini == null) return;
miniArt = (ImageView) findViewById(R.id.mini_art);
miniTitle = (TextView) findViewById(R.id.mini_title);
miniSub = (TextView) findViewById(R.id.mini_sub);
miniPlay = (ImageButton) findViewById(R.id.mini_play);
miniProgress = (ProgressBar) findViewById(R.id.mini_progress);
miniPlayGlyph = Ui.glyph(this, Glyph.PLAY, getResources().getColor(R.color.text), 18);
miniPlayGlyph.scale(1f);
miniPlay.setImageDrawable(miniPlayGlyph);
ImageButton next = (ImageButton) findViewById(R.id.mini_next);
Ui.icon(next, Glyph.NEXT, getResources().getColor(R.color.text), 16);
miniPlay.setOnClickListener(new View.OnClickListener() {
public void onClick(View v) { PlayerService.send(BaseActivity.this, PlayerService.ACTION_TOGGLE); }
});
next.setOnClickListener(new View.OnClickListener() {
public void onClick(View v) { PlayerService.send(BaseActivity.this, PlayerService.ACTION_NEXT); }
});
findViewById(R.id.mini_body).setOnClickListener(new View.OnClickListener() {
public void onClick(View v) { startActivity(new Intent(BaseActivity.this, NowPlayingActivity.class)); }
});
}
updateMini();
}
private void updateMini() {
if (mini == null) return;
Item it = PlayQueue.get().current();
if (it == null) { mini.setVisibility(View.GONE); return; }
mini.setVisibility(View.VISIBLE);
miniTitle.setText(it.title);
String st = PlayerService.status();
miniSub.setText(PlayerService.state() == PlayerService.BUFFERING && st.length() > 0 ? st : it.artist);
miniPlayGlyph.setType(PlayerService.isPlaying() ? Glyph.PAUSE : Glyph.PLAY);
if (!it.thumb.equals(miniThumb)) {
miniThumb = it.thumb;
app.images.load(miniArt, it.thumb, Ui.dp(this, 40), R.drawable.art_placeholder);
}
updateMiniProgress();
}
private void updateMiniProgress() {
if (mini == null || miniProgress == null) return;
long d = PlayerService.duration();
miniProgress.setProgress(d > 0 ? (int) (PlayerService.position() * 1000 / d) : 0);
}
// ------------------------------------------------------------------ item actions
protected Library lib() { return app.library; }
/** Tap on an item in a list. `context` = the list it's in (tracks play from there). */
protected void open(Item it, List<Item> contextTracks, int trackIndex, String sourceTitle) {
switch (it.type) {
case Item.ARTIST:
case Item.ALBUM:
case Item.PLAYLIST:
startActivity(new Intent(this, BrowseActivity.class).putExtra(BrowseActivity.EXTRA_ITEM, it.serialize()));
break;
case Item.TRACK:
List<Item> q = contextTracks;
int idx = trackIndex;
if (q == null || q.isEmpty()) { q = new ArrayList<Item>(); q.add(it); idx = 0; }
playList(q, idx, false, sourceTitle);
break;
}
}
protected void playList(List<Item> tracks, int start, boolean shuffle, String title) {
if (tracks.isEmpty()) { toast("Nothing to play"); return; }
PlayQueue.get().set(tracks, start, shuffle, title);
PlayerService.playCurrent(this, 0);
}
/** Fetch the tracks of an album/artist/playlist, then play/queue them. */
protected void playContainer(final Item it, final boolean shuffle, final int how) {
final Library lib = lib();
if (lib == null) return;
toast(how == 0 ? (shuffle ? "Shuffling " : "Playing ") + it.title : "Adding " + it.title + "…");
run(new Task<List<Item>>() {
public List<Item> work() throws Exception { return lib.tracksOf(it); }
public void done(List<Item> tracks) {
if (tracks.isEmpty()) { toast("No tracks"); return; }
boolean wasEmpty = PlayQueue.get().size() == 0;
if (how == 1) { PlayQueue.get().playNext(tracks); toast("Playing next"); }
else if (how == 2) { PlayQueue.get().append(tracks); toast("Added to queue"); }
else { playList(tracks, shuffle ? -1 : 0, shuffle, it.title); return; }
if (wasEmpty) PlayerService.playCurrent(BaseActivity.this, 0);
}
public void fail(Exception e) { toast(msg(e)); }
});
}
protected void runRadio(final String title, final Task<List<Item>> fetch) {
toast("Starting " + title + "…");
run(new Task<List<Item>>() {
public List<Item> work() throws Exception { return fetch.work(); }
public void done(List<Item> tracks) {
if (tracks.isEmpty()) { toast("Nothing found for " + title); return; }
playList(tracks, 0, false, title);
}
public void fail(Exception e) { toast(msg(e)); }
});
}
/** Long-press menu for any item. */
protected void itemMenu(final Item it) {
final List<String> labels = new ArrayList<String>();
final List<Integer> actions = new ArrayList<Integer>();
if (it.type == Item.TRACK) {
labels.add("Play next"); actions.add(1);
labels.add("Add to queue"); actions.add(2);
if (it.albumKey.length() > 0) { labels.add("Go to album"); actions.add(5); }
if (it.artistKey.length() > 0) { labels.add("Go to artist"); actions.add(6); }
labels.add(it.loved() ? "Unlove" : "Love"); actions.add(7);
} else if (it.isContainer()) {
labels.add("Play"); actions.add(0);
labels.add(it.type == Item.ARTIST ? "Artist radio (shuffle)" : "Shuffle"); actions.add(3);
labels.add("Play next"); actions.add(1);
labels.add("Add to queue"); actions.add(2);
if (it.type == Item.ALBUM && it.artistKey.length() > 0) { labels.add("Go to artist"); actions.add(6); }
} else return;
new AlertDialog.Builder(this)
.setTitle(it.title)
.setItems(labels.toArray(new String[labels.size()]), new DialogInterface.OnClickListener() {
public void onClick(DialogInterface d, int which) { itemAction(it, actions.get(which).intValue()); }
})
.show();
}
protected void itemAction(final Item it, int action) {
switch (action) {
case 0: playContainer(it, false, 0); break;
case 3: playContainer(it, true, 0); break;
case 1:
case 2:
if (it.type == Item.TRACK) {
List<Item> one = new ArrayList<Item>();
one.add(it);
boolean wasEmpty = PlayQueue.get().size() == 0;
if (action == 1) PlayQueue.get().playNext(one); else PlayQueue.get().append(one);
if (wasEmpty) PlayerService.playCurrent(this, 0);
toast(action == 1 ? "Playing next" : "Added to queue");
} else {
playContainer(it, false, action);
}
break;
case 5: openKey(it.albumKey, Item.ALBUM, it.album); break;
case 6: openKey(it.artistKey, Item.ARTIST, it.type == Item.ALBUM ? it.artist : it.artist); break;
case 7: toggleLove(it, null); break;
}
}
protected void openKey(String ratingKey, int type, String title) {
Item i = new Item();
i.type = type;
i.ratingKey = ratingKey;
i.title = title;
startActivity(new Intent(this, BrowseActivity.class).putExtra(BrowseActivity.EXTRA_ITEM, i.serialize()));
}
protected void toggleLove(final Item it, final Runnable after) {
final Library lib = lib();
if (lib == null) return;
final boolean love = !it.loved();
final double old = it.userRating;
it.userRating = love ? 10 : -1;
Item cur = PlayQueue.get().current();
if (cur != null && cur != it && cur.ratingKey.equals(it.ratingKey)) cur.userRating = it.userRating;
if (after != null) after.run();
run(new Task<Object>() {
public Object work() throws Exception { lib.rate(it, love ? 10 : -1); return null; }
public void done(Object o) { toast(love ? "Loved" : "Removed from loved"); }
public void fail(Exception e) {
it.userRating = old;
if (after != null) after.run();
toast("Couldn't save rating: " + msg(e));
}
});
}
// ------------------------------------------------------------------ menu
@Override
public boolean onCreateOptionsMenu(Menu menu) {
menu.add(0, 1, 0, "Now playing");
menu.add(0, 2, 0, "Search");
menu.add(0, 3, 0, "Settings");
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case 1: startActivity(new Intent(this, NowPlayingActivity.class)); return true;
case 2: startActivity(new Intent(this, SearchActivity.class)); return true;
case 3: showSettings(); return true;
}
return super.onOptionsItemSelected(item);
}
protected void showSettings() {
Server s = app.server;
String conn = s == null || s.active() == null ? "not connected yet" : Server.kindLabel(s.active().kind)
+ (s.active().uri.startsWith("https") ? " (secure)" : " (unencrypted)");
final String[] rows = {
"Quality at home: " + qualityLabel(app.qualityHome()),
"Quality away / mobile data: " + qualityLabel(app.qualityAway()),
"Download tracks before playing: " + (app.downloadFirst() ? "On" : "Off"),
"Library: " + app.sectionTitle(),
"Server: " + (s == null ? "-" : s.name),
"Connection: " + conn,
"Sign out",
};
new AlertDialog.Builder(this)
.setTitle("Settings")
.setItems(rows, new DialogInterface.OnClickListener() {
public void onClick(DialogInterface d, int which) {
switch (which) {
case 0: pickQuality("qHome", app.qualityHome(), "Quality at home"); break;
case 1: pickQuality("qAway", app.qualityAway(), "Quality away / mobile"); break;
case 2:
app.prefs.edit().putBoolean("downloadFirst", !app.downloadFirst()).commit();
toast("Download before playing: " + (app.downloadFirst() ? "on" : "off"));
break;
case 3: startActivity(new Intent(BaseActivity.this, SetupActivity.class).putExtra(SetupActivity.EXTRA_MODE, SetupActivity.MODE_LIBRARY)); break;
case 4: startActivity(new Intent(BaseActivity.this, SetupActivity.class).putExtra(SetupActivity.EXTRA_MODE, SetupActivity.MODE_SERVER)); break;
case 5:
if (app.server != null) {
app.server.invalidate();
toast("Reconnecting…");
final Server sv = app.server;
run(new Task<Server.Conn>() {
public Server.Conn work() throws Exception { return sv.pick(); }
public void done(Server.Conn c) { toast("Connected: " + Server.kindLabel(c.kind)); onConnectionChanged(); }
public void fail(Exception e) { toast(msg(e)); }
});
}
break;
case 6: confirmSignOut(); break;
}
}
}).show();
}
protected void onConnectionChanged() {}
private static String qualityLabel(int kbps) {
for (int i = 0; i < App.QUALITIES.length; i++) if (App.QUALITIES[i] == kbps) return App.QUALITY_LABELS[i];
return kbps + " kbps";
}
private void pickQuality(final String key, int current, String title) {
int sel = 0;
for (int i = 0; i < App.QUALITIES.length; i++) if (App.QUALITIES[i] == current) sel = i;
new AlertDialog.Builder(this)
.setTitle(title)
.setSingleChoiceItems(App.QUALITY_LABELS, sel, new DialogInterface.OnClickListener() {
public void onClick(DialogInterface d, int which) {
app.prefs.edit().putInt(key, App.QUALITIES[which]).commit();
d.dismiss();
toast("Applies from the next track");
}
}).show();
}
private void confirmSignOut() {
new AlertDialog.Builder(this)
.setMessage("Sign out and forget this server?")
.setPositiveButton("Sign out", new DialogInterface.OnClickListener() {
public void onClick(DialogInterface d, int w) {
PlayerService.send(BaseActivity.this, PlayerService.ACTION_STOP);
PlayQueue.get().clear();
app.signOut();
startActivity(new Intent(BaseActivity.this, SetupActivity.class).addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP));
finish();
}
})
.setNegativeButton("Cancel", null)
.show();
}
}