Private
Public Access
Minamp 1.5: Galician interface
- Interface in Galician by default regardless of the phone's language, with English selectable under Axustes -> Idioma; switching rebuilds the screens - All text in tools/strings.py (key -> English, Galician), generating res/values/strings.xml (gl) and res/values-en/strings.xml; layouts now use string resources - util/Tr.java: translations for the network and Plex layers, which can't read Android resources (falls back to English in desktop tests) - net/RefusedException: distinguish "server answered but refused" from connection failures - The connection log stays in English for troubleshooting Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
6fd17bc9c5
commit
fa67e95fde
29 files changed
+1029
-206
No files matched your search
@@ -49,6 +49,13 @@ public class App extends Application {
|
||||
handler = new Handler();
|
||||
prefs = getSharedPreferences("minamp", MODE_PRIVATE);
|
||||
installCrashLogger();
|
||||
applyLanguage(this);
|
||||
com.minamp.util.Tr.source = new com.minamp.util.Tr.Source() {
|
||||
public String get(String key) {
|
||||
int id = getResources().getIdentifier("tr_" + key, "string", getPackageName());
|
||||
return id == 0 ? null : getString(id);
|
||||
}
|
||||
};
|
||||
|
||||
// Android 2.2's HttpURLConnection pool is buggy; we use our own client, but be safe.
|
||||
System.setProperty("http.keepAlive", "false");
|
||||
@@ -91,6 +98,38 @@ public class App extends Application {
|
||||
}, new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION));
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ language
|
||||
|
||||
public static final String[] LANGUAGES = {"gl", "en"};
|
||||
public static final String[] LANGUAGE_NAMES = {"Galego", "English"};
|
||||
|
||||
/** Galician unless the user picked English, whatever language the phone itself uses. */
|
||||
public String language() { return prefs.getString("lang", "gl"); }
|
||||
|
||||
public static String languageName(String code) {
|
||||
for (int i = 0; i < LANGUAGES.length; i++) if (LANGUAGES[i].equals(code)) return LANGUAGE_NAMES[i];
|
||||
return code;
|
||||
}
|
||||
|
||||
/** Forces the app's language on a context's resources (call before inflating any layout). */
|
||||
public static void applyLanguage(android.content.Context c) {
|
||||
App app = instance;
|
||||
if (app == null) return;
|
||||
java.util.Locale loc = new java.util.Locale(app.language());
|
||||
java.util.Locale.setDefault(loc);
|
||||
android.content.res.Resources r = c.getResources();
|
||||
android.content.res.Configuration cfg = r.getConfiguration();
|
||||
if (loc.equals(cfg.locale)) return;
|
||||
cfg.locale = loc;
|
||||
r.updateConfiguration(cfg, r.getDisplayMetrics());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onConfigurationChanged(android.content.res.Configuration newConfig) {
|
||||
super.onConfigurationChanged(newConfig);
|
||||
applyLanguage(this); // the system resets the locale on config changes
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ crash reports
|
||||
|
||||
private static final String CRASH_FILE = "crash.txt";
|
||||
|
||||
@@ -64,6 +64,8 @@ public abstract class BaseActivity extends Activity implements PlayerService.Lis
|
||||
});
|
||||
}
|
||||
|
||||
protected String str(int id, Object... args) { return args.length == 0 ? getString(id) : getString(id, args); }
|
||||
|
||||
protected void toast(String s) { Toast.makeText(this, s, s != null && s.length() > 40 ? Toast.LENGTH_LONG : Toast.LENGTH_SHORT).show(); }
|
||||
|
||||
protected static String msg(Exception e) { return PlayerService.message(e); }
|
||||
@@ -72,8 +74,10 @@ public abstract class BaseActivity extends Activity implements PlayerService.Lis
|
||||
|
||||
@Override
|
||||
protected void onCreate(android.os.Bundle b) {
|
||||
super.onCreate(b);
|
||||
app = App.get();
|
||||
App.applyLanguage(this);
|
||||
App.applyLanguage(getApplicationContext());
|
||||
super.onCreate(b);
|
||||
}
|
||||
|
||||
/** If the last run hit an error, show it so it can be reported (also saved as Minamp-error.txt on the SD card). */
|
||||
@@ -89,9 +93,9 @@ public abstract class BaseActivity extends Activity implements PlayerService.Lis
|
||||
tv.setPadding(p, p, p, p);
|
||||
sv.addView(tv);
|
||||
new AlertDialog.Builder(this)
|
||||
.setTitle("Minamp hit an error last time")
|
||||
.setTitle(R.string.error_last_time)
|
||||
.setView(sv)
|
||||
.setPositiveButton("OK", null)
|
||||
.setPositiveButton(R.string.ok, null)
|
||||
.show();
|
||||
}
|
||||
|
||||
@@ -254,7 +258,7 @@ public abstract class BaseActivity extends Activity implements PlayerService.Lis
|
||||
}
|
||||
|
||||
protected void playList(List<Item> tracks, int start, boolean shuffle, String title) {
|
||||
if (tracks.isEmpty()) { toast("Nothing to play"); return; }
|
||||
if (tracks.isEmpty()) { toast(str(R.string.nothing_to_play)); return; }
|
||||
PlayQueue.get().set(tracks, start, shuffle, title);
|
||||
PlayerService.playCurrent(this, 0);
|
||||
}
|
||||
@@ -263,14 +267,14 @@ public abstract class BaseActivity extends Activity implements PlayerService.Lis
|
||||
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 + "…");
|
||||
toast(how == 0 ? str(shuffle ? R.string.shuffling_x : R.string.playing_x, it.title) : str(R.string.adding_x, 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; }
|
||||
if (tracks.isEmpty()) { toast(str(R.string.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"); }
|
||||
if (how == 1) { PlayQueue.get().playNext(tracks); toast(str(R.string.playing_next)); }
|
||||
else if (how == 2) { PlayQueue.get().append(tracks); toast(str(R.string.added_to_queue)); }
|
||||
else { playList(tracks, shuffle ? -1 : 0, shuffle, it.title); return; }
|
||||
if (wasEmpty) PlayerService.playCurrent(BaseActivity.this, 0);
|
||||
}
|
||||
@@ -279,11 +283,11 @@ public abstract class BaseActivity extends Activity implements PlayerService.Lis
|
||||
}
|
||||
|
||||
protected void runRadio(final String title, final Task<List<Item>> fetch) {
|
||||
toast("Starting " + title + "…");
|
||||
toast(str(R.string.starting_x, 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; }
|
||||
if (tracks.isEmpty()) { toast(str(R.string.nothing_found_for, title)); return; }
|
||||
playList(tracks, 0, false, title);
|
||||
}
|
||||
public void fail(Exception e) { toast(msg(e)); }
|
||||
@@ -295,17 +299,17 @@ public abstract class BaseActivity extends Activity implements PlayerService.Lis
|
||||
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);
|
||||
labels.add(str(R.string.play_next)); actions.add(1);
|
||||
labels.add(str(R.string.add_to_queue)); actions.add(2);
|
||||
if (it.albumKey.length() > 0) { labels.add(str(R.string.go_to_album)); actions.add(5); }
|
||||
if (it.artistKey.length() > 0) { labels.add(str(R.string.go_to_artist)); actions.add(6); }
|
||||
labels.add(str(it.loved() ? R.string.unlove : R.string.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); }
|
||||
labels.add(str(R.string.play)); actions.add(0);
|
||||
labels.add(str(it.type == Item.ARTIST ? R.string.artist_radio_shuffle : R.string.shuffle)); actions.add(3);
|
||||
labels.add(str(R.string.play_next)); actions.add(1);
|
||||
labels.add(str(R.string.add_to_queue)); actions.add(2);
|
||||
if (it.type == Item.ALBUM && it.artistKey.length() > 0) { labels.add(str(R.string.go_to_artist)); actions.add(6); }
|
||||
} else return;
|
||||
new AlertDialog.Builder(this)
|
||||
.setTitle(it.title)
|
||||
@@ -327,7 +331,7 @@ public abstract class BaseActivity extends Activity implements PlayerService.Lis
|
||||
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");
|
||||
toast(str(action == 1 ? R.string.playing_next : R.string.added_to_queue));
|
||||
} else {
|
||||
playContainer(it, false, action);
|
||||
}
|
||||
@@ -357,11 +361,11 @@ public abstract class BaseActivity extends Activity implements PlayerService.Lis
|
||||
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 done(Object o) { toast(str(love ? R.string.loved : R.string.unloved)); }
|
||||
public void fail(Exception e) {
|
||||
it.userRating = old;
|
||||
if (after != null) after.run();
|
||||
toast("Couldn't save rating: " + msg(e));
|
||||
toast(str(R.string.cant_save_rating, msg(e)));
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -370,9 +374,9 @@ public abstract class BaseActivity extends Activity implements PlayerService.Lis
|
||||
|
||||
@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");
|
||||
menu.add(0, 1, 0, R.string.now_playing);
|
||||
menu.add(0, 2, 0, R.string.search);
|
||||
menu.add(0, 3, 0, R.string.settings);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -388,53 +392,54 @@ public abstract class BaseActivity extends Activity implements PlayerService.Lis
|
||||
|
||||
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)");
|
||||
String conn = s == null || s.active() == null ? str(R.string.not_connected_yet) : Server.kindLabel(s.active().kind)
|
||||
+ str(s.active().uri.startsWith("https") ? R.string.secure : R.string.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"),
|
||||
"Encrypt at home too (slower): " + (Server.preferSecureAtHome ? "On" : "Off"),
|
||||
"Library: " + app.sectionTitle(),
|
||||
"Server: " + (s == null ? "-" : s.name),
|
||||
"Connection: " + conn,
|
||||
"Connection log & streaming test",
|
||||
"Sign out",
|
||||
str(R.string.set_quality_home, qualityLabel(app.qualityHome())),
|
||||
str(R.string.set_quality_away, qualityLabel(app.qualityAway())),
|
||||
str(R.string.set_download_first, str(app.downloadFirst() ? R.string.on : R.string.off)),
|
||||
str(R.string.set_secure_home, str(Server.preferSecureAtHome ? R.string.on : R.string.off)),
|
||||
str(R.string.set_library, app.sectionTitle()),
|
||||
str(R.string.set_server, s == null ? "-" : s.name),
|
||||
str(R.string.set_connection, conn),
|
||||
str(R.string.set_log),
|
||||
str(R.string.set_language, App.languageName(app.language())),
|
||||
str(R.string.sign_out),
|
||||
};
|
||||
new AlertDialog.Builder(this)
|
||||
.setTitle("Settings")
|
||||
.setTitle(R.string.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 0: pickQuality("qHome", app.qualityHome(), str(R.string.quality_home_title)); break;
|
||||
case 1: pickQuality("qAway", app.qualityAway(), str(R.string.quality_away_title)); break;
|
||||
case 2:
|
||||
app.prefs.edit().putBoolean("downloadFirst", !app.downloadFirst()).commit();
|
||||
toast("Download before playing: " + (app.downloadFirst() ? "on" : "off"));
|
||||
toast(str(R.string.download_first_toast, str(app.downloadFirst() ? R.string.on : R.string.off)));
|
||||
break;
|
||||
case 3:
|
||||
Server.preferSecureAtHome = !Server.preferSecureAtHome;
|
||||
app.prefs.edit().putBoolean("secureHome", Server.preferSecureAtHome).commit();
|
||||
if (app.server != null) app.server.invalidate();
|
||||
toast(Server.preferSecureAtHome ? "Using HTTPS at home (each new connection is slower on this phone)"
|
||||
: "Using plain HTTP at home (faster)");
|
||||
toast(str(Server.preferSecureAtHome ? R.string.https_home_on : R.string.https_home_off));
|
||||
break;
|
||||
case 4: startActivity(new Intent(BaseActivity.this, SetupActivity.class).putExtra(SetupActivity.EXTRA_MODE, SetupActivity.MODE_LIBRARY)); break;
|
||||
case 5: startActivity(new Intent(BaseActivity.this, SetupActivity.class).putExtra(SetupActivity.EXTRA_MODE, SetupActivity.MODE_SERVER)); break;
|
||||
case 6:
|
||||
if (app.server != null) {
|
||||
app.server.invalidate();
|
||||
toast("Reconnecting…");
|
||||
toast(str(R.string.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 done(Server.Conn c) { toast(str(R.string.connected_x, Server.kindLabel(c.kind))); onConnectionChanged(); }
|
||||
public void fail(Exception e) { toast(msg(e)); }
|
||||
});
|
||||
}
|
||||
break;
|
||||
case 7: showDiagnostics(); break;
|
||||
case 8: confirmSignOut(); break;
|
||||
case 8: pickLanguage(); break;
|
||||
case 9: confirmSignOut(); break;
|
||||
}
|
||||
}
|
||||
}).show();
|
||||
@@ -454,17 +459,17 @@ public abstract class BaseActivity extends Activity implements PlayerService.Lis
|
||||
int p = Ui.dp(this, 6);
|
||||
tv.setPadding(p, p, p, p);
|
||||
String log = com.minamp.net.DiagLog.dump();
|
||||
tv.setText(log.length() > 0 ? log : "Nothing logged yet.");
|
||||
tv.setText(log.length() > 0 ? log : str(R.string.log_empty));
|
||||
sv.addView(tv);
|
||||
AlertDialog.Builder b = new AlertDialog.Builder(this)
|
||||
.setTitle("Connection log")
|
||||
.setTitle(R.string.connection_log)
|
||||
.setView(sv)
|
||||
.setNegativeButton("Save to SD card", new DialogInterface.OnClickListener() {
|
||||
.setNegativeButton(R.string.save_to_sd, new DialogInterface.OnClickListener() {
|
||||
public void onClick(DialogInterface d, int w) { saveDiagnostics(); }
|
||||
})
|
||||
.setPositiveButton("Close", null);
|
||||
.setPositiveButton(R.string.close, null);
|
||||
if (app.library != null) {
|
||||
b.setNeutralButton("Test streaming", new DialogInterface.OnClickListener() {
|
||||
b.setNeutralButton(R.string.test_streaming, new DialogInterface.OnClickListener() {
|
||||
public void onClick(DialogInterface d, int w) { testStreaming(); }
|
||||
});
|
||||
}
|
||||
@@ -476,7 +481,7 @@ public abstract class BaseActivity extends Activity implements PlayerService.Lis
|
||||
private void saveDiagnostics() {
|
||||
try {
|
||||
if (!android.os.Environment.MEDIA_MOUNTED.equals(android.os.Environment.getExternalStorageState())) {
|
||||
toast("No SD card available");
|
||||
toast(str(R.string.no_sd));
|
||||
return;
|
||||
}
|
||||
java.io.File f = new java.io.File(android.os.Environment.getExternalStorageDirectory(), "Minamp-log.txt");
|
||||
@@ -485,9 +490,9 @@ public abstract class BaseActivity extends Activity implements PlayerService.Lis
|
||||
+ ", Android " + android.os.Build.VERSION.RELEASE + "\n";
|
||||
o.write((head + com.minamp.net.DiagLog.dump()).getBytes("UTF-8"));
|
||||
o.close();
|
||||
toast("Saved as Minamp-log.txt on the SD card");
|
||||
toast(str(R.string.saved_log));
|
||||
} catch (Exception e) {
|
||||
toast("Couldn't save: " + msg(e));
|
||||
toast(str(R.string.cant_save, msg(e)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -495,7 +500,7 @@ public abstract class BaseActivity extends Activity implements PlayerService.Lis
|
||||
private void testStreaming() {
|
||||
final Library lib = lib();
|
||||
final Item current = PlayQueue.get().current();
|
||||
toast("Testing\u2026 (about 30 s)");
|
||||
toast(str(R.string.testing));
|
||||
run(new Task<Object>() {
|
||||
public Object work() throws Exception {
|
||||
com.minamp.net.DiagLog.log("=== streaming test ===");
|
||||
@@ -559,29 +564,56 @@ public abstract class BaseActivity extends Activity implements PlayerService.Lis
|
||||
});
|
||||
}
|
||||
|
||||
private static String qualityLabel(int kbps) {
|
||||
private String qualityLabel(int kbps) {
|
||||
if (kbps <= 0) return str(R.string.quality_original);
|
||||
for (int i = 0; i < App.QUALITIES.length; i++) if (App.QUALITIES[i] == kbps) return App.QUALITY_LABELS[i];
|
||||
return kbps + " kbps";
|
||||
}
|
||||
|
||||
private String[] qualityLabels() {
|
||||
String[] l = App.QUALITY_LABELS.clone();
|
||||
l[0] = str(R.string.quality_original);
|
||||
return l;
|
||||
}
|
||||
|
||||
private void pickLanguage() {
|
||||
int sel = 0;
|
||||
for (int i = 0; i < App.LANGUAGES.length; i++) if (App.LANGUAGES[i].equals(app.language())) sel = i;
|
||||
new AlertDialog.Builder(this)
|
||||
.setTitle(R.string.language_title)
|
||||
.setSingleChoiceItems(App.LANGUAGE_NAMES, sel, new DialogInterface.OnClickListener() {
|
||||
public void onClick(DialogInterface d, int which) {
|
||||
d.dismiss();
|
||||
if (App.LANGUAGES[which].equals(app.language())) return;
|
||||
app.prefs.edit().putString("lang", App.LANGUAGES[which]).commit();
|
||||
App.applyLanguage(getApplicationContext());
|
||||
App.applyLanguage(BaseActivity.this);
|
||||
// rebuild the screens in the new language
|
||||
startActivity(new Intent(BaseActivity.this, HomeActivity.class)
|
||||
.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP).putExtra("relaunch", true));
|
||||
finish();
|
||||
}
|
||||
}).show();
|
||||
}
|
||||
|
||||
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() {
|
||||
.setSingleChoiceItems(qualityLabels(), 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");
|
||||
toast(str(R.string.applies_next_track));
|
||||
}
|
||||
}).show();
|
||||
}
|
||||
|
||||
private void confirmSignOut() {
|
||||
new AlertDialog.Builder(this)
|
||||
.setMessage("Sign out and forget this server?")
|
||||
.setPositiveButton("Sign out", new DialogInterface.OnClickListener() {
|
||||
.setMessage(R.string.sign_out_confirm)
|
||||
.setPositiveButton(R.string.sign_out, new DialogInterface.OnClickListener() {
|
||||
public void onClick(DialogInterface d, int w) {
|
||||
PlayerService.send(BaseActivity.this, PlayerService.ACTION_STOP);
|
||||
PlayQueue.get().clear();
|
||||
@@ -590,7 +622,7 @@ public abstract class BaseActivity extends Activity implements PlayerService.Lis
|
||||
finish();
|
||||
}
|
||||
})
|
||||
.setNegativeButton("Cancel", null)
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
.show();
|
||||
}
|
||||
}
|
||||
@@ -63,7 +63,7 @@ public class BrowseActivity extends BaseActivity {
|
||||
|
||||
Button play = (Button) header.findViewById(R.id.detail_play);
|
||||
Button shuffle = (Button) header.findViewById(R.id.detail_shuffle);
|
||||
if (item.type == Item.ARTIST) { play.setText("Play all"); shuffle.setText("Artist radio"); }
|
||||
if (item.type == Item.ARTIST) { play.setText(R.string.play_all); shuffle.setText(R.string.artist_radio); }
|
||||
play.setOnClickListener(new View.OnClickListener() { public void onClick(View v) { playAll(false); } });
|
||||
shuffle.setOnClickListener(new View.OnClickListener() { public void onClick(View v) { playAll(true); } });
|
||||
|
||||
@@ -73,9 +73,9 @@ public class BrowseActivity extends BaseActivity {
|
||||
|
||||
private String kindLabel() {
|
||||
switch (item.type) {
|
||||
case Item.ARTIST: return "Artist";
|
||||
case Item.ALBUM: return "Album";
|
||||
case Item.PLAYLIST: return "Playlist";
|
||||
case Item.ARTIST: return str(R.string.artist);
|
||||
case Item.ALBUM: return str(R.string.album);
|
||||
case Item.PLAYLIST: return str(R.string.playlist);
|
||||
default: return "";
|
||||
}
|
||||
}
|
||||
@@ -95,13 +95,13 @@ public class BrowseActivity extends BaseActivity {
|
||||
private void setMeta(List<Item> children) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
if (item.type == Item.ARTIST) {
|
||||
sb.append(children.size()).append(children.size() == 1 ? " album" : " albums");
|
||||
sb.append(str(children.size() == 1 ? R.string.n_albums_one : R.string.n_albums, children.size()));
|
||||
} else {
|
||||
long total = 0;
|
||||
int n = 0;
|
||||
for (int i = 0; i < children.size(); i++) if (children.get(i).type == Item.TRACK) { n++; total += children.get(i).duration; }
|
||||
if (item.year > 0) sb.append(item.year).append(" · ");
|
||||
sb.append(n).append(n == 1 ? " track" : " tracks");
|
||||
sb.append(str(n == 1 ? R.string.n_tracks_one : R.string.n_tracks, n));
|
||||
if (total > 0) sb.append(" · ").append(Ui.longDuration(total));
|
||||
}
|
||||
((TextView) header.findViewById(R.id.detail_meta)).setText(sb.toString());
|
||||
@@ -110,7 +110,7 @@ public class BrowseActivity extends BaseActivity {
|
||||
private void load() {
|
||||
final Library lib = lib();
|
||||
final Item target = item;
|
||||
showStatus(true, "Loading…", null);
|
||||
showStatus(true, str(R.string.loading), null);
|
||||
run(new Task<Object[]>() {
|
||||
public Object[] work() throws Exception {
|
||||
Item full = null;
|
||||
@@ -137,7 +137,7 @@ public class BrowseActivity extends BaseActivity {
|
||||
adapter.setAlbumArtist(item.artist);
|
||||
adapter.set(children, -1);
|
||||
setMeta(children);
|
||||
if (children.isEmpty()) showStatus(false, "Nothing here", null);
|
||||
if (children.isEmpty()) showStatus(false, str(R.string.nothing_here), null);
|
||||
}
|
||||
public void fail(Exception e) {
|
||||
showStatus(false, msg(e), new Runnable() { public void run() { load(); } });
|
||||
|
||||
@@ -29,7 +29,7 @@ public class HomeActivity extends BaseActivity {
|
||||
|
||||
private static final int TAB_HOME = 0, TAB_ARTISTS = 1, TAB_ALBUMS = 2, TAB_PLAYLISTS = 3;
|
||||
private static final String[] ALBUM_SORTS = {"titleSort", "artist.titleSort", "addedAt:desc", "year:desc", "viewCount:desc"};
|
||||
private static final String[] ALBUM_SORT_LABELS = {"Title", "Artist", "Recently added", "Year (newest)", "Most played"};
|
||||
private static final int[] ALBUM_SORT_LABELS = {R.string.sort_title, R.string.sort_artist, R.string.sort_added, R.string.sort_year, R.string.sort_played};
|
||||
|
||||
private TextView[] tabs;
|
||||
private View tabLine;
|
||||
@@ -90,6 +90,14 @@ public class HomeActivity extends BaseActivity {
|
||||
@Override
|
||||
protected void onNewIntent(Intent intent) {
|
||||
super.onNewIntent(intent);
|
||||
if (intent.getBooleanExtra("relaunch", false)) {
|
||||
// language changed: rebuild from scratch so every inflated text is re-read
|
||||
finish();
|
||||
overridePendingTransition(0, 0);
|
||||
startActivity(new Intent(this, HomeActivity.class));
|
||||
overridePendingTransition(0, 0);
|
||||
return;
|
||||
}
|
||||
// library or server may have changed in setup
|
||||
if (!app.isConfigured()) { startActivity(new Intent(this, SetupActivity.class)); finish(); return; }
|
||||
setupTopBar(app.sectionTitle(), connectionLine(), false);
|
||||
@@ -154,33 +162,33 @@ public class HomeActivity extends BaseActivity {
|
||||
int pad = Ui.dp(this, 8);
|
||||
|
||||
LinearLayout row1 = tileRow();
|
||||
row1.addView(tile(inf, Glyph.RADIO, "Library radio", "Shuffle everything", new View.OnClickListener() {
|
||||
row1.addView(tile(inf, Glyph.RADIO, str(R.string.library_radio), str(R.string.library_radio_sub), new View.OnClickListener() {
|
||||
public void onClick(View v) {
|
||||
final Library lib = lib();
|
||||
runRadio("Library radio", new Task<List<Item>>() { public List<Item> work() throws Exception { return lib.libraryRadio(); } });
|
||||
runRadio(str(R.string.library_radio), new Task<List<Item>>() { public List<Item> work() throws Exception { return lib.libraryRadio(); } });
|
||||
}
|
||||
}));
|
||||
row1.addView(spacer());
|
||||
row1.addView(tile(inf, Glyph.HEART, "Loved radio", "Your top rated", new View.OnClickListener() {
|
||||
row1.addView(tile(inf, Glyph.HEART, str(R.string.loved_radio), str(R.string.loved_radio_sub), new View.OnClickListener() {
|
||||
public void onClick(View v) {
|
||||
final Library lib = lib();
|
||||
runRadio("Loved radio", new Task<List<Item>>() { public List<Item> work() throws Exception { return lib.topRatedRadio(); } });
|
||||
runRadio(str(R.string.loved_radio), new Task<List<Item>>() { public List<Item> work() throws Exception { return lib.topRatedRadio(); } });
|
||||
}
|
||||
}));
|
||||
homeContent.addView(row1);
|
||||
|
||||
LinearLayout row2 = tileRow();
|
||||
row2.addView(tile(inf, Glyph.CLOCK, "Time travel", "Pick a decade", new View.OnClickListener() {
|
||||
row2.addView(tile(inf, Glyph.CLOCK, str(R.string.time_travel), str(R.string.time_travel_sub), new View.OnClickListener() {
|
||||
public void onClick(View v) { timeTravel(); }
|
||||
}));
|
||||
row2.addView(spacer());
|
||||
row2.addView(tile(inf, Glyph.SHUFFLE, "Random album", "Feeling lucky", new View.OnClickListener() {
|
||||
row2.addView(tile(inf, Glyph.SHUFFLE, str(R.string.random_album), str(R.string.random_album_sub), new View.OnClickListener() {
|
||||
public void onClick(View v) { randomAlbum(); }
|
||||
}));
|
||||
homeContent.addView(row2);
|
||||
|
||||
addShelf(inf, "Recently added", lib().recentlyAddedPath(20), ALBUM_SORTS[2]);
|
||||
addShelf(inf, "Recently played", lib().recentlyPlayedPath(20), null);
|
||||
addShelf(inf, str(R.string.recently_added), lib().recentlyAddedPath(20), ALBUM_SORTS[2]);
|
||||
addShelf(inf, str(R.string.recently_played), lib().recentlyPlayedPath(20), null);
|
||||
homeContent.setPadding(0, pad / 2, 0, pad);
|
||||
}
|
||||
|
||||
@@ -216,7 +224,7 @@ public class HomeActivity extends BaseActivity {
|
||||
head.addView(h, new LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f));
|
||||
if (seeAllSort != null) {
|
||||
TextView all = (TextView) inf.inflate(R.layout.row_header, null);
|
||||
all.setText("SEE ALL ›");
|
||||
all.setText(R.string.see_all);
|
||||
all.setTextColor(getResources().getColor(R.color.text2));
|
||||
all.setPadding(Ui.dp(this, 10), Ui.dp(this, 12), Ui.dp(this, 10), Ui.dp(this, 4));
|
||||
all.setBackgroundResource(R.drawable.bg_pressable);
|
||||
@@ -239,7 +247,7 @@ public class HomeActivity extends BaseActivity {
|
||||
strip.setPadding(Ui.dp(this, 6), 0, Ui.dp(this, 6), 0);
|
||||
hs.addView(strip);
|
||||
final TextView loading = new TextView(this);
|
||||
loading.setText("Loading…");
|
||||
loading.setText(R.string.loading);
|
||||
loading.setTextColor(getResources().getColor(R.color.text2));
|
||||
loading.setTextSize(12);
|
||||
loading.setPadding(Ui.dp(this, 6), Ui.dp(this, 30), 0, Ui.dp(this, 30));
|
||||
@@ -251,7 +259,7 @@ public class HomeActivity extends BaseActivity {
|
||||
public List<Item> work() throws Exception { return lib.server.items(path); }
|
||||
public void done(List<Item> items) {
|
||||
strip.removeAllViews();
|
||||
if (items.isEmpty()) { loading.setText("Nothing here yet"); strip.addView(loading); return; }
|
||||
if (items.isEmpty()) { loading.setText(R.string.nothing_here_yet); strip.addView(loading); return; }
|
||||
LayoutInflater li = getLayoutInflater();
|
||||
int art = Ui.dp(HomeActivity.this, 88);
|
||||
for (int i = 0; i < items.size(); i++) {
|
||||
@@ -266,7 +274,7 @@ public class HomeActivity extends BaseActivity {
|
||||
}
|
||||
}
|
||||
public void fail(Exception e) {
|
||||
loading.setText("Couldn't load: " + msg(e) + " (tap to retry)");
|
||||
loading.setText(str(R.string.cant_load_tap, msg(e)));
|
||||
loading.setOnClickListener(new View.OnClickListener() {
|
||||
public void onClick(View v) { loaded[TAB_HOME] = false; buildHome(); loaded[TAB_HOME] = true; }
|
||||
});
|
||||
@@ -276,20 +284,20 @@ public class HomeActivity extends BaseActivity {
|
||||
|
||||
private void timeTravel() {
|
||||
final Library lib = lib();
|
||||
toast("Finding decades…");
|
||||
toast(str(R.string.finding_decades));
|
||||
run(new Task<List<Integer>>() {
|
||||
public List<Integer> work() throws Exception { return lib.decades(); }
|
||||
public void done(final List<Integer> decades) {
|
||||
if (decades.isEmpty()) { toast("No release years in this library"); return; }
|
||||
if (decades.isEmpty()) { toast(str(R.string.no_years)); return; }
|
||||
String[] labels = new String[decades.size() + 1];
|
||||
labels[0] = "Surprise me";
|
||||
for (int i = 0; i < decades.size(); i++) labels[i + 1] = decades.get(i) + "s";
|
||||
new AlertDialog.Builder(HomeActivity.this).setTitle("Time travel radio")
|
||||
labels[0] = str(R.string.surprise_me);
|
||||
for (int i = 0; i < decades.size(); i++) labels[i + 1] = str(R.string.decade_x, decades.get(i));
|
||||
new AlertDialog.Builder(HomeActivity.this).setTitle(R.string.time_travel_title)
|
||||
.setItems(labels, new DialogInterface.OnClickListener() {
|
||||
public void onClick(DialogInterface d, int which) {
|
||||
final int dec = which == 0 ? decades.get(new Random().nextInt(decades.size())).intValue()
|
||||
: decades.get(which - 1).intValue();
|
||||
runRadio(dec + "s radio", new Task<List<Item>>() {
|
||||
runRadio(str(R.string.decade_radio_x, dec), new Task<List<Item>>() {
|
||||
public List<Item> work() throws Exception { return lib.timeTravelRadio(dec); }
|
||||
});
|
||||
}
|
||||
@@ -304,7 +312,7 @@ public class HomeActivity extends BaseActivity {
|
||||
run(new Task<List<Item>>() {
|
||||
public List<Item> work() throws Exception { return lib.server.items(lib.albumsPath("random", 0, 1)); }
|
||||
public void done(List<Item> l) {
|
||||
if (l.isEmpty()) { toast("No albums"); return; }
|
||||
if (l.isEmpty()) { toast(str(R.string.no_albums)); return; }
|
||||
Item a = l.get(0);
|
||||
playContainer(a, false, 0);
|
||||
open(a, null, 0, null);
|
||||
@@ -322,7 +330,7 @@ public class HomeActivity extends BaseActivity {
|
||||
|
||||
private void loadArtists(final int from) {
|
||||
final Library lib = lib();
|
||||
if (from == 0) showStatus(true, "Loading artists…", null);
|
||||
if (from == 0) showStatus(true, str(R.string.loading_artists), null);
|
||||
run(new Task<Object[]>() {
|
||||
public Object[] work() throws Exception {
|
||||
Json.Obj mc = lib.server.get(lib.artistsPath(from, 100));
|
||||
@@ -334,7 +342,7 @@ public class HomeActivity extends BaseActivity {
|
||||
List<Item> l = (List<Item>) r[0];
|
||||
int total = ((Integer) r[1]).intValue();
|
||||
if (from == 0) artists.set(l, total); else artists.append(l, total);
|
||||
if (from == 0 && l.isEmpty() && tab == TAB_ARTISTS) showStatus(false, "No artists in this library", null);
|
||||
if (from == 0 && l.isEmpty() && tab == TAB_ARTISTS) showStatus(false, str(R.string.no_artists_library), null);
|
||||
}
|
||||
public void fail(Exception e) {
|
||||
artists.loadFailed();
|
||||
@@ -347,7 +355,7 @@ public class HomeActivity extends BaseActivity {
|
||||
private void loadAlbums(final int from) {
|
||||
final Library lib = lib();
|
||||
final String sort = ALBUM_SORTS[Math.min(app.prefs.getInt("albumSort", 0), ALBUM_SORTS.length - 1)];
|
||||
if (from == 0) showStatus(true, "Loading albums…", null);
|
||||
if (from == 0) showStatus(true, str(R.string.loading_albums), null);
|
||||
run(new Task<Object[]>() {
|
||||
public Object[] work() throws Exception {
|
||||
Json.Obj mc = lib.server.get(lib.albumsPath(sort, from, 100));
|
||||
@@ -359,7 +367,7 @@ public class HomeActivity extends BaseActivity {
|
||||
List<Item> l = (List<Item>) r[0];
|
||||
int total = ((Integer) r[1]).intValue();
|
||||
if (from == 0) { albums.set(l, total); albumsList.setSelection(0); } else albums.append(l, total);
|
||||
if (from == 0 && l.isEmpty() && tab == TAB_ALBUMS) showStatus(false, "No albums in this library", null);
|
||||
if (from == 0 && l.isEmpty() && tab == TAB_ALBUMS) showStatus(false, str(R.string.no_albums_library), null);
|
||||
}
|
||||
public void fail(Exception e) {
|
||||
albums.loadFailed();
|
||||
@@ -371,7 +379,7 @@ public class HomeActivity extends BaseActivity {
|
||||
|
||||
private void loadPlaylists() {
|
||||
final Library lib = lib();
|
||||
showStatus(true, "Loading playlists…", null);
|
||||
showStatus(true, str(R.string.loading_playlists), null);
|
||||
run(new Task<List<Item>>() {
|
||||
public List<Item> work() throws Exception { return lib.server.items(lib.playlistsPath()); }
|
||||
public void done(List<Item> l) {
|
||||
@@ -379,7 +387,7 @@ public class HomeActivity extends BaseActivity {
|
||||
List<Item> audio = new ArrayList<Item>();
|
||||
for (int i = 0; i < l.size(); i++) if (l.get(i).type == Item.PLAYLIST) audio.add(l.get(i));
|
||||
playlists.set(audio, -1);
|
||||
if (audio.isEmpty() && tab == TAB_PLAYLISTS) showStatus(false, "No music playlists yet", null);
|
||||
if (audio.isEmpty() && tab == TAB_PLAYLISTS) showStatus(false, str(R.string.no_playlists), null);
|
||||
}
|
||||
public void fail(Exception e) {
|
||||
if (tab == TAB_PLAYLISTS) showStatus(false, msg(e), new Runnable() { public void run() { loadPlaylists(); } });
|
||||
@@ -392,8 +400,8 @@ public class HomeActivity extends BaseActivity {
|
||||
@Override
|
||||
public boolean onCreateOptionsMenu(Menu menu) {
|
||||
super.onCreateOptionsMenu(menu);
|
||||
menu.add(0, 10, 0, "Sort albums");
|
||||
menu.add(0, 11, 0, "Refresh");
|
||||
menu.add(0, 10, 0, R.string.sort_albums);
|
||||
menu.add(0, 11, 0, R.string.refresh);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -407,8 +415,10 @@ public class HomeActivity extends BaseActivity {
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
if (item.getItemId() == 10) {
|
||||
new AlertDialog.Builder(this).setTitle("Sort albums by")
|
||||
.setSingleChoiceItems(ALBUM_SORT_LABELS, app.prefs.getInt("albumSort", 0), new DialogInterface.OnClickListener() {
|
||||
String[] sortLabels = new String[ALBUM_SORT_LABELS.length];
|
||||
for (int i = 0; i < sortLabels.length; i++) sortLabels[i] = str(ALBUM_SORT_LABELS[i]);
|
||||
new AlertDialog.Builder(this).setTitle(R.string.sort_albums_by)
|
||||
.setSingleChoiceItems(sortLabels, app.prefs.getInt("albumSort", 0), new DialogInterface.OnClickListener() {
|
||||
public void onClick(DialogInterface d, int which) {
|
||||
d.dismiss();
|
||||
app.prefs.edit().putInt("albumSort", which).commit();
|
||||
|
||||
@@ -87,7 +87,7 @@ public class NowPlayingActivity extends BaseActivity {
|
||||
public void onClick(View v) {
|
||||
PlayQueue.get().cycleRepeat();
|
||||
int r = PlayQueue.get().repeat();
|
||||
toast(r == PlayQueue.REPEAT_ALL ? "Repeat queue" : r == PlayQueue.REPEAT_ONE ? "Repeat track" : "Repeat off");
|
||||
toast(str(r == PlayQueue.REPEAT_ALL ? R.string.repeat_queue : r == PlayQueue.REPEAT_ONE ? R.string.repeat_track : R.string.repeat_off));
|
||||
}
|
||||
});
|
||||
loveGlyph = Ui.glyph(this, Glyph.HEART_OUTLINE, white, 20); loveGlyph.scale(1f);
|
||||
@@ -153,8 +153,8 @@ public class NowPlayingActivity extends BaseActivity {
|
||||
Item it = PlayQueue.get().current();
|
||||
PlayQueue q = PlayQueue.get();
|
||||
if (it == null) {
|
||||
title.setText("Nothing playing");
|
||||
sub.setText("Pick something from your library");
|
||||
title.setText(R.string.nothing_playing);
|
||||
sub.setText(R.string.pick_something);
|
||||
from.setText("");
|
||||
art.setImageResource(R.drawable.art_placeholder);
|
||||
shownThumb = null;
|
||||
@@ -170,7 +170,7 @@ public class NowPlayingActivity extends BaseActivity {
|
||||
loveGlyph.setType(it.loved() ? Glyph.HEART : Glyph.HEART_OUTLINE);
|
||||
loveGlyph.setColor(it.loved() ? accent : white);
|
||||
albumLink.setVisibility(it.albumKey.length() > 0 ? View.VISIBLE : View.INVISIBLE);
|
||||
albumLink.setText("Album ›");
|
||||
albumLink.setText(R.string.album_link);
|
||||
|
||||
if (!it.thumb.equals(shownThumb)) {
|
||||
shownThumb = it.thumb;
|
||||
@@ -203,9 +203,9 @@ public class NowPlayingActivity extends BaseActivity {
|
||||
|
||||
private void updateSleep() {
|
||||
long at = PlayerService.sleepAt();
|
||||
if (PlayerService.sleepEndOfTrack()) sleepText.setText("End of track");
|
||||
if (PlayerService.sleepEndOfTrack()) sleepText.setText(R.string.end_of_track);
|
||||
else if (at > 0) sleepText.setText(Ui.time(at - System.currentTimeMillis()));
|
||||
else sleepText.setText("Sleep");
|
||||
else sleepText.setText(R.string.sleep);
|
||||
}
|
||||
|
||||
private void setBackdrop(int color) {
|
||||
@@ -216,8 +216,9 @@ public class NowPlayingActivity extends BaseActivity {
|
||||
|
||||
private void sleepDialog() {
|
||||
final int[] mins = {0, 15, 30, 45, 60, 90, -1};
|
||||
String[] labels = {"Off", "15 minutes", "30 minutes", "45 minutes", "1 hour", "1½ hours", "End of this track"};
|
||||
new AlertDialog.Builder(this).setTitle("Sleep timer")
|
||||
String[] labels = {str(R.string.sleep_off), str(R.string.sleep_15), str(R.string.sleep_30), str(R.string.sleep_45),
|
||||
str(R.string.sleep_60), str(R.string.sleep_90), str(R.string.sleep_eot)};
|
||||
new AlertDialog.Builder(this).setTitle(R.string.sleep_timer)
|
||||
.setItems(labels, new DialogInterface.OnClickListener() {
|
||||
public void onClick(DialogInterface d, int which) {
|
||||
startService(new Intent(NowPlayingActivity.this, PlayerService.class)
|
||||
|
||||
@@ -146,6 +146,7 @@ public class PlayerService extends Service implements MediaPlayer.OnPreparedList
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
App.applyLanguage(this);
|
||||
instance = this;
|
||||
audio = (AudioManager) getSystemService(AUDIO_SERVICE);
|
||||
audio.registerMediaButtonEventReceiver(new ComponentName(this, MediaButtonReceiver.class));
|
||||
@@ -261,7 +262,7 @@ public class PlayerService extends Service implements MediaPlayer.OnPreparedList
|
||||
offsetMs = 0;
|
||||
lastPos = posMs;
|
||||
playWhenReady = play;
|
||||
setState(BUFFERING, "Connecting…");
|
||||
setState(BUFFERING, getString(R.string.connecting));
|
||||
goForeground();
|
||||
if (play) requestFocus();
|
||||
final boolean download = forceDownload || app.downloadFirst();
|
||||
@@ -297,8 +298,13 @@ public class PlayerService extends Service implements MediaPlayer.OnPreparedList
|
||||
} catch (final Throwable e) {
|
||||
if (!(e instanceof Exception)) App.logError("player", e);
|
||||
final String m = com.minamp.net.Threads.describe(e);
|
||||
final int code = e instanceof com.minamp.net.RefusedException ? ((com.minamp.net.RefusedException) e).code : 0;
|
||||
post(new Runnable() {
|
||||
public void run() { if (g == gen) trackFailed(m); }
|
||||
public void run() {
|
||||
if (g != gen) return;
|
||||
if (code > 0) { refusal = m; refusalCode = code; }
|
||||
trackFailed(m);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -319,6 +325,7 @@ public class PlayerService extends Service implements MediaPlayer.OnPreparedList
|
||||
File f = new File(base, "play-" + (g % 2) + ".tmp");
|
||||
Http.Response r = null;
|
||||
String why = null;
|
||||
int lastCode = 0;
|
||||
for (int i = 0; i < urls.size(); i++) {
|
||||
r = Http.get(urls.get(i), App.get().server != null ? App.get().server.mediaHeaders() : Plex.headers(null), 30000);
|
||||
if (r.code == 200) {
|
||||
@@ -328,12 +335,13 @@ public class PlayerService extends Service implements MediaPlayer.OnPreparedList
|
||||
}
|
||||
break;
|
||||
}
|
||||
why = "Server refused the stream (" + r.code + ")";
|
||||
why = com.minamp.util.Tr.t("refused", "Server refused the stream (%1$d)", r.code);
|
||||
String body = new String(r.readAll(), "UTF-8");
|
||||
if (body.trim().length() > 0) why += ": " + Plex.snippet(body);
|
||||
lastCode = r.code;
|
||||
r = null;
|
||||
}
|
||||
if (r == null) throw new IOException(why);
|
||||
if (r == null) throw new com.minamp.net.RefusedException(lastCode, why);
|
||||
InputStream in = r.body;
|
||||
FileOutputStream out = new FileOutputStream(f);
|
||||
try {
|
||||
@@ -349,7 +357,7 @@ public class PlayerService extends Service implements MediaPlayer.OnPreparedList
|
||||
lastUi = got;
|
||||
final int pct = (int) Math.min(99, got * 100 / total);
|
||||
final int fg = g;
|
||||
post(new Runnable() { public void run() { if (fg == gen) setState(BUFFERING, "Downloading " + pct + "%"); } });
|
||||
post(new Runnable() { public void run() { if (fg == gen) setState(BUFFERING, getString(R.string.downloading_pct, pct)); } });
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
@@ -400,7 +408,7 @@ public class PlayerService extends Service implements MediaPlayer.OnPreparedList
|
||||
mp.setDataSource(proxy.register(routeId, urls, ext, transcoding,
|
||||
App.get().server != null ? App.get().server.mediaHeaders() : null));
|
||||
}
|
||||
setState(BUFFERING, "Buffering…");
|
||||
setState(BUFFERING, getString(R.string.buffering));
|
||||
mp.prepareAsync();
|
||||
} catch (Exception e) {
|
||||
trackFailed(message(e));
|
||||
@@ -433,7 +441,7 @@ public class PlayerService extends Service implements MediaPlayer.OnPreparedList
|
||||
Item it = track;
|
||||
// A stream that ends well before the track does means the network dropped.
|
||||
if (it != null && it.duration > 0 && pos > 0 && pos < it.duration - 15000 && !fromFile) {
|
||||
trackFailed("Stream interrupted");
|
||||
trackFailed(getString(R.string.stream_interrupted));
|
||||
return;
|
||||
}
|
||||
if (it != null) {
|
||||
@@ -443,7 +451,7 @@ public class PlayerService extends Service implements MediaPlayer.OnPreparedList
|
||||
if (sleepEndOfTrack) {
|
||||
sleepEndOfTrack = false;
|
||||
PlayQueue.get().advance(false);
|
||||
stopAll("Sleep timer");
|
||||
stopAll(getString(R.string.sleep_timer));
|
||||
return;
|
||||
}
|
||||
if (PlayQueue.get().advance(false)) {
|
||||
@@ -457,15 +465,14 @@ public class PlayerService extends Service implements MediaPlayer.OnPreparedList
|
||||
|
||||
public boolean onError(MediaPlayer p, int what, int extra) {
|
||||
if (p != mp) return true;
|
||||
trackFailed("Playback error (" + what + "/" + extra + ")");
|
||||
trackFailed(getString(R.string.playback_error, what, extra));
|
||||
return true;
|
||||
}
|
||||
|
||||
private void trackFailed(String msg) {
|
||||
long pos = Math.max(currentPosition(), lastPos);
|
||||
if (refusal != null) msg = refusal; // the server's own reason beats "Playback error (1/-1004)"
|
||||
boolean permanent = refusalCode >= 400 && refusalCode < 500 && refusalCode != 408 && refusalCode != 429
|
||||
|| (msg != null && msg.startsWith("Server refused the stream (4"));
|
||||
boolean permanent = refusalCode >= 400 && refusalCode < 500 && refusalCode != 408 && refusalCode != 429;
|
||||
refusal = null;
|
||||
refusalCode = 0;
|
||||
retries = permanent ? 99 : retries + 1;
|
||||
@@ -593,7 +600,7 @@ public class PlayerService extends Service implements MediaPlayer.OnPreparedList
|
||||
if (sleepAt > 0 && now >= sleepAt) {
|
||||
sleepAt = 0;
|
||||
pause();
|
||||
toast("Sleep timer: paused");
|
||||
toast(getString(R.string.sleep_paused));
|
||||
}
|
||||
if (state == PLAYING && track != null) {
|
||||
long pos = currentPosition();
|
||||
@@ -700,7 +707,7 @@ public class PlayerService extends Service implements MediaPlayer.OnPreparedList
|
||||
n.flags |= Notification.FLAG_ONGOING_EVENT | Notification.FLAG_NO_CLEAR;
|
||||
Intent open = new Intent(this, NowPlayingActivity.class).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
PendingIntent pi = PendingIntent.getActivity(this, 0, open, PendingIntent.FLAG_UPDATE_CURRENT);
|
||||
String line = (state == PAUSED ? "Paused · " : "") + it.artist;
|
||||
String line = (state == PAUSED ? getString(R.string.paused_dot) : "") + it.artist;
|
||||
n.setLatestEventInfo(this, it.title, line, pi);
|
||||
if (state == PAUSED || state == IDLE) {
|
||||
// let Android reclaim us while paused, but keep the notification as a shortcut back
|
||||
|
||||
@@ -18,11 +18,11 @@ public class QueueActivity extends BaseActivity {
|
||||
protected void onCreate(Bundle b) {
|
||||
super.onCreate(b);
|
||||
setContentView(R.layout.activity_queue);
|
||||
setupTopBar("Up next", "", true);
|
||||
setupTopBar(str(R.string.up_next), "", true);
|
||||
barAction(R.id.btn_action, Glyph.SHUFFLE, new View.OnClickListener() {
|
||||
public void onClick(View v) {
|
||||
PlayQueue.get().setShuffle(!PlayQueue.get().shuffle());
|
||||
toast(PlayQueue.get().shuffle() ? "Shuffle on" : "Shuffle off");
|
||||
toast(str(PlayQueue.get().shuffle() ? R.string.shuffle_on : R.string.shuffle_off));
|
||||
}
|
||||
});
|
||||
barAction(R.id.btn_action2, Glyph.MORE, new View.OnClickListener() {
|
||||
@@ -41,8 +41,8 @@ public class QueueActivity extends BaseActivity {
|
||||
public boolean onItemLongClick(AdapterView<?> p, View v, final int pos, long id) {
|
||||
final Item it = PlayQueue.get().at(pos);
|
||||
final boolean isCurrent = pos == PlayQueue.get().index();
|
||||
String[] opts = isCurrent ? new String[] {"Go to album", "Go to artist"}
|
||||
: new String[] {"Play next", "Remove from queue", "Go to album", "Go to artist"};
|
||||
String[] opts = isCurrent ? new String[] {str(R.string.go_to_album), str(R.string.go_to_artist)}
|
||||
: new String[] {str(R.string.play_next), str(R.string.remove_from_queue), str(R.string.go_to_album), str(R.string.go_to_artist)};
|
||||
new AlertDialog.Builder(QueueActivity.this).setTitle(it.title)
|
||||
.setItems(opts, new DialogInterface.OnClickListener() {
|
||||
public void onClick(DialogInterface d, int w) {
|
||||
@@ -73,14 +73,14 @@ public class QueueActivity extends BaseActivity {
|
||||
PlayQueue q = PlayQueue.get();
|
||||
adapter.set(q.items(), -1);
|
||||
adapter.setHighlight(q.index());
|
||||
setBarSub(q.size() + (q.size() == 1 ? " track" : " tracks") + (q.title().length() > 0 ? " · " + q.title() : ""));
|
||||
if (q.size() == 0) showStatus(false, "The queue is empty", null); else hideStatus();
|
||||
setBarSub(str(q.size() == 1 ? R.string.n_tracks_one : R.string.n_tracks, q.size()) + (q.title().length() > 0 ? " \u00b7 " + q.title() : ""));
|
||||
if (q.size() == 0) showStatus(false, str(R.string.queue_empty), null); else hideStatus();
|
||||
if (scroll && q.index() > 0) list.setSelection(Math.max(0, q.index() - 1));
|
||||
}
|
||||
|
||||
private void clearDialog() {
|
||||
new AlertDialog.Builder(this)
|
||||
.setItems(new String[] {"Clear upcoming tracks", "Clear everything"}, new DialogInterface.OnClickListener() {
|
||||
.setItems(new String[] {str(R.string.clear_upcoming), str(R.string.clear_everything)}, new DialogInterface.OnClickListener() {
|
||||
public void onClick(DialogInterface d, int w) {
|
||||
PlayQueue q = PlayQueue.get();
|
||||
if (w == 0) {
|
||||
|
||||
@@ -43,7 +43,7 @@ public class SearchActivity extends BaseActivity {
|
||||
public void onItemClick(AdapterView<?> p, View v, int pos, long id) {
|
||||
Item it = adapter.getItem(pos);
|
||||
hideKeyboard();
|
||||
open(it, null, 0, "Search");
|
||||
open(it, null, 0, str(R.string.search));
|
||||
}
|
||||
});
|
||||
list.setOnItemLongClickListener(new AdapterView.OnItemLongClickListener() {
|
||||
@@ -75,7 +75,7 @@ public class SearchActivity extends BaseActivity {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
showStatus(false, "Search " + app.sectionTitle(), null);
|
||||
showStatus(false, str(R.string.search_in, app.sectionTitle()), null);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -93,19 +93,19 @@ public class SearchActivity extends BaseActivity {
|
||||
|
||||
private void search(boolean force) {
|
||||
final String q = box.getText().toString().trim();
|
||||
if (q.length() < 2) { adapter.set(new java.util.ArrayList<Item>(), -1); showStatus(false, "Search " + app.sectionTitle(), null); return; }
|
||||
if (q.length() < 2) { adapter.set(new java.util.ArrayList<Item>(), -1); showStatus(false, str(R.string.search_in, app.sectionTitle()), null); return; }
|
||||
if (!force && q.equals(lastQuery)) return;
|
||||
lastQuery = q;
|
||||
final int my = ++seq;
|
||||
final Library lib = lib();
|
||||
showStatus(true, "Searching…", null);
|
||||
showStatus(true, str(R.string.searching), null);
|
||||
run(new Task<List<Item>>() {
|
||||
public List<Item> work() throws Exception { return lib.search(q); }
|
||||
public void done(List<Item> r) {
|
||||
if (my != seq) return;
|
||||
adapter.set(r, -1);
|
||||
list.setSelection(0);
|
||||
if (r.isEmpty()) showStatus(false, "Nothing found for “" + q + "”", null); else hideStatus();
|
||||
if (r.isEmpty()) showStatus(false, str(R.string.nothing_found_q, q), null); else hideStatus();
|
||||
}
|
||||
public void fail(Exception e) {
|
||||
if (my != seq) return;
|
||||
|
||||
@@ -103,7 +103,7 @@ public class SetupActivity extends BaseActivity {
|
||||
|
||||
private void startPin() {
|
||||
clearPendingPin();
|
||||
showBusy("Contacting plex.tv\u2026\n(the first secure connection takes a few seconds on this phone)");
|
||||
showBusy(str(R.string.contacting_plextv));
|
||||
final int my = ++pinSeq;
|
||||
run(new Task<Plex.Pin>() {
|
||||
public Plex.Pin work() throws Exception { return Plex.createPin(); }
|
||||
@@ -117,7 +117,7 @@ public class SetupActivity extends BaseActivity {
|
||||
public void fail(Exception e) {
|
||||
if (my != pinSeq) return;
|
||||
show(welcome);
|
||||
toast("Couldn't reach plex.tv: " + msg(e));
|
||||
toast(str(R.string.cant_reach_plextv, msg(e)));
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -147,7 +147,7 @@ public class SetupActivity extends BaseActivity {
|
||||
StringBuilder spaced = new StringBuilder();
|
||||
for (int i = 0; i < code.length(); i++) { if (i > 0) spaced.append(' '); spaced.append(code.charAt(i)); }
|
||||
pinCode.setText(spaced.toString());
|
||||
pinStatus.setText("Waiting for you to link\u2026");
|
||||
pinStatus.setText(R.string.pin_waiting);
|
||||
show(pinPage);
|
||||
int my = ++pinSeq;
|
||||
checkPinNow(my);
|
||||
@@ -170,7 +170,7 @@ public class SetupActivity extends BaseActivity {
|
||||
if (my != pinSeq || destroyed || pinCheckRunning) return;
|
||||
long age = System.currentTimeMillis() - pinStarted;
|
||||
if (age > 14 * 60 * 1000) {
|
||||
pinStatus.setText("Code expired \u2014 press Cancel and sign in again");
|
||||
pinStatus.setText(R.string.pin_expired);
|
||||
clearPendingPin();
|
||||
return;
|
||||
}
|
||||
@@ -183,11 +183,11 @@ public class SetupActivity extends BaseActivity {
|
||||
pinChecks++;
|
||||
if (token == null) {
|
||||
long left = (14 * 60 * 1000 - (System.currentTimeMillis() - pinStarted)) / 60000;
|
||||
pinStatus.setText("Not linked yet \u00b7 checked " + pinChecks + "\u00d7 \u00b7 " + left + " min left");
|
||||
pinStatus.setText(str(R.string.pin_not_yet, pinChecks, (int) left));
|
||||
schedulePinCheck(my);
|
||||
return;
|
||||
}
|
||||
pinStatus.setText("Linked!");
|
||||
pinStatus.setText(R.string.pin_linked);
|
||||
pinSeq++;
|
||||
clearPendingPin();
|
||||
signedIn(token);
|
||||
@@ -196,14 +196,14 @@ public class SetupActivity extends BaseActivity {
|
||||
pinCheckRunning = false;
|
||||
if (my != pinSeq) return;
|
||||
pinChecks++;
|
||||
pinStatus.setText("Check " + pinChecks + " failed: " + msg(e) + " \u2014 retrying");
|
||||
pinStatus.setText(str(R.string.pin_check_failed, pinChecks, msg(e)));
|
||||
schedulePinCheck(my);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void signedIn(final String account) {
|
||||
showBusy("Signed in. Finding your servers…");
|
||||
showBusy(str(R.string.signed_in_finding));
|
||||
app.prefs.edit().putString("account", account).commit();
|
||||
run(new Task<String>() {
|
||||
public String work() throws Exception { return Plex.username(account); }
|
||||
@@ -213,18 +213,18 @@ public class SetupActivity extends BaseActivity {
|
||||
}
|
||||
|
||||
private void loadServers(final String account) {
|
||||
showBusy("Finding your servers…");
|
||||
showBusy(str(R.string.finding_servers));
|
||||
run(new Task<List<Server>>() {
|
||||
public List<Server> work() throws Exception { return Plex.servers(account); }
|
||||
public void done(final List<Server> servers) {
|
||||
if (servers.isEmpty()) {
|
||||
show(welcome);
|
||||
toast("No Plex Media Servers on this account");
|
||||
toast(str(R.string.no_servers));
|
||||
return;
|
||||
}
|
||||
if (servers.size() == 1) { pickServer(servers.get(0), null); return; }
|
||||
chooseTitle.setText("Choose a server");
|
||||
chooseSub.setText("Signed in as " + app.prefs.getString("username", "you"));
|
||||
chooseTitle.setText(R.string.choose_server);
|
||||
chooseSub.setText(str(R.string.signed_in_as, app.prefs.getString("username", str(R.string.you))));
|
||||
chooseList.removeAllViews();
|
||||
for (int i = 0; i < servers.size(); i++) {
|
||||
final Server s = servers.get(i);
|
||||
@@ -233,7 +233,7 @@ public class SetupActivity extends BaseActivity {
|
||||
int kind = s.connections.get(k).kind;
|
||||
if (kind == Server.LOCAL || kind == Server.LOCAL_HTTP) local++; else remote++;
|
||||
}
|
||||
String sub = (s.owned ? "Yours" : "Shared with you") + " · " + (local > 0 ? "home + " : "") + (remote > 0 ? "remote" : "local only");
|
||||
String sub = str(s.owned ? R.string.server_yours : R.string.server_shared) + " \u00b7 " + (local > 0 ? str(R.string.server_home_plus) : "") + str(remote > 0 ? R.string.server_remote : R.string.server_local_only);
|
||||
addChoice(s.name, sub, new View.OnClickListener() {
|
||||
public void onClick(View v) { pickServer(s, new Runnable() { public void run() { done(servers); } }); }
|
||||
});
|
||||
@@ -243,7 +243,7 @@ public class SetupActivity extends BaseActivity {
|
||||
}
|
||||
public void fail(Exception e) {
|
||||
show(welcome);
|
||||
toast("Couldn't list servers: " + msg(e));
|
||||
toast(str(R.string.cant_list_servers, msg(e)));
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -257,7 +257,7 @@ public class SetupActivity extends BaseActivity {
|
||||
}
|
||||
|
||||
private void pickServer(final Server s, final Runnable back) {
|
||||
showBusy("Connecting to " + s.name + "…\nTrying home network, then remote, then relay.");
|
||||
showBusy(str(R.string.connecting_to, s.name));
|
||||
run(new Task<Server.Conn>() {
|
||||
public Server.Conn work() throws Exception { return s.pick(); }
|
||||
public void done(Server.Conn c) {
|
||||
@@ -275,12 +275,12 @@ public class SetupActivity extends BaseActivity {
|
||||
private void connectManual() {
|
||||
String url = ((EditText) findViewById(R.id.edit_url)).getText().toString().trim();
|
||||
String token = ((EditText) findViewById(R.id.edit_token)).getText().toString().trim();
|
||||
if (url.length() == 0) { toast("Enter your server's address"); return; }
|
||||
if (url.length() == 0) { toast(str(R.string.enter_address)); return; }
|
||||
final Server s = new Server();
|
||||
s.name = "Plex server";
|
||||
s.name = str(R.string.plex_server);
|
||||
s.token = token;
|
||||
s.add(Server.normalizeManual(url), Server.MANUAL);
|
||||
showBusy("Connecting…");
|
||||
showBusy(str(R.string.connecting));
|
||||
run(new Task<String>() {
|
||||
public String work() throws Exception {
|
||||
s.pick();
|
||||
@@ -294,7 +294,7 @@ public class SetupActivity extends BaseActivity {
|
||||
public void fail(Exception e) {
|
||||
show(manual);
|
||||
boolean auth = e instanceof Plex.PlexException && ((Plex.PlexException) e).code == 401;
|
||||
toast(auth ? "The server wants a token (or sign in with Plex instead)" : msg(e));
|
||||
toast(auth ? str(R.string.server_wants_token) : msg(e));
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -302,22 +302,22 @@ public class SetupActivity extends BaseActivity {
|
||||
// ------------------------------------------------------------------ libraries
|
||||
|
||||
private void loadSections(final Server s, final Runnable back) {
|
||||
showBusy("Loading libraries…");
|
||||
showBusy(str(R.string.loading_libraries));
|
||||
run(new Task<List<Server.Section>>() {
|
||||
public List<Server.Section> work() throws Exception { return s.musicSections(); }
|
||||
public void done(List<Server.Section> secs) {
|
||||
if (secs.isEmpty()) {
|
||||
if (back != null) back.run(); else show(welcome);
|
||||
toast(s.name + " has no music libraries");
|
||||
toast(str(R.string.no_music_libraries, s.name));
|
||||
return;
|
||||
}
|
||||
if (secs.size() == 1) { finishWith(s, secs.get(0)); return; }
|
||||
chooseTitle.setText("Choose a music library");
|
||||
chooseTitle.setText(R.string.choose_library);
|
||||
chooseSub.setText(s.name);
|
||||
chooseList.removeAllViews();
|
||||
for (int i = 0; i < secs.size(); i++) {
|
||||
final Server.Section sec = secs.get(i);
|
||||
addChoice(sec.title, "Music", new View.OnClickListener() {
|
||||
addChoice(sec.title, str(R.string.music), new View.OnClickListener() {
|
||||
public void onClick(View v) { finishWith(s, sec); }
|
||||
});
|
||||
}
|
||||
|
||||
@@ -43,8 +43,9 @@ public final class Ui {
|
||||
|
||||
public static String longDuration(long ms) {
|
||||
long min = (ms + 30000) / 60000;
|
||||
if (min < 60) return min + " min";
|
||||
return (min / 60) + " h " + (min % 60) + " min";
|
||||
App app = App.get();
|
||||
if (min < 60) return app.getString(R.string.min, (int) min);
|
||||
return app.getString(R.string.h_min, (int) (min / 60), (int) (min % 60));
|
||||
}
|
||||
|
||||
/** Average colour of a bitmap, nudged towards something vivid but dark enough for white text. */
|
||||
|
||||
@@ -7,6 +7,7 @@ import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
import com.minamp.util.Tr;
|
||||
import com.minamp.bc.asn1.ASN1Encoding;
|
||||
import com.minamp.bc.asn1.ASN1ObjectIdentifier;
|
||||
import com.minamp.bc.asn1.ASN1OctetString;
|
||||
@@ -69,7 +70,7 @@ public final class CertVerifier {
|
||||
synchronized (verifiedChains) { known = verifiedChains.contains(chainKey); }
|
||||
if (known) {
|
||||
Certificate leaf = Certificate.getInstance(chainDer.get(0));
|
||||
if (!hostMatches(leaf, host)) throw new IOException("Certificate does not match " + host);
|
||||
if (!hostMatches(leaf, host)) throw new IOException(Tr.t("cert_mismatch", "Certificate does not match %1$s", host));
|
||||
if (new Date().getTime() > 1704067200000L) checkDates(leaf, new Date());
|
||||
return;
|
||||
}
|
||||
@@ -83,7 +84,7 @@ public final class CertVerifier {
|
||||
|
||||
Certificate leaf = chain.get(0);
|
||||
if (!hostMatches(leaf, host)) {
|
||||
throw new IOException("Certificate does not match " + host);
|
||||
throw new IOException(Tr.t("cert_mismatch", "Certificate does not match %1$s", host));
|
||||
}
|
||||
|
||||
Date now = new Date();
|
||||
@@ -115,7 +116,7 @@ public final class CertVerifier {
|
||||
if (!isCa(cand)) continue;
|
||||
if (signatureValid(current, cand)) { next = cand; break; }
|
||||
}
|
||||
if (next == null) throw new IOException("Untrusted certificate chain for " + host);
|
||||
if (next == null) throw new IOException(Tr.t("cert_untrusted", "Untrusted certificate chain for %1$s", host));
|
||||
current = next;
|
||||
}
|
||||
throw new IOException("Certificate chain too long");
|
||||
@@ -125,7 +126,7 @@ public final class CertVerifier {
|
||||
Date from = c.getStartDate().getDate();
|
||||
Date to = c.getEndDate().getDate();
|
||||
if (now.before(from) || now.after(to)) {
|
||||
throw new IOException("Certificate expired or not yet valid (check the phone's date)");
|
||||
throw new IOException(Tr.t("cert_dates", "Certificate expired or not yet valid (check the phone's date)"));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.minamp.net;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/** The server answered, but refused the request (HTTP status in {@link #code}). */
|
||||
public class RefusedException extends IOException {
|
||||
public final int code;
|
||||
|
||||
public RefusedException(int code, String message) {
|
||||
super(message);
|
||||
this.code = code;
|
||||
}
|
||||
}
|
||||
@@ -197,10 +197,6 @@ public final class StreamProxy implements Runnable {
|
||||
out.flush();
|
||||
}
|
||||
|
||||
private static final class RefusedException extends IOException {
|
||||
final int code;
|
||||
RefusedException(int code, String msg) { super(msg); this.code = code; }
|
||||
}
|
||||
|
||||
/** Tries the route's URLs in order; returns the accepted response or throws with the server's words. */
|
||||
private Http.Response openUpstream(Route route, String method, String range, int first, int last) throws IOException {
|
||||
@@ -223,9 +219,10 @@ public final class StreamProxy implements Runnable {
|
||||
try { body = "HEAD".equals(method) ? "" : new String(up.readAll(), "UTF-8"); } catch (IOException ignored) {}
|
||||
up.close();
|
||||
code = up.code;
|
||||
refusal = "Server refused the stream (" + up.code + ")" + (body.trim().length() > 0 ? ": " + com.minamp.plex.Plex.snippet(body) : "");
|
||||
refusal = com.minamp.util.Tr.t("refused", "Server refused the stream (%1$d)", up.code)
|
||||
+ (body.trim().length() > 0 ? ": " + com.minamp.plex.Plex.snippet(body) : "");
|
||||
if (up.code == 503 && route.urls.get(i).indexOf("/library/parts/") >= 0) {
|
||||
refusal = "Plex couldn't open this file (503). Is the drive or folder with your music available to the server?";
|
||||
refusal = com.minamp.util.Tr.t("file_503", "Plex couldn't open this file (503). Is the drive or folder with your music available to the server?");
|
||||
}
|
||||
DiagLog.log("stream variant " + i + " refused: " + refusal);
|
||||
}
|
||||
|
||||
@@ -117,8 +117,9 @@ public final class Item {
|
||||
switch (type) {
|
||||
case TRACK: return artist + (album.length() > 0 ? " · " + album : "");
|
||||
case ALBUM: return artist + (year > 0 ? " · " + year : "");
|
||||
case PLAYLIST: return leafCount + (leafCount == 1 ? " track" : " tracks");
|
||||
case ARTIST: return "Artist";
|
||||
case PLAYLIST: return leafCount == 1 ? com.minamp.util.Tr.t("n_tracks_one", "%1$d track", leafCount)
|
||||
: com.minamp.util.Tr.t("n_tracks", "%1$d tracks", leafCount);
|
||||
case ARTIST: return com.minamp.util.Tr.t("artist", "Artist");
|
||||
default: return "";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -147,7 +147,7 @@ public final class Library {
|
||||
t.bitrate = full.bitrate; t.partSize = full.partSize;
|
||||
if (t.duration == 0) t.duration = full.duration;
|
||||
}
|
||||
if (t.partKey.length() == 0) throw new IOException("Track has no playable media");
|
||||
if (t.partKey.length() == 0) throw new IOException(com.minamp.util.Tr.t("no_media", "Track has no playable media"));
|
||||
}
|
||||
|
||||
public static void shuffle(List<Item> l) {
|
||||
|
||||
@@ -49,7 +49,7 @@ public final class Plex {
|
||||
static Json.Obj fetchJson(String method, String url, String token, int timeout) throws IOException {
|
||||
Http.Response r = Http.request(method, url, headers(token), "POST".equals(method) ? new byte[0] : null, timeout);
|
||||
String body = r.readString();
|
||||
if (r.code == 401) throw new PlexException(401, "Not authorized (401) on " + describePath(url));
|
||||
if (r.code == 401) throw new PlexException(401, com.minamp.util.Tr.t("not_authorized", "Not authorized (401) on %1$s", describePath(url)));
|
||||
if (r.code < 200 || r.code >= 300) {
|
||||
String snippet = snippet(body);
|
||||
com.minamp.net.DiagLog.log(" server said: " + snippet);
|
||||
|
||||
@@ -7,6 +7,7 @@ import java.util.Comparator;
|
||||
import java.util.List;
|
||||
|
||||
import com.minamp.net.Http;
|
||||
import com.minamp.util.Tr;
|
||||
import com.minamp.util.Json;
|
||||
|
||||
/**
|
||||
@@ -104,10 +105,10 @@ public final class Server {
|
||||
|
||||
public static String kindLabel(int kind) {
|
||||
switch (kind) {
|
||||
case LOCAL: case LOCAL_HTTP: return "Home network";
|
||||
case MANUAL: return "Direct";
|
||||
case REMOTE: case REMOTE_HTTP: return "Remote";
|
||||
case RELAY: return "Plex Relay";
|
||||
case LOCAL: case LOCAL_HTTP: return Tr.t("kind_home", "Home network");
|
||||
case MANUAL: return Tr.t("kind_direct", "Direct");
|
||||
case REMOTE: case REMOTE_HTTP: return Tr.t("kind_remote", "Remote");
|
||||
case RELAY: return Tr.t("kind_relay", "Plex Relay");
|
||||
default: return "";
|
||||
}
|
||||
}
|
||||
@@ -240,7 +241,8 @@ public final class Server {
|
||||
if (err == null) return choose(c);
|
||||
if (firstError == null) firstError = kindLabel(c.kind) + ": " + err;
|
||||
}
|
||||
throw new IOException("Can't reach " + (name.length() > 0 ? name : "server") + (firstError != null ? "\n" + firstError : ""));
|
||||
throw new IOException(Tr.t("cant_reach", "Can't reach %1$s", name.length() > 0 ? name : Tr.t("server", "server"))
|
||||
+ (firstError != null ? "\n" + firstError : ""));
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ requests
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.minamp.util;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
/**
|
||||
* Translations for the pure-Java layers (network, Plex), which can't see Android resources.
|
||||
* The app installs a {@link Source} that reads string resources named "tr_" + key; without one
|
||||
* (desktop tests) the English text passed in is used.
|
||||
*/
|
||||
public final class Tr {
|
||||
public interface Source { String get(String key); }
|
||||
public static volatile Source source;
|
||||
|
||||
private Tr() {}
|
||||
|
||||
public static String t(String key, String english, Object... args) {
|
||||
String f = null;
|
||||
Source s = source;
|
||||
if (s != null) {
|
||||
try { f = s.get(key); } catch (Exception ignored) {}
|
||||
}
|
||||
if (f == null) f = english;
|
||||
return args.length == 0 ? f : String.format(Locale.US, f, args);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user