Import miniabs 1.0 source
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
commit
4fe9eed870
78 files changed
+5800
No files matched your search
@@ -0,0 +1,506 @@
|
||||
package com.miniabs;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import android.app.AlertDialog;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.view.ContextMenu;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.widget.AdapterView;
|
||||
import android.widget.ListView;
|
||||
import android.widget.ProgressBar;
|
||||
import android.widget.TextView;
|
||||
|
||||
public class HomeActivity extends BaseActivity {
|
||||
static final int TAB_LATEST = 0, TAB_SHOWS = 1, TAB_SAVED = 2;
|
||||
private static final int PAGE = 20;
|
||||
|
||||
// Kept across screen changes so going back is instant on a slow phone.
|
||||
private static List<String[]> podcastLibs;
|
||||
private static List<Rows.Row> latestCache, showsCache;
|
||||
private static int latestPage;
|
||||
private static boolean latestMore;
|
||||
|
||||
private ListView list;
|
||||
private TextView empty, library;
|
||||
private ProgressBar loading;
|
||||
private Rows adapter;
|
||||
private TextView[] tabs;
|
||||
private int tab;
|
||||
private boolean loadingLatest, loadingShows;
|
||||
private String latestError, showsError;
|
||||
private String savedSignature = "";
|
||||
private int ticks;
|
||||
|
||||
static void clearCache() {
|
||||
podcastLibs = null;
|
||||
latestCache = null;
|
||||
showsCache = null;
|
||||
latestPage = 0;
|
||||
latestMore = false;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle b) {
|
||||
super.onCreate(b);
|
||||
if (!Prefs.get(this).loggedIn()) {
|
||||
startActivity(new Intent(this, LoginActivity.class));
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
setContentView(R.layout.activity_home);
|
||||
list = (ListView) findViewById(R.id.list);
|
||||
empty = (TextView) findViewById(R.id.empty);
|
||||
loading = (ProgressBar) findViewById(R.id.loading);
|
||||
library = (TextView) findViewById(R.id.library);
|
||||
adapter = new Rows(this);
|
||||
list.setAdapter(adapter);
|
||||
registerForContextMenu(list);
|
||||
setupMiniBar();
|
||||
|
||||
tabs = new TextView[]{(TextView) findViewById(R.id.tab_latest), (TextView) findViewById(R.id.tab_shows),
|
||||
(TextView) findViewById(R.id.tab_saved)};
|
||||
for (int i = 0; i < tabs.length; i++) {
|
||||
final int t = i;
|
||||
tabs[i].setOnClickListener(new View.OnClickListener() {
|
||||
public void onClick(View v) {
|
||||
selectTab(t);
|
||||
}
|
||||
});
|
||||
}
|
||||
library.setOnClickListener(new View.OnClickListener() {
|
||||
public void onClick(View v) {
|
||||
chooseLibrary();
|
||||
}
|
||||
});
|
||||
findViewById(R.id.refresh).setOnClickListener(new View.OnClickListener() {
|
||||
public void onClick(View v) {
|
||||
refresh();
|
||||
}
|
||||
});
|
||||
list.setOnItemClickListener(new AdapterView.OnItemClickListener() {
|
||||
public void onItemClick(AdapterView<?> parent, View view, int pos, long id) {
|
||||
onRowClick(adapter.getItem(pos));
|
||||
}
|
||||
});
|
||||
|
||||
updateLibraryLabel();
|
||||
int start = getIntent().getIntExtra("tab", Prefs.get(this).sp.getInt("tab", TAB_LATEST));
|
||||
selectTab(start);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onNewIntent(Intent intent) {
|
||||
super.onNewIntent(intent);
|
||||
if (intent.hasExtra("tab")) selectTab(intent.getIntExtra("tab", TAB_LATEST));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
if (adapter == null) return;
|
||||
if (!Prefs.get(this).loggedIn()) { // logged out from Settings
|
||||
startActivity(new Intent(this, LoginActivity.class));
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
updateLibraryLabel();
|
||||
if (tab == TAB_SAVED) showSaved();
|
||||
else if ((tab == TAB_LATEST && latestCache == null && !loadingLatest)
|
||||
|| (tab == TAB_SHOWS && showsCache == null && !loadingShows)) selectTab(tab); // signed in again
|
||||
else adapter.notifyDataSetChanged();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onTick() {
|
||||
super.onTick();
|
||||
if (adapter == null) return;
|
||||
ticks++;
|
||||
if (tab == TAB_SAVED) {
|
||||
Episode cur = DownloadService.current;
|
||||
String sig = (cur == null ? "" : cur.id) + "/" + DownloadService.queued().size()
|
||||
+ "/" + DownloadService.lastError;
|
||||
if (!sig.equals(savedSignature)) showSaved();
|
||||
else if (cur != null) adapter.notifyDataSetChanged();
|
||||
} else if (ticks % 3 == 0) {
|
||||
adapter.notifyDataSetChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private void updateLibraryLabel() {
|
||||
String name = Prefs.get(this).libraryName();
|
||||
library.setText(name.length() > 0 ? name : "");
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ tabs
|
||||
|
||||
private void selectTab(int t) {
|
||||
if (t < 0 || t > 2) t = TAB_LATEST;
|
||||
tab = t;
|
||||
Prefs.get(this).sp.edit().putInt("tab", t).commit();
|
||||
for (int i = 0; i < tabs.length; i++) {
|
||||
tabs[i].setSelected(i == t);
|
||||
tabs[i].setTextColor(getResources().getColor(i == t ? R.color.text : R.color.sub));
|
||||
}
|
||||
adapter.showPodcastName = t != TAB_SHOWS;
|
||||
if (t == TAB_LATEST) {
|
||||
if (latestCache != null) show(latestCache, null);
|
||||
else if (!loadingLatest) loadLatest(false);
|
||||
else show(null, null);
|
||||
} else if (t == TAB_SHOWS) {
|
||||
if (showsCache != null) show(showsCache, null);
|
||||
else if (!loadingShows) loadShows();
|
||||
else show(null, null);
|
||||
} else {
|
||||
showSaved();
|
||||
}
|
||||
list.setSelection(0);
|
||||
}
|
||||
|
||||
/** rows == null means "still loading". */
|
||||
private void show(List<Rows.Row> rows, String emptyText) {
|
||||
boolean busy = rows == null && ((tab == TAB_LATEST && loadingLatest) || (tab == TAB_SHOWS && loadingShows));
|
||||
String err = tab == TAB_LATEST ? latestError : tab == TAB_SHOWS ? showsError : null;
|
||||
adapter.set(rows == null ? new ArrayList<Rows.Row>() : rows);
|
||||
loading.setVisibility(busy ? View.VISIBLE : View.GONE);
|
||||
String msg = null;
|
||||
if (!busy && (rows == null || rows.isEmpty())) msg = err != null ? err : emptyText;
|
||||
empty.setText(msg == null ? "" : msg);
|
||||
empty.setVisibility(msg == null ? View.GONE : View.VISIBLE);
|
||||
}
|
||||
|
||||
private void refresh() {
|
||||
App.invalidate();
|
||||
Covers.clearMemory();
|
||||
latestCache = null;
|
||||
showsCache = null;
|
||||
podcastLibs = null;
|
||||
latestError = null;
|
||||
showsError = null;
|
||||
if (tab == TAB_SHOWS) loadShows();
|
||||
else if (tab == TAB_LATEST) loadLatest(false);
|
||||
else {
|
||||
showSaved();
|
||||
new Job<Integer>() {
|
||||
Integer work() {
|
||||
return App.flushPending(HomeActivity.this);
|
||||
}
|
||||
|
||||
void done(Integer n) {
|
||||
if (n > 0) toast("Synced offline progress");
|
||||
}
|
||||
}.start();
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ data
|
||||
|
||||
/** Picks a podcast library (the saved one if it still exists). Runs in the background. */
|
||||
private String ensureLibrary() throws Exception {
|
||||
Prefs p = Prefs.get(this);
|
||||
if (podcastLibs == null) {
|
||||
JSONArray libs = Api.libraries(this);
|
||||
List<String[]> found = new ArrayList<String[]>();
|
||||
for (int i = 0; i < libs.length(); i++) {
|
||||
JSONObject l = libs.optJSONObject(i);
|
||||
if (l != null && "podcast".equals(Util.str(l, "mediaType"))) {
|
||||
found.add(new String[]{Util.str(l, "id"), Util.str(l, "name")});
|
||||
}
|
||||
}
|
||||
if (found.isEmpty()) throw new IOException("This server has no podcast libraries.");
|
||||
podcastLibs = found;
|
||||
}
|
||||
String id = p.libraryId();
|
||||
for (String[] l : podcastLibs) {
|
||||
if (l[0].equals(id)) {
|
||||
if (!l[1].equals(p.libraryName())) p.setLibrary(l[0], l[1]);
|
||||
return id;
|
||||
}
|
||||
}
|
||||
String[] first = podcastLibs.get(0);
|
||||
p.setLibrary(first[0], first[1]);
|
||||
return first[0];
|
||||
}
|
||||
|
||||
private void loadLatest(final boolean more) {
|
||||
loadingLatest = true;
|
||||
latestError = null;
|
||||
if (!more && tab == TAB_LATEST) show(null, null);
|
||||
final int page = more ? latestPage + 1 : 0;
|
||||
new Job<List<Rows.Row>>() {
|
||||
boolean hasMore;
|
||||
|
||||
List<Rows.Row> work() throws Exception {
|
||||
String lib = ensureLibrary();
|
||||
List<Rows.Row> rows = new ArrayList<Rows.Row>();
|
||||
if (!more) {
|
||||
App.flushPending(HomeActivity.this);
|
||||
App.loadProgress(HomeActivity.this);
|
||||
List<Rows.Row> cont = new ArrayList<Rows.Row>();
|
||||
try {
|
||||
JSONArray items = Api.itemsInProgress(HomeActivity.this).optJSONArray("libraryItems");
|
||||
for (int i = 0; items != null && i < items.length(); i++) {
|
||||
JSONObject it = items.optJSONObject(i);
|
||||
if (it == null || !lib.equals(Util.str(it, "libraryId"))) continue;
|
||||
JSONObject media = it.optJSONObject("media");
|
||||
JSONObject md = media == null ? null : media.optJSONObject("metadata");
|
||||
Episode e = Episode.fromApi(it.optJSONObject("recentEpisode"), Util.str(it, "id"),
|
||||
Util.str(md, "title"), Util.str(md, "author"));
|
||||
if (e == null) continue;
|
||||
double[] p = App.progress(e.id);
|
||||
if (p != null && p[2] > 0) continue;
|
||||
cont.add(Rows.Row.episode(e));
|
||||
if (cont.size() >= 5) break;
|
||||
}
|
||||
} catch (Api.AuthException ae) {
|
||||
throw ae;
|
||||
} catch (Exception ignored) {
|
||||
// older servers: just skip this section
|
||||
}
|
||||
if (!cont.isEmpty()) {
|
||||
rows.add(Rows.Row.header("CONTINUE LISTENING"));
|
||||
rows.addAll(cont);
|
||||
}
|
||||
rows.add(Rows.Row.header("LATEST EPISODES"));
|
||||
}
|
||||
JSONArray eps = Api.recentEpisodes(HomeActivity.this, lib, page, PAGE).optJSONArray("episodes");
|
||||
int n = eps == null ? 0 : eps.length();
|
||||
for (int i = 0; i < n; i++) {
|
||||
Episode e = Episode.fromApi(eps.optJSONObject(i), null, null, null);
|
||||
if (e != null) rows.add(Rows.Row.episode(e));
|
||||
}
|
||||
hasMore = n >= PAGE;
|
||||
return rows;
|
||||
}
|
||||
|
||||
void done(List<Rows.Row> rows) {
|
||||
loadingLatest = false;
|
||||
if (isFinishing()) return;
|
||||
updateLibraryLabel();
|
||||
if (more && latestCache != null) {
|
||||
int last = latestCache.size() - 1;
|
||||
if (last >= 0 && latestCache.get(last).type == Rows.MORE) latestCache.remove(last);
|
||||
latestCache.addAll(rows);
|
||||
} else {
|
||||
latestCache = rows;
|
||||
}
|
||||
latestPage = page;
|
||||
latestMore = hasMore;
|
||||
if (hasMore) latestCache.add(Rows.Row.more("Load more"));
|
||||
if (tab == TAB_LATEST) show(latestCache, "No episodes yet.");
|
||||
}
|
||||
|
||||
void failed(Exception e) {
|
||||
loadingLatest = false;
|
||||
if (isFinishing()) return;
|
||||
if (e instanceof Api.AuthException) {
|
||||
showError(e);
|
||||
return;
|
||||
}
|
||||
if (more) {
|
||||
toast(Util.describe(e));
|
||||
if (latestCache != null && !latestCache.isEmpty()) {
|
||||
latestCache.get(latestCache.size() - 1).text = "Load more";
|
||||
adapter.notifyDataSetChanged();
|
||||
}
|
||||
return;
|
||||
}
|
||||
latestError = Util.describe(e) + "\n\nTap refresh to try again.";
|
||||
if (tab == TAB_LATEST) offlineFallback(e);
|
||||
}
|
||||
}.start();
|
||||
}
|
||||
|
||||
private void loadShows() {
|
||||
loadingShows = true;
|
||||
showsError = null;
|
||||
if (tab == TAB_SHOWS) show(null, null);
|
||||
new Job<List<Rows.Row>>() {
|
||||
List<Rows.Row> work() throws Exception {
|
||||
String lib = ensureLibrary();
|
||||
List<Rows.Row> rows = new ArrayList<Rows.Row>();
|
||||
for (int page = 0; page < 20; page++) {
|
||||
JSONObject r = Api.libraryItems(HomeActivity.this, lib, page, 100);
|
||||
JSONArray res = r.optJSONArray("results");
|
||||
int n = res == null ? 0 : res.length();
|
||||
for (int i = 0; i < n; i++) {
|
||||
JSONObject it = res.optJSONObject(i);
|
||||
if (it == null) continue;
|
||||
JSONObject media = it.optJSONObject("media");
|
||||
JSONObject md = media == null ? null : media.optJSONObject("metadata");
|
||||
Rows.Show s = new Rows.Show();
|
||||
s.id = Util.str(it, "id");
|
||||
s.title = Util.str(md, "title");
|
||||
s.author = Util.str(md, "author");
|
||||
s.episodes = media == null ? 0 : media.optInt("numEpisodes", 0);
|
||||
if (s.episodes == 0 && media != null && media.optJSONArray("episodes") != null) {
|
||||
s.episodes = media.optJSONArray("episodes").length();
|
||||
}
|
||||
rows.add(Rows.Row.show(s));
|
||||
}
|
||||
int total = r.optInt("total", 0);
|
||||
if (n < 100 || rows.size() >= total) break;
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
void done(List<Rows.Row> rows) {
|
||||
loadingShows = false;
|
||||
if (isFinishing()) return;
|
||||
updateLibraryLabel();
|
||||
showsCache = rows;
|
||||
if (tab == TAB_SHOWS) show(rows, "No podcasts in this library.");
|
||||
}
|
||||
|
||||
void failed(Exception e) {
|
||||
loadingShows = false;
|
||||
if (isFinishing()) return;
|
||||
if (e instanceof Api.AuthException) {
|
||||
showError(e);
|
||||
return;
|
||||
}
|
||||
showsError = Util.describe(e) + "\n\nTap refresh to try again.";
|
||||
if (tab == TAB_SHOWS) offlineFallback(e);
|
||||
}
|
||||
}.start();
|
||||
}
|
||||
|
||||
/** When the server can't be reached but episodes are saved, show those instead. */
|
||||
private void offlineFallback(Exception e) {
|
||||
if (!(e instanceof Api.ApiException) && !Downloads.list(this).isEmpty()) {
|
||||
toast("Can't reach the server. Showing saved episodes.");
|
||||
selectTab(TAB_SAVED);
|
||||
} else {
|
||||
show(null, null);
|
||||
}
|
||||
}
|
||||
|
||||
private void showSaved() {
|
||||
List<Rows.Row> rows = new ArrayList<Rows.Row>();
|
||||
Episode cur = DownloadService.current;
|
||||
List<Episode> queued = DownloadService.queued();
|
||||
savedSignature = (cur == null ? "" : cur.id) + "/" + queued.size() + "/" + DownloadService.lastError;
|
||||
if (DownloadService.lastError != null) rows.add(Rows.Row.header(DownloadService.lastError));
|
||||
if (cur != null || !queued.isEmpty()) {
|
||||
rows.add(Rows.Row.header("DOWNLOADING"));
|
||||
if (cur != null) rows.add(Rows.Row.episode(cur));
|
||||
for (Episode q : queued) rows.add(Rows.Row.episode(q));
|
||||
}
|
||||
List<Episode> saved = Downloads.list(this);
|
||||
if (!saved.isEmpty()) {
|
||||
rows.add(Rows.Row.header("SAVED ON SD CARD (" + saved.size() + ")"));
|
||||
for (Episode e : saved) rows.add(Rows.Row.episode(e));
|
||||
}
|
||||
if (rows.isEmpty()) {
|
||||
show(rows, "Nothing saved yet.\n\nLong-press an episode and choose Download to listen offline.");
|
||||
} else {
|
||||
show(rows, null);
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ interaction
|
||||
|
||||
private void onRowClick(Rows.Row r) {
|
||||
if (r == null) return;
|
||||
switch (r.type) {
|
||||
case Rows.EPISODE:
|
||||
playEpisode(r.ep);
|
||||
break;
|
||||
case Rows.SHOW:
|
||||
startActivity(new Intent(this, PodcastActivity.class)
|
||||
.putExtra("itemId", r.show.id).putExtra("title", r.show.title)
|
||||
.putExtra("author", r.show.author));
|
||||
break;
|
||||
case Rows.MORE:
|
||||
if (!loadingLatest && latestMore) {
|
||||
r.text = "Loading…";
|
||||
adapter.notifyDataSetChanged();
|
||||
loadLatest(true);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private Episode menuEpisode;
|
||||
|
||||
@Override
|
||||
public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo info) {
|
||||
AdapterView.AdapterContextMenuInfo mi = (AdapterView.AdapterContextMenuInfo) info;
|
||||
Rows.Row r = adapter.getItem(mi.position);
|
||||
if (r.type != Rows.EPISODE) return;
|
||||
menuEpisode = r.ep;
|
||||
episodeMenu(menu, r.ep, true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onContextItemSelected(MenuItem item) {
|
||||
return onEpisodeContextItem(item, menuEpisode, new Runnable() {
|
||||
public void run() {
|
||||
if (tab == TAB_SAVED) showSaved();
|
||||
else adapter.notifyDataSetChanged();
|
||||
}
|
||||
}) || super.onContextItemSelected(item);
|
||||
}
|
||||
|
||||
private void chooseLibrary() {
|
||||
if (podcastLibs == null || podcastLibs.size() < 2) {
|
||||
toast(podcastLibs == null ? "Still loading libraries…" : "This is your only podcast library.");
|
||||
return;
|
||||
}
|
||||
String[] names = new String[podcastLibs.size()];
|
||||
for (int i = 0; i < names.length; i++) names[i] = podcastLibs.get(i)[1];
|
||||
new AlertDialog.Builder(this).setTitle("Podcast library")
|
||||
.setItems(names, new DialogInterface.OnClickListener() {
|
||||
public void onClick(DialogInterface d, int which) {
|
||||
String[] l = podcastLibs.get(which);
|
||||
Prefs.get(HomeActivity.this).setLibrary(l[0], l[1]);
|
||||
updateLibraryLabel();
|
||||
latestCache = null;
|
||||
showsCache = null;
|
||||
selectTab(tab == TAB_SAVED ? TAB_LATEST : tab);
|
||||
}
|
||||
}).show();
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ menu key
|
||||
|
||||
@Override
|
||||
public boolean onCreateOptionsMenu(Menu menu) {
|
||||
menu.add(0, 1, 0, "Refresh").setIcon(android.R.drawable.ic_menu_rotate);
|
||||
menu.add(0, 2, 0, "Library").setIcon(android.R.drawable.ic_menu_agenda);
|
||||
menu.add(0, 3, 0, "Now playing").setIcon(android.R.drawable.ic_media_play);
|
||||
menu.add(0, 4, 0, "Settings").setIcon(android.R.drawable.ic_menu_preferences);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case 1:
|
||||
refresh();
|
||||
return true;
|
||||
case 2:
|
||||
chooseLibrary();
|
||||
return true;
|
||||
case 3:
|
||||
startActivity(new Intent(this, PlayerActivity.class));
|
||||
return true;
|
||||
case 4:
|
||||
startActivity(new Intent(this, SettingsActivity.class));
|
||||
return true;
|
||||
default:
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user