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:
xockoandClaude Opus 5.5 committed 2026-10-15 00:11:41 +02:00
1 parent 6fd17bc9c5
commit fa67e95fde
29 files changed
+1029 -206

No files matched your search

+2 -2
View File
@@ -1,8 +1,8 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.minamp" package="com.minamp"
android:versionCode="5" android:versionCode="6"
android:versionName="1.4" android:versionName="1.5"
android:installLocation="auto"> android:installLocation="auto">
<uses-sdk android:minSdkVersion="8" android:targetSdkVersion="10" /> <uses-sdk android:minSdkVersion="8" android:targetSdkVersion="10" />
+7
View File
@@ -3,6 +3,13 @@
A small Plexamp-style music player for Plex, built for the Samsung Galaxy Mini (GT-S5570: A small Plexamp-style music player for Plex, built for the Samsung Galaxy Mini (GT-S5570:
Android 2.2/2.3, 240×320 ldpi screen, 600 MHz ARMv6, 384 MB RAM). Runs on any Android 2.2+ phone. Android 2.2/2.3, 240×320 ldpi screen, 600 MHz ARMv6, 384 MB RAM). Runs on any Android 2.2+ phone.
## Language
The interface is in **Galician** by default, whatever language the phone is set to. English is
available under *Axustes → Idioma*. All text lives in `tools/strings.py` (key → English, Galician),
which generates `res/values/strings.xml` (Galician) and `res/values-en/strings.xml`; the connection
log stays in English since it's meant for troubleshooting.
## Install ## Install
1. On the phone: **Settings → Applications → Unknown sources** (tick it). 1. On the phone: **Settings → Applications → Unknown sources** (tick it).
+4 -4
View File
@@ -11,10 +11,10 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="32dp" android:layout_height="32dp"
android:orientation="horizontal"> android:orientation="horizontal">
<TextView android:id="@+id/tab_home" style="@style/Tab" android:text="HOME" /> <TextView android:id="@+id/tab_home" style="@style/Tab" android:text="@string/tab_home" />
<TextView android:id="@+id/tab_artists" style="@style/Tab" android:text="ARTISTS" /> <TextView android:id="@+id/tab_artists" style="@style/Tab" android:text="@string/tab_artists" />
<TextView android:id="@+id/tab_albums" style="@style/Tab" android:text="ALBUMS" /> <TextView android:id="@+id/tab_albums" style="@style/Tab" android:text="@string/tab_albums" />
<TextView android:id="@+id/tab_playlists" style="@style/Tab" android:text="PLAYLISTS" /> <TextView android:id="@+id/tab_playlists" style="@style/Tab" android:text="@string/tab_playlists" />
</LinearLayout> </LinearLayout>
<View <View
+3 -3
View File
@@ -17,7 +17,7 @@
android:layout_width="0dp" android:layout_height="wrap_content" android:layout_width="0dp" android:layout_height="wrap_content"
android:layout_weight="1" android:orientation="vertical" android:gravity="center"> android:layout_weight="1" android:orientation="vertical" android:gravity="center">
<TextView android:layout_width="wrap_content" android:layout_height="wrap_content" <TextView android:layout_width="wrap_content" android:layout_height="wrap_content"
android:text="PLAYING FROM" android:textSize="9sp" android:textStyle="bold" android:text="@string/playing_from" android:textSize="9sp" android:textStyle="bold"
android:textColor="@color/text2" /> android:textColor="@color/text2" />
<TextView android:id="@+id/np_from" style="@style/Sub" <TextView android:id="@+id/np_from" style="@style/Sub"
android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_width="wrap_content" android:layout_height="wrap_content"
@@ -133,13 +133,13 @@
<TextView android:id="@+id/np_sleep_text" <TextView android:id="@+id/np_sleep_text"
android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_width="wrap_content" android:layout_height="wrap_content"
android:textSize="11sp" android:textColor="#B3FFFFFF" android:paddingLeft="5dp" android:textSize="11sp" android:textColor="#B3FFFFFF" android:paddingLeft="5dp"
android:text="Sleep" /> android:text="@string/sleep" />
</LinearLayout> </LinearLayout>
<View android:layout_width="0dp" android:layout_height="1dp" android:layout_weight="1" /> <View android:layout_width="0dp" android:layout_height="1dp" android:layout_weight="1" />
<TextView android:id="@+id/np_artist_link" <TextView android:id="@+id/np_artist_link"
android:layout_width="wrap_content" android:layout_height="match_parent" android:layout_width="wrap_content" android:layout_height="match_parent"
android:gravity="center_vertical" android:background="@drawable/bg_pressable" android:gravity="center_vertical" android:background="@drawable/bg_pressable"
android:paddingLeft="8dp" android:paddingRight="8dp" android:paddingLeft="8dp" android:paddingRight="8dp"
android:textSize="11sp" android:textColor="#B3FFFFFF" android:text="Album ›" /> android:textSize="11sp" android:textColor="#B3FFFFFF" android:text="@string/album_link" />
</LinearLayout> </LinearLayout>
</LinearLayout> </LinearLayout>
+1 -1
View File
@@ -13,7 +13,7 @@
<ImageButton android:id="@+id/btn_back" style="@style/IconButton" /> <ImageButton android:id="@+id/btn_back" style="@style/IconButton" />
<EditText android:id="@+id/search_box" style="@style/Edit" <EditText android:id="@+id/search_box" style="@style/Edit"
android:layout_width="0dp" android:layout_weight="1" android:layout_width="0dp" android:layout_weight="1"
android:hint="Artists, albums, tracks" android:hint="@string/search_hint"
android:imeOptions="actionSearch" android:imeOptions="actionSearch"
android:inputType="text" /> android:inputType="text" />
</LinearLayout> </LinearLayout>
+17 -17
View File
@@ -19,23 +19,23 @@
android:layout_marginTop="12dp" android:src="@drawable/ic_launcher" android:layout_marginTop="12dp" android:src="@drawable/ic_launcher"
android:scaleType="fitCenter" /> android:scaleType="fitCenter" />
<TextView android:layout_width="wrap_content" android:layout_height="wrap_content" <TextView android:layout_width="wrap_content" android:layout_height="wrap_content"
android:text="Minamp" android:textSize="26sp" android:textStyle="bold" android:text="@string/app_name" android:textSize="26sp" android:textStyle="bold"
android:paddingTop="8dp" /> android:paddingTop="8dp" />
<TextView android:layout_width="wrap_content" android:layout_height="wrap_content" <TextView android:layout_width="wrap_content" android:layout_height="wrap_content"
android:gravity="center" android:textColor="@color/text2" android:textSize="13sp" android:gravity="center" android:textColor="@color/text2" android:textSize="13sp"
android:paddingTop="4dp" android:paddingBottom="22dp" android:paddingTop="4dp" android:paddingBottom="22dp"
android:text="Your Plex music, anywhere,\non a very small phone." /> android:text="@string/tagline" />
<Button android:id="@+id/btn_signin" style="@style/ButtonAccent" <Button android:id="@+id/btn_signin" style="@style/ButtonAccent"
android:layout_width="match_parent" android:text="Sign in with Plex" /> android:layout_width="match_parent" android:text="@string/sign_in" />
<Button android:id="@+id/btn_manual" style="@style/ButtonOutline" <Button android:id="@+id/btn_manual" style="@style/ButtonOutline"
android:layout_width="match_parent" android:layout_marginTop="10dp" android:layout_width="match_parent" android:layout_marginTop="10dp"
android:text="Connect to a server address" /> android:text="@string/connect_address" />
<TextView android:id="@+id/btn_setup_log" <TextView android:id="@+id/btn_setup_log"
android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_width="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="18dp" android:padding="8dp" android:layout_marginTop="18dp" android:padding="8dp"
android:background="@drawable/bg_pressable" android:background="@drawable/bg_pressable"
android:textColor="@color/text2" android:textSize="12sp" android:textColor="@color/text2" android:textSize="12sp"
android:text="Connection log" /> android:text="@string/connection_log" />
</LinearLayout> </LinearLayout>
<!-- PIN --> <!-- PIN -->
@@ -46,13 +46,13 @@
<TextView android:layout_width="wrap_content" android:layout_height="wrap_content" <TextView android:layout_width="wrap_content" android:layout_height="wrap_content"
android:gravity="center" android:textColor="@color/text2" android:textSize="13sp" android:gravity="center" android:textColor="@color/text2" android:textSize="13sp"
android:paddingTop="12dp" android:paddingTop="12dp"
android:text="On a computer or a newer phone, open" /> android:text="@string/pin_open" />
<TextView android:layout_width="wrap_content" android:layout_height="wrap_content" <TextView android:layout_width="wrap_content" android:layout_height="wrap_content"
android:text="plex.tv/link" android:textSize="22sp" android:textStyle="bold" android:text="plex.tv/link" android:textSize="22sp" android:textStyle="bold"
android:textColor="@color/accent" android:paddingTop="4dp" /> android:textColor="@color/accent" android:paddingTop="4dp" />
<TextView android:layout_width="wrap_content" android:layout_height="wrap_content" <TextView android:layout_width="wrap_content" android:layout_height="wrap_content"
android:textColor="@color/text2" android:textSize="13sp" android:paddingTop="10dp" android:textColor="@color/text2" android:textSize="13sp" android:paddingTop="10dp"
android:text="and enter this code:" /> android:text="@string/pin_enter" />
<TextView android:id="@+id/pin_code" <TextView android:id="@+id/pin_code"
android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_width="wrap_content" android:layout_height="wrap_content"
android:textSize="40sp" android:textStyle="bold" android:typeface="monospace" android:textSize="40sp" android:textStyle="bold" android:typeface="monospace"
@@ -64,14 +64,14 @@
<TextView android:id="@+id/pin_status" <TextView android:id="@+id/pin_status"
android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_width="wrap_content" android:layout_height="wrap_content"
android:textColor="@color/text2" android:textSize="12sp" android:paddingLeft="8dp" android:textColor="@color/text2" android:textSize="12sp" android:paddingLeft="8dp"
android:text="Waiting for you to link…" /> android:text="@string/pin_waiting" />
</LinearLayout> </LinearLayout>
<Button android:id="@+id/btn_pin_check" style="@style/ButtonAccent" <Button android:id="@+id/btn_pin_check" style="@style/ButtonAccent"
android:layout_width="match_parent" android:layout_marginTop="20dp" android:layout_width="match_parent" android:layout_marginTop="20dp"
android:text="I've entered it \u2014 check now" /> android:text="@string/pin_check_now" />
<Button android:id="@+id/btn_pin_cancel" style="@style/ButtonOutline" <Button android:id="@+id/btn_pin_cancel" style="@style/ButtonOutline"
android:layout_width="match_parent" android:layout_marginTop="10dp" android:layout_width="match_parent" android:layout_marginTop="10dp"
android:text="Cancel" /> android:text="@string/cancel" />
</LinearLayout> </LinearLayout>
<!-- manual --> <!-- manual -->
@@ -79,21 +79,21 @@
android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_width="match_parent" android:layout_height="wrap_content"
android:orientation="vertical" android:visibility="gone"> android:orientation="vertical" android:visibility="gone">
<TextView android:layout_width="wrap_content" android:layout_height="wrap_content" <TextView android:layout_width="wrap_content" android:layout_height="wrap_content"
android:text="Server address" android:textSize="18sp" android:textStyle="bold" /> android:text="@string/server_address" android:textSize="18sp" android:textStyle="bold" />
<TextView style="@style/Sub" android:layout_width="match_parent" android:layout_height="wrap_content" <TextView style="@style/Sub" android:layout_width="match_parent" android:layout_height="wrap_content"
android:singleLine="false" android:paddingTop="4dp" android:paddingBottom="10dp" android:singleLine="false" android:paddingTop="4dp" android:paddingBottom="10dp"
android:text="e.g. 192.168.1.20 or https://plex.example.com. Signing in with Plex is easier and finds remote addresses for you." /> android:text="@string/server_address_help" />
<EditText android:id="@+id/edit_url" style="@style/Edit" <EditText android:id="@+id/edit_url" style="@style/Edit"
android:hint="Address" android:inputType="textUri" /> android:hint="@string/address_hint" android:inputType="textUri" />
<EditText android:id="@+id/edit_token" style="@style/Edit" <EditText android:id="@+id/edit_token" style="@style/Edit"
android:layout_marginTop="8dp" android:hint="X-Plex-Token (optional on your LAN)" android:layout_marginTop="8dp" android:hint="@string/token_hint"
android:inputType="textVisiblePassword" /> android:inputType="textVisiblePassword" />
<Button android:id="@+id/btn_manual_go" style="@style/ButtonAccent" <Button android:id="@+id/btn_manual_go" style="@style/ButtonAccent"
android:layout_width="match_parent" android:layout_marginTop="14dp" android:layout_width="match_parent" android:layout_marginTop="14dp"
android:text="Connect" /> android:text="@string/connect" />
<Button android:id="@+id/btn_manual_back" style="@style/ButtonOutline" <Button android:id="@+id/btn_manual_back" style="@style/ButtonOutline"
android:layout_width="match_parent" android:layout_marginTop="10dp" android:layout_width="match_parent" android:layout_marginTop="10dp"
android:text="Back" /> android:text="@string/back" />
</LinearLayout> </LinearLayout>
<!-- choose server / library --> <!-- choose server / library -->
@@ -111,7 +111,7 @@
android:orientation="vertical" /> android:orientation="vertical" />
<Button android:id="@+id/btn_choose_back" style="@style/ButtonOutline" <Button android:id="@+id/btn_choose_back" style="@style/ButtonOutline"
android:layout_width="match_parent" android:layout_marginTop="14dp" android:layout_width="match_parent" android:layout_marginTop="14dp"
android:text="Back" /> android:text="@string/back" />
</LinearLayout> </LinearLayout>
<!-- busy --> <!-- busy -->
+2 -2
View File
@@ -37,9 +37,9 @@
android:paddingTop="10dp"> android:paddingTop="10dp">
<Button android:id="@+id/detail_play" style="@style/ButtonAccent" <Button android:id="@+id/detail_play" style="@style/ButtonAccent"
android:layout_width="0dp" android:layout_weight="1" android:layout_width="0dp" android:layout_weight="1"
android:layout_marginRight="5dp" android:text="Play" /> android:layout_marginRight="5dp" android:text="@string/play" />
<Button android:id="@+id/detail_shuffle" style="@style/ButtonOutline" <Button android:id="@+id/detail_shuffle" style="@style/ButtonOutline"
android:layout_width="0dp" android:layout_weight="1" android:layout_width="0dp" android:layout_weight="1"
android:layout_marginLeft="5dp" android:text="Shuffle" /> android:layout_marginLeft="5dp" android:text="@string/shuffle" />
</LinearLayout> </LinearLayout>
</LinearLayout> </LinearLayout>
+1 -1
View File
@@ -16,5 +16,5 @@
android:paddingTop="8dp" /> android:paddingTop="8dp" />
<Button android:id="@+id/status_retry" style="@style/ButtonOutline" <Button android:id="@+id/status_retry" style="@style/ButtonOutline"
android:layout_width="wrap_content" android:layout_marginTop="12dp" android:layout_width="wrap_content" android:layout_marginTop="12dp"
android:text="Try again" android:visibility="gone" /> android:text="@string/try_again" android:visibility="gone" />
</LinearLayout> </LinearLayout>
+209
View File
@@ -0,0 +1,209 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- generated by tools/strings.py: edit that file instead -->
<resources>
<string name="add_to_queue">Add to queue</string>
<string name="added_to_queue">Added to queue</string>
<string name="adding_x">Adding %1$s…</string>
<string name="address_hint">Address</string>
<string name="album">Album</string>
<string name="album_link">Album ›</string>
<string name="app_name">Minamp</string>
<string name="applies_next_track">Applies from the next track</string>
<string name="artist">Artist</string>
<string name="artist_radio">Artist radio</string>
<string name="artist_radio_shuffle">Artist radio (shuffle)</string>
<string name="back">Back</string>
<string name="buffering">Buffering…</string>
<string name="cancel">Cancel</string>
<string name="cant_list_servers">Couldn\'t list servers: %1$s</string>
<string name="cant_load_tap">Couldn\'t load: %1$s (tap to retry)</string>
<string name="cant_reach_plextv">Couldn\'t reach plex.tv: %1$s</string>
<string name="cant_save">Couldn\'t save: %1$s</string>
<string name="cant_save_rating">Couldn\'t save rating: %1$s</string>
<string name="choose_library">Choose a music library</string>
<string name="choose_server">Choose a server</string>
<string name="clear_everything">Clear everything</string>
<string name="clear_upcoming">Clear upcoming tracks</string>
<string name="close">Close</string>
<string name="connect">Connect</string>
<string name="connect_address">Connect to a server address</string>
<string name="connected_x">Connected: %1$s</string>
<string name="connecting">Connecting…</string>
<string name="connecting_to">Connecting to %1$s…\nTrying home network, then remote, then relay.</string>
<string name="connection_log">Connection log</string>
<string name="contacting_plextv">Contacting plex.tv…\n(the first secure connection takes a few seconds on this phone)</string>
<string name="decade_radio_x">%1$ds radio</string>
<string name="decade_x">%1$ds</string>
<string name="download_first_toast">Download before playing: %1$s</string>
<string name="downloading_pct">Downloading %1$d%%</string>
<string name="end_of_track">End of track</string>
<string name="enter_address">Enter your server\'s address</string>
<string name="error_last_time">Minamp hit an error last time</string>
<string name="finding_decades">Finding decades…</string>
<string name="finding_servers">Finding your servers…</string>
<string name="go_to_album">Go to album</string>
<string name="go_to_artist">Go to artist</string>
<string name="h_min">%1$d h %2$d min</string>
<string name="https_home_off">Using plain HTTP at home (faster)</string>
<string name="https_home_on">Using HTTPS at home (each new connection is slower on this phone)</string>
<string name="language_title">Language</string>
<string name="library_radio">Library radio</string>
<string name="library_radio_sub">Shuffle everything</string>
<string name="loading">Loading…</string>
<string name="loading_albums">Loading albums…</string>
<string name="loading_artists">Loading artists…</string>
<string name="loading_libraries">Loading libraries…</string>
<string name="loading_playlists">Loading playlists…</string>
<string name="log_empty">Nothing logged yet.</string>
<string name="love">Add to favourites</string>
<string name="loved">Added to favourites</string>
<string name="loved_radio">Favourites radio</string>
<string name="loved_radio_sub">Your top rated</string>
<string name="min">%1$d min</string>
<string name="music">Music</string>
<string name="n_albums">%1$d albums</string>
<string name="n_albums_one">%1$d album</string>
<string name="n_tracks">%1$d tracks</string>
<string name="n_tracks_one">%1$d track</string>
<string name="no_albums">No albums</string>
<string name="no_albums_library">No albums in this library</string>
<string name="no_artists_library">No artists in this library</string>
<string name="no_music_libraries">%1$s has no music libraries</string>
<string name="no_playlists">No music playlists yet</string>
<string name="no_sd">No SD card available</string>
<string name="no_servers">No Plex Media Servers on this account</string>
<string name="no_tracks">No tracks</string>
<string name="no_years">No release years in this library</string>
<string name="not_connected_yet">not connected yet</string>
<string name="nothing_found_for">Nothing found for %1$s</string>
<string name="nothing_found_q">Nothing found for “%1$s”</string>
<string name="nothing_here">Nothing here</string>
<string name="nothing_here_yet">Nothing here yet</string>
<string name="nothing_playing">Nothing playing</string>
<string name="nothing_to_play">Nothing to play</string>
<string name="now_playing">Now playing</string>
<string name="off">Off</string>
<string name="ok">OK</string>
<string name="on">On</string>
<string name="paused_dot">Paused · </string>
<string name="pick_something">Pick something from your library</string>
<string name="pin_check_failed">Check %1$d failed: %2$s — retrying</string>
<string name="pin_check_now">I\'ve entered it — check now</string>
<string name="pin_enter">and enter this code:</string>
<string name="pin_expired">Code expired — press Cancel and sign in again</string>
<string name="pin_linked">Linked!</string>
<string name="pin_not_yet">Not linked yet · checked %1$d× · %2$d min left</string>
<string name="pin_open">On a computer or a newer phone, open</string>
<string name="pin_waiting">Waiting for you to link…</string>
<string name="play">Play</string>
<string name="play_all">Play all</string>
<string name="play_next">Play next</string>
<string name="playback_error">Playback error (%1$d/%2$d)</string>
<string name="playing_from">PLAYING FROM</string>
<string name="playing_next">Playing next</string>
<string name="playing_x">Playing %1$s</string>
<string name="playlist">Playlist</string>
<string name="plex_server">Plex server</string>
<string name="quality_away_title">Quality away / mobile</string>
<string name="quality_home_title">Quality at home</string>
<string name="quality_original">Original</string>
<string name="queue_empty">The queue is empty</string>
<string name="random_album">Random album</string>
<string name="random_album_sub">Feeling lucky</string>
<string name="recently_added">Recently added</string>
<string name="recently_played">Recently played</string>
<string name="reconnecting">Reconnecting…</string>
<string name="refresh">Refresh</string>
<string name="remove_from_queue">Remove from queue</string>
<string name="repeat_off">Repeat off</string>
<string name="repeat_queue">Repeat queue</string>
<string name="repeat_track">Repeat track</string>
<string name="save_to_sd">Save to SD card</string>
<string name="saved_log">Saved as Minamp-log.txt on the SD card</string>
<string name="search">Search</string>
<string name="search_hint">Artists, albums, tracks</string>
<string name="search_in">Search %1$s</string>
<string name="searching">Searching…</string>
<string name="secure"> (secure)</string>
<string name="see_all">SEE ALL ›</string>
<string name="server_address">Server address</string>
<string name="server_address_help">e.g. 192.168.1.20 or https://plex.example.com. Signing in with Plex is easier and finds remote addresses for you.</string>
<string name="server_home_plus">home + </string>
<string name="server_local_only">local only</string>
<string name="server_remote">remote</string>
<string name="server_shared">Shared with you</string>
<string name="server_wants_token">The server wants a token (or sign in with Plex instead)</string>
<string name="server_yours">Yours</string>
<string name="set_connection">Connection: %1$s</string>
<string name="set_download_first">Download tracks before playing: %1$s</string>
<string name="set_language">Language: %1$s</string>
<string name="set_library">Library: %1$s</string>
<string name="set_log">Connection log &amp; streaming test</string>
<string name="set_quality_away">Quality away / mobile data: %1$s</string>
<string name="set_quality_home">Quality at home: %1$s</string>
<string name="set_secure_home">Encrypt at home too (slower): %1$s</string>
<string name="set_server">Server: %1$s</string>
<string name="settings">Settings</string>
<string name="shuffle">Shuffle</string>
<string name="shuffle_off">Shuffle off</string>
<string name="shuffle_on">Shuffle on</string>
<string name="shuffling_x">Shuffling %1$s</string>
<string name="sign_in">Sign in with Plex</string>
<string name="sign_out">Sign out</string>
<string name="sign_out_confirm">Sign out and forget this server?</string>
<string name="signed_in_as">Signed in as %1$s</string>
<string name="signed_in_finding">Signed in. Finding your servers…</string>
<string name="sleep">Sleep</string>
<string name="sleep_15">15 minutes</string>
<string name="sleep_30">30 minutes</string>
<string name="sleep_45">45 minutes</string>
<string name="sleep_60">1 hour</string>
<string name="sleep_90">1½ hours</string>
<string name="sleep_eot">End of this track</string>
<string name="sleep_off">Off</string>
<string name="sleep_paused">Sleep timer: paused</string>
<string name="sleep_timer">Sleep timer</string>
<string name="sort_added">Recently added</string>
<string name="sort_albums">Sort albums</string>
<string name="sort_albums_by">Sort albums by</string>
<string name="sort_artist">Artist</string>
<string name="sort_played">Most played</string>
<string name="sort_title">Title</string>
<string name="sort_year">Year (newest)</string>
<string name="starting_x">Starting %1$s…</string>
<string name="stream_interrupted">Stream interrupted</string>
<string name="surprise_me">Surprise me</string>
<string name="tab_albums">ALBUMS</string>
<string name="tab_artists">ARTISTS</string>
<string name="tab_home">HOME</string>
<string name="tab_playlists">PLAYLISTS</string>
<string name="tagline">Your Plex music, anywhere,\non a very small phone.</string>
<string name="test_streaming">Test streaming</string>
<string name="testing">Testing… (about 30 s)</string>
<string name="time_travel">Time travel</string>
<string name="time_travel_sub">Pick a decade</string>
<string name="time_travel_title">Time travel radio</string>
<string name="token_hint">X-Plex-Token (optional on your LAN)</string>
<string name="tr_artist">Artist</string>
<string name="tr_cant_reach">Can\'t reach %1$s</string>
<string name="tr_cert_dates">Certificate expired or not yet valid (check the phone\'s date)</string>
<string name="tr_cert_mismatch">Certificate does not match %1$s</string>
<string name="tr_cert_untrusted">Untrusted certificate chain for %1$s</string>
<string name="tr_file_503">Plex couldn\'t open this file (503). Is the drive or folder with your music available to the server?</string>
<string name="tr_kind_direct">Direct</string>
<string name="tr_kind_home">Home network</string>
<string name="tr_kind_relay">Plex Relay</string>
<string name="tr_kind_remote">Remote</string>
<string name="tr_n_tracks">%1$d tracks</string>
<string name="tr_n_tracks_one">%1$d track</string>
<string name="tr_no_media">Track has no playable media</string>
<string name="tr_not_authorized">Not authorized (401) on %1$s</string>
<string name="tr_refused">Server refused the stream (%1$d)</string>
<string name="tr_server">server</string>
<string name="try_again">Try again</string>
<string name="unencrypted"> (unencrypted)</string>
<string name="unlove">Remove from favourites</string>
<string name="unloved">Removed from favourites</string>
<string name="up_next">Up next</string>
<string name="you">you</string>
</resources>
+205
View File
@@ -1,4 +1,209 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<!-- generated by tools/strings.py: edit that file instead -->
<resources> <resources>
<string name="add_to_queue">Engadir á cola</string>
<string name="added_to_queue">Engadido á cola</string>
<string name="adding_x">Engadindo %1$s…</string>
<string name="address_hint">Enderezo</string>
<string name="album">Álbum</string>
<string name="album_link">Álbum ›</string>
<string name="app_name">Minamp</string> <string name="app_name">Minamp</string>
<string name="applies_next_track">Aplícase desde a seguinte pista</string>
<string name="artist">Artista</string>
<string name="artist_radio">Radio do artista</string>
<string name="artist_radio_shuffle">Radio do artista (aleatorio)</string>
<string name="back">Volver</string>
<string name="buffering">Cargando…</string>
<string name="cancel">Cancelar</string>
<string name="cant_list_servers">Non se puideron listar os servidores: %1$s</string>
<string name="cant_load_tap">Non se puido cargar: %1$s (toca para tentar de novo)</string>
<string name="cant_reach_plextv">Non se puido contactar con plex.tv: %1$s</string>
<string name="cant_save">Non se puido gardar: %1$s</string>
<string name="cant_save_rating">Non se puido gardar a valoración: %1$s</string>
<string name="choose_library">Escolle unha biblioteca de música</string>
<string name="choose_server">Escolle un servidor</string>
<string name="clear_everything">Borralo todo</string>
<string name="clear_upcoming">Borrar as pistas seguintes</string>
<string name="close">Pechar</string>
<string name="connect">Conectar</string>
<string name="connect_address">Conectar a un enderezo de servidor</string>
<string name="connected_x">Conectado: %1$s</string>
<string name="connecting">Conectando…</string>
<string name="connecting_to">Conectando con %1$s…\nProbando a rede da casa, despois a remota e despois o relay.</string>
<string name="connection_log">Rexistro de conexión</string>
<string name="contacting_plextv">Contactando con plex.tv…\n(a primeira conexión segura tarda uns segundos neste teléfono)</string>
<string name="decade_radio_x">Radio da década de %1$d</string>
<string name="decade_x">Década de %1$d</string>
<string name="download_first_toast">Descargar antes de reproducir: %1$s</string>
<string name="downloading_pct">Descargando %1$d%%</string>
<string name="end_of_track">Fin da pista</string>
<string name="enter_address">Escribe o enderezo do teu servidor</string>
<string name="error_last_time">Minamp tivo un erro a última vez</string>
<string name="finding_decades">Buscando décadas…</string>
<string name="finding_servers">Buscando os teus servidores…</string>
<string name="go_to_album">Ir ao álbum</string>
<string name="go_to_artist">Ir ao artista</string>
<string name="h_min">%1$d h %2$d min</string>
<string name="https_home_off">Usando HTTP simple na casa (máis rápido)</string>
<string name="https_home_on">Usando HTTPS na casa (cada conexión nova é máis lenta neste teléfono)</string>
<string name="language_title">Idioma</string>
<string name="library_radio">Radio da biblioteca</string>
<string name="library_radio_sub">Todo ao chou</string>
<string name="loading">Cargando…</string>
<string name="loading_albums">Cargando álbums…</string>
<string name="loading_artists">Cargando artistas…</string>
<string name="loading_libraries">Cargando bibliotecas…</string>
<string name="loading_playlists">Cargando listas…</string>
<string name="log_empty">Aínda non hai nada no rexistro.</string>
<string name="love">Engadir a favoritas</string>
<string name="loved">Engadida a favoritas</string>
<string name="loved_radio">Radio de favoritas</string>
<string name="loved_radio_sub">As mellor valoradas</string>
<string name="min">%1$d min</string>
<string name="music">Música</string>
<string name="n_albums">%1$d álbums</string>
<string name="n_albums_one">%1$d álbum</string>
<string name="n_tracks">%1$d pistas</string>
<string name="n_tracks_one">%1$d pista</string>
<string name="no_albums">Non hai álbums</string>
<string name="no_albums_library">Esta biblioteca non ten álbums</string>
<string name="no_artists_library">Esta biblioteca non ten artistas</string>
<string name="no_music_libraries">%1$s non ten bibliotecas de música</string>
<string name="no_playlists">Aínda non hai listas de música</string>
<string name="no_sd">Non hai tarxeta SD dispoñible</string>
<string name="no_servers">Esta conta non ten ningún Plex Media Server</string>
<string name="no_tracks">Non hai pistas</string>
<string name="no_years">Esta biblioteca non ten anos de publicación</string>
<string name="not_connected_yet">aínda sen conectar</string>
<string name="nothing_found_for">Non se atopou nada para %1$s</string>
<string name="nothing_found_q">Non se atopou nada para “%1$s”</string>
<string name="nothing_here">Non hai nada aquí</string>
<string name="nothing_here_yet">Aínda non hai nada</string>
<string name="nothing_playing">Non se está a reproducir nada</string>
<string name="nothing_to_play">Non hai nada que reproducir</string>
<string name="now_playing">En reprodución</string>
<string name="off">Desactivado</string>
<string name="ok">Aceptar</string>
<string name="on">Activado</string>
<string name="paused_dot">En pausa · </string>
<string name="pick_something">Escolle algo da túa biblioteca</string>
<string name="pin_check_failed">Fallou a comprobación %1$d: %2$s; tentando de novo</string>
<string name="pin_check_now">Xa o escribín: comprobar agora</string>
<string name="pin_enter">e escribe este código:</string>
<string name="pin_expired">O código caducou: preme Cancelar e inicia sesión de novo</string>
<string name="pin_linked">Vinculado!</string>
<string name="pin_not_yet">Aínda sen vincular · comprobado %1$d× · quedan %2$d min</string>
<string name="pin_open">Nun ordenador ou nun teléfono máis novo, abre</string>
<string name="pin_waiting">Agardando a que vincules a conta…</string>
<string name="play">Reproducir</string>
<string name="play_all">Reproducir todo</string>
<string name="play_next">Reproducir a continuación</string>
<string name="playback_error">Erro de reprodución (%1$d/%2$d)</string>
<string name="playing_from">REPRODUCINDO DESDE</string>
<string name="playing_next">Soará a continuación</string>
<string name="playing_x">Reproducindo %1$s</string>
<string name="playlist">Lista de reprodución</string>
<string name="plex_server">Servidor Plex</string>
<string name="quality_away_title">Calidade fóra / datos móbiles</string>
<string name="quality_home_title">Calidade na casa</string>
<string name="quality_original">Orixinal</string>
<string name="queue_empty">A cola está baleira</string>
<string name="random_album">Álbum ao chou</string>
<string name="random_album_sub">Proba sorte</string>
<string name="recently_added">Engadidos recentemente</string>
<string name="recently_played">Escoitados recentemente</string>
<string name="reconnecting">Reconectando…</string>
<string name="refresh">Actualizar</string>
<string name="remove_from_queue">Quitar da cola</string>
<string name="repeat_off">Sen repetición</string>
<string name="repeat_queue">Repetir a cola</string>
<string name="repeat_track">Repetir a pista</string>
<string name="save_to_sd">Gardar na tarxeta SD</string>
<string name="saved_log">Gardado como Minamp-log.txt na tarxeta SD</string>
<string name="search">Buscar</string>
<string name="search_hint">Artistas, álbums, cancións</string>
<string name="search_in">Buscar en %1$s</string>
<string name="searching">Buscando…</string>
<string name="secure"> (cifrada)</string>
<string name="see_all">VER TODO ›</string>
<string name="server_address">Enderezo do servidor</string>
<string name="server_address_help">p. ex. 192.168.1.20 ou https://plex.exemplo.com. Iniciar sesión con Plex é máis doado e atopa por ti os enderezos remotos.</string>
<string name="server_home_plus">casa + </string>
<string name="server_local_only">só local</string>
<string name="server_remote">remoto</string>
<string name="server_shared">Compartido contigo</string>
<string name="server_wants_token">O servidor pide un token (ou inicia sesión con Plex)</string>
<string name="server_yours">Teu</string>
<string name="set_connection">Conexión: %1$s</string>
<string name="set_download_first">Descargar as pistas antes de reproducilas: %1$s</string>
<string name="set_language">Idioma: %1$s</string>
<string name="set_library">Biblioteca: %1$s</string>
<string name="set_log">Rexistro de conexión e proba de reprodución</string>
<string name="set_quality_away">Calidade fóra / datos móbiles: %1$s</string>
<string name="set_quality_home">Calidade na casa: %1$s</string>
<string name="set_secure_home">Cifrar tamén na casa (máis lento): %1$s</string>
<string name="set_server">Servidor: %1$s</string>
<string name="settings">Axustes</string>
<string name="shuffle">Aleatorio</string>
<string name="shuffle_off">Aleatorio desactivado</string>
<string name="shuffle_on">Aleatorio activado</string>
<string name="shuffling_x">Reproducindo ao chou %1$s</string>
<string name="sign_in">Iniciar sesión con Plex</string>
<string name="sign_out">Pechar sesión</string>
<string name="sign_out_confirm">Pechar sesión e esquecer este servidor?</string>
<string name="signed_in_as">Sesión iniciada como %1$s</string>
<string name="signed_in_finding">Sesión iniciada. Buscando os teus servidores…</string>
<string name="sleep">Temporizador</string>
<string name="sleep_15">15 minutos</string>
<string name="sleep_30">30 minutos</string>
<string name="sleep_45">45 minutos</string>
<string name="sleep_60">1 hora</string>
<string name="sleep_90">1½ horas</string>
<string name="sleep_eot">Ao rematar esta pista</string>
<string name="sleep_off">Desactivado</string>
<string name="sleep_paused">Temporizador de apagado: en pausa</string>
<string name="sleep_timer">Temporizador de apagado</string>
<string name="sort_added">Engadidos recentemente</string>
<string name="sort_albums">Ordenar álbums</string>
<string name="sort_albums_by">Ordenar álbums por</string>
<string name="sort_artist">Artista</string>
<string name="sort_played">Máis escoitados</string>
<string name="sort_title">Título</string>
<string name="sort_year">Ano (máis recentes)</string>
<string name="starting_x">Iniciando %1$s…</string>
<string name="stream_interrupted">Interrompeuse a transmisión</string>
<string name="surprise_me">Sorpréndeme</string>
<string name="tab_albums">ÁLBUMS</string>
<string name="tab_artists">ARTISTAS</string>
<string name="tab_home">INICIO</string>
<string name="tab_playlists">LISTAS</string>
<string name="tagline">A túa música de Plex, onde queiras,\nnun teléfono moi pequeno.</string>
<string name="test_streaming">Probar reprodución</string>
<string name="testing">Probando… (uns 30 s)</string>
<string name="time_travel">Viaxe no tempo</string>
<string name="time_travel_sub">Escolle unha década</string>
<string name="time_travel_title">Radio viaxe no tempo</string>
<string name="token_hint">X-Plex-Token (opcional na rede da casa)</string>
<string name="tr_artist">Artista</string>
<string name="tr_cant_reach">Non se pode conectar con %1$s</string>
<string name="tr_cert_dates">Certificado caducado ou aínda non válido (comproba a data do teléfono)</string>
<string name="tr_cert_mismatch">O certificado non corresponde a %1$s</string>
<string name="tr_cert_untrusted">Cadea de certificados non fiable para %1$s</string>
<string name="tr_file_503">Plex non puido abrir este ficheiro (503). Está dispoñible para o servidor o disco ou cartafol coa túa música?</string>
<string name="tr_kind_direct">Directa</string>
<string name="tr_kind_home">Rede da casa</string>
<string name="tr_kind_relay">Plex Relay</string>
<string name="tr_kind_remote">Remota</string>
<string name="tr_n_tracks">%1$d pistas</string>
<string name="tr_n_tracks_one">%1$d pista</string>
<string name="tr_no_media">A pista non ten ningún ficheiro reproducible</string>
<string name="tr_not_authorized">Sen autorización (401) en %1$s</string>
<string name="tr_refused">O servidor rexeitou a transmisión (%1$d)</string>
<string name="tr_server">servidor</string>
<string name="try_again">Tentar de novo</string>
<string name="unencrypted"> (sen cifrar)</string>
<string name="unlove">Quitar de favoritas</string>
<string name="unloved">Quitada de favoritas</string>
<string name="up_next">A continuación</string>
<string name="you">ti</string>
</resources> </resources>
+39
View File
@@ -49,6 +49,13 @@ public class App extends Application {
handler = new Handler(); handler = new Handler();
prefs = getSharedPreferences("minamp", MODE_PRIVATE); prefs = getSharedPreferences("minamp", MODE_PRIVATE);
installCrashLogger(); 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. // Android 2.2's HttpURLConnection pool is buggy; we use our own client, but be safe.
System.setProperty("http.keepAlive", "false"); System.setProperty("http.keepAlive", "false");
@@ -91,6 +98,38 @@ public class App extends Application {
}, new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION)); }, 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 // ------------------------------------------------------------------ crash reports
private static final String CRASH_FILE = "crash.txt"; private static final String CRASH_FILE = "crash.txt";
+93 -61
View File
@@ -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 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); } protected static String msg(Exception e) { return PlayerService.message(e); }
@@ -72,8 +74,10 @@ public abstract class BaseActivity extends Activity implements PlayerService.Lis
@Override @Override
protected void onCreate(android.os.Bundle b) { protected void onCreate(android.os.Bundle b) {
super.onCreate(b);
app = App.get(); 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). */ /** 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); tv.setPadding(p, p, p, p);
sv.addView(tv); sv.addView(tv);
new AlertDialog.Builder(this) new AlertDialog.Builder(this)
.setTitle("Minamp hit an error last time") .setTitle(R.string.error_last_time)
.setView(sv) .setView(sv)
.setPositiveButton("OK", null) .setPositiveButton(R.string.ok, null)
.show(); .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) { 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); PlayQueue.get().set(tracks, start, shuffle, title);
PlayerService.playCurrent(this, 0); 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) { protected void playContainer(final Item it, final boolean shuffle, final int how) {
final Library lib = lib(); final Library lib = lib();
if (lib == null) return; 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>>() { run(new Task<List<Item>>() {
public List<Item> work() throws Exception { return lib.tracksOf(it); } public List<Item> work() throws Exception { return lib.tracksOf(it); }
public void done(List<Item> tracks) { 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; boolean wasEmpty = PlayQueue.get().size() == 0;
if (how == 1) { PlayQueue.get().playNext(tracks); toast("Playing next"); } if (how == 1) { PlayQueue.get().playNext(tracks); toast(str(R.string.playing_next)); }
else if (how == 2) { PlayQueue.get().append(tracks); toast("Added to queue"); } 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; } else { playList(tracks, shuffle ? -1 : 0, shuffle, it.title); return; }
if (wasEmpty) PlayerService.playCurrent(BaseActivity.this, 0); 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) { 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>>() { run(new Task<List<Item>>() {
public List<Item> work() throws Exception { return fetch.work(); } public List<Item> work() throws Exception { return fetch.work(); }
public void done(List<Item> tracks) { 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); playList(tracks, 0, false, title);
} }
public void fail(Exception e) { toast(msg(e)); } 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<String> labels = new ArrayList<String>();
final List<Integer> actions = new ArrayList<Integer>(); final List<Integer> actions = new ArrayList<Integer>();
if (it.type == Item.TRACK) { if (it.type == Item.TRACK) {
labels.add("Play next"); actions.add(1); labels.add(str(R.string.play_next)); actions.add(1);
labels.add("Add to queue"); actions.add(2); labels.add(str(R.string.add_to_queue)); actions.add(2);
if (it.albumKey.length() > 0) { labels.add("Go to album"); actions.add(5); } if (it.albumKey.length() > 0) { labels.add(str(R.string.go_to_album)); actions.add(5); }
if (it.artistKey.length() > 0) { labels.add("Go to artist"); actions.add(6); } if (it.artistKey.length() > 0) { labels.add(str(R.string.go_to_artist)); actions.add(6); }
labels.add(it.loved() ? "Unlove" : "Love"); actions.add(7); labels.add(str(it.loved() ? R.string.unlove : R.string.love)); actions.add(7);
} else if (it.isContainer()) { } else if (it.isContainer()) {
labels.add("Play"); actions.add(0); labels.add(str(R.string.play)); actions.add(0);
labels.add(it.type == Item.ARTIST ? "Artist radio (shuffle)" : "Shuffle"); actions.add(3); labels.add(str(it.type == Item.ARTIST ? R.string.artist_radio_shuffle : R.string.shuffle)); actions.add(3);
labels.add("Play next"); actions.add(1); labels.add(str(R.string.play_next)); actions.add(1);
labels.add("Add to queue"); actions.add(2); labels.add(str(R.string.add_to_queue)); actions.add(2);
if (it.type == Item.ALBUM && it.artistKey.length() > 0) { labels.add("Go to artist"); actions.add(6); } if (it.type == Item.ALBUM && it.artistKey.length() > 0) { labels.add(str(R.string.go_to_artist)); actions.add(6); }
} else return; } else return;
new AlertDialog.Builder(this) new AlertDialog.Builder(this)
.setTitle(it.title) .setTitle(it.title)
@@ -327,7 +331,7 @@ public abstract class BaseActivity extends Activity implements PlayerService.Lis
boolean wasEmpty = PlayQueue.get().size() == 0; boolean wasEmpty = PlayQueue.get().size() == 0;
if (action == 1) PlayQueue.get().playNext(one); else PlayQueue.get().append(one); if (action == 1) PlayQueue.get().playNext(one); else PlayQueue.get().append(one);
if (wasEmpty) PlayerService.playCurrent(this, 0); 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 { } else {
playContainer(it, false, action); playContainer(it, false, action);
} }
@@ -357,11 +361,11 @@ public abstract class BaseActivity extends Activity implements PlayerService.Lis
if (after != null) after.run(); if (after != null) after.run();
run(new Task<Object>() { run(new Task<Object>() {
public Object work() throws Exception { lib.rate(it, love ? 10 : -1); return null; } 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) { public void fail(Exception e) {
it.userRating = old; it.userRating = old;
if (after != null) after.run(); 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 @Override
public boolean onCreateOptionsMenu(Menu menu) { public boolean onCreateOptionsMenu(Menu menu) {
menu.add(0, 1, 0, "Now playing"); menu.add(0, 1, 0, R.string.now_playing);
menu.add(0, 2, 0, "Search"); menu.add(0, 2, 0, R.string.search);
menu.add(0, 3, 0, "Settings"); menu.add(0, 3, 0, R.string.settings);
return true; return true;
} }
@@ -388,53 +392,54 @@ public abstract class BaseActivity extends Activity implements PlayerService.Lis
protected void showSettings() { protected void showSettings() {
Server s = app.server; Server s = app.server;
String conn = s == null || s.active() == null ? "not connected yet" : Server.kindLabel(s.active().kind) String conn = s == null || s.active() == null ? str(R.string.not_connected_yet) : Server.kindLabel(s.active().kind)
+ (s.active().uri.startsWith("https") ? " (secure)" : " (unencrypted)"); + str(s.active().uri.startsWith("https") ? R.string.secure : R.string.unencrypted);
final String[] rows = { final String[] rows = {
"Quality at home: " + qualityLabel(app.qualityHome()), str(R.string.set_quality_home, qualityLabel(app.qualityHome())),
"Quality away / mobile data: " + qualityLabel(app.qualityAway()), str(R.string.set_quality_away, qualityLabel(app.qualityAway())),
"Download tracks before playing: " + (app.downloadFirst() ? "On" : "Off"), str(R.string.set_download_first, str(app.downloadFirst() ? R.string.on : R.string.off)),
"Encrypt at home too (slower): " + (Server.preferSecureAtHome ? "On" : "Off"), str(R.string.set_secure_home, str(Server.preferSecureAtHome ? R.string.on : R.string.off)),
"Library: " + app.sectionTitle(), str(R.string.set_library, app.sectionTitle()),
"Server: " + (s == null ? "-" : s.name), str(R.string.set_server, s == null ? "-" : s.name),
"Connection: " + conn, str(R.string.set_connection, conn),
"Connection log & streaming test", str(R.string.set_log),
"Sign out", str(R.string.set_language, App.languageName(app.language())),
str(R.string.sign_out),
}; };
new AlertDialog.Builder(this) new AlertDialog.Builder(this)
.setTitle("Settings") .setTitle(R.string.settings)
.setItems(rows, new DialogInterface.OnClickListener() { .setItems(rows, new DialogInterface.OnClickListener() {
public void onClick(DialogInterface d, int which) { public void onClick(DialogInterface d, int which) {
switch (which) { switch (which) {
case 0: pickQuality("qHome", app.qualityHome(), "Quality at home"); break; case 0: pickQuality("qHome", app.qualityHome(), str(R.string.quality_home_title)); break;
case 1: pickQuality("qAway", app.qualityAway(), "Quality away / mobile"); break; case 1: pickQuality("qAway", app.qualityAway(), str(R.string.quality_away_title)); break;
case 2: case 2:
app.prefs.edit().putBoolean("downloadFirst", !app.downloadFirst()).commit(); 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; break;
case 3: case 3:
Server.preferSecureAtHome = !Server.preferSecureAtHome; Server.preferSecureAtHome = !Server.preferSecureAtHome;
app.prefs.edit().putBoolean("secureHome", Server.preferSecureAtHome).commit(); app.prefs.edit().putBoolean("secureHome", Server.preferSecureAtHome).commit();
if (app.server != null) app.server.invalidate(); if (app.server != null) app.server.invalidate();
toast(Server.preferSecureAtHome ? "Using HTTPS at home (each new connection is slower on this phone)" toast(str(Server.preferSecureAtHome ? R.string.https_home_on : R.string.https_home_off));
: "Using plain HTTP at home (faster)");
break; break;
case 4: 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_LIBRARY)); break;
case 5: startActivity(new Intent(BaseActivity.this, SetupActivity.class).putExtra(SetupActivity.EXTRA_MODE, SetupActivity.MODE_SERVER)); break; case 5: startActivity(new Intent(BaseActivity.this, SetupActivity.class).putExtra(SetupActivity.EXTRA_MODE, SetupActivity.MODE_SERVER)); break;
case 6: case 6:
if (app.server != null) { if (app.server != null) {
app.server.invalidate(); app.server.invalidate();
toast("Reconnecting…"); toast(str(R.string.reconnecting));
final Server sv = app.server; final Server sv = app.server;
run(new Task<Server.Conn>() { run(new Task<Server.Conn>() {
public Server.Conn work() throws Exception { return sv.pick(); } 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)); } public void fail(Exception e) { toast(msg(e)); }
}); });
} }
break; break;
case 7: showDiagnostics(); break; case 7: showDiagnostics(); break;
case 8: confirmSignOut(); break; case 8: pickLanguage(); break;
case 9: confirmSignOut(); break;
} }
} }
}).show(); }).show();
@@ -454,17 +459,17 @@ public abstract class BaseActivity extends Activity implements PlayerService.Lis
int p = Ui.dp(this, 6); int p = Ui.dp(this, 6);
tv.setPadding(p, p, p, p); tv.setPadding(p, p, p, p);
String log = com.minamp.net.DiagLog.dump(); 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); sv.addView(tv);
AlertDialog.Builder b = new AlertDialog.Builder(this) AlertDialog.Builder b = new AlertDialog.Builder(this)
.setTitle("Connection log") .setTitle(R.string.connection_log)
.setView(sv) .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(); } public void onClick(DialogInterface d, int w) { saveDiagnostics(); }
}) })
.setPositiveButton("Close", null); .setPositiveButton(R.string.close, null);
if (app.library != 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(); } public void onClick(DialogInterface d, int w) { testStreaming(); }
}); });
} }
@@ -476,7 +481,7 @@ public abstract class BaseActivity extends Activity implements PlayerService.Lis
private void saveDiagnostics() { private void saveDiagnostics() {
try { try {
if (!android.os.Environment.MEDIA_MOUNTED.equals(android.os.Environment.getExternalStorageState())) { if (!android.os.Environment.MEDIA_MOUNTED.equals(android.os.Environment.getExternalStorageState())) {
toast("No SD card available"); toast(str(R.string.no_sd));
return; return;
} }
java.io.File f = new java.io.File(android.os.Environment.getExternalStorageDirectory(), "Minamp-log.txt"); 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"; + ", Android " + android.os.Build.VERSION.RELEASE + "\n";
o.write((head + com.minamp.net.DiagLog.dump()).getBytes("UTF-8")); o.write((head + com.minamp.net.DiagLog.dump()).getBytes("UTF-8"));
o.close(); o.close();
toast("Saved as Minamp-log.txt on the SD card"); toast(str(R.string.saved_log));
} catch (Exception e) { } 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() { private void testStreaming() {
final Library lib = lib(); final Library lib = lib();
final Item current = PlayQueue.get().current(); final Item current = PlayQueue.get().current();
toast("Testing\u2026 (about 30 s)"); toast(str(R.string.testing));
run(new Task<Object>() { run(new Task<Object>() {
public Object work() throws Exception { public Object work() throws Exception {
com.minamp.net.DiagLog.log("=== streaming test ==="); 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]; for (int i = 0; i < App.QUALITIES.length; i++) if (App.QUALITIES[i] == kbps) return App.QUALITY_LABELS[i];
return kbps + " kbps"; 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) { private void pickQuality(final String key, int current, String title) {
int sel = 0; int sel = 0;
for (int i = 0; i < App.QUALITIES.length; i++) if (App.QUALITIES[i] == current) sel = i; for (int i = 0; i < App.QUALITIES.length; i++) if (App.QUALITIES[i] == current) sel = i;
new AlertDialog.Builder(this) new AlertDialog.Builder(this)
.setTitle(title) .setTitle(title)
.setSingleChoiceItems(App.QUALITY_LABELS, sel, new DialogInterface.OnClickListener() { .setSingleChoiceItems(qualityLabels(), sel, new DialogInterface.OnClickListener() {
public void onClick(DialogInterface d, int which) { public void onClick(DialogInterface d, int which) {
app.prefs.edit().putInt(key, App.QUALITIES[which]).commit(); app.prefs.edit().putInt(key, App.QUALITIES[which]).commit();
d.dismiss(); d.dismiss();
toast("Applies from the next track"); toast(str(R.string.applies_next_track));
} }
}).show(); }).show();
} }
private void confirmSignOut() { private void confirmSignOut() {
new AlertDialog.Builder(this) new AlertDialog.Builder(this)
.setMessage("Sign out and forget this server?") .setMessage(R.string.sign_out_confirm)
.setPositiveButton("Sign out", new DialogInterface.OnClickListener() { .setPositiveButton(R.string.sign_out, new DialogInterface.OnClickListener() {
public void onClick(DialogInterface d, int w) { public void onClick(DialogInterface d, int w) {
PlayerService.send(BaseActivity.this, PlayerService.ACTION_STOP); PlayerService.send(BaseActivity.this, PlayerService.ACTION_STOP);
PlayQueue.get().clear(); PlayQueue.get().clear();
@@ -590,7 +622,7 @@ public abstract class BaseActivity extends Activity implements PlayerService.Lis
finish(); finish();
} }
}) })
.setNegativeButton("Cancel", null) .setNegativeButton(R.string.cancel, null)
.show(); .show();
} }
} }
+8 -8
View File
@@ -63,7 +63,7 @@ public class BrowseActivity extends BaseActivity {
Button play = (Button) header.findViewById(R.id.detail_play); Button play = (Button) header.findViewById(R.id.detail_play);
Button shuffle = (Button) header.findViewById(R.id.detail_shuffle); 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); } }); play.setOnClickListener(new View.OnClickListener() { public void onClick(View v) { playAll(false); } });
shuffle.setOnClickListener(new View.OnClickListener() { public void onClick(View v) { playAll(true); } }); shuffle.setOnClickListener(new View.OnClickListener() { public void onClick(View v) { playAll(true); } });
@@ -73,9 +73,9 @@ public class BrowseActivity extends BaseActivity {
private String kindLabel() { private String kindLabel() {
switch (item.type) { switch (item.type) {
case Item.ARTIST: return "Artist"; case Item.ARTIST: return str(R.string.artist);
case Item.ALBUM: return "Album"; case Item.ALBUM: return str(R.string.album);
case Item.PLAYLIST: return "Playlist"; case Item.PLAYLIST: return str(R.string.playlist);
default: return ""; default: return "";
} }
} }
@@ -95,13 +95,13 @@ public class BrowseActivity extends BaseActivity {
private void setMeta(List<Item> children) { private void setMeta(List<Item> children) {
StringBuilder sb = new StringBuilder(); StringBuilder sb = new StringBuilder();
if (item.type == Item.ARTIST) { 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 { } else {
long total = 0; long total = 0;
int n = 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; } 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(" · "); 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)); if (total > 0) sb.append(" · ").append(Ui.longDuration(total));
} }
((TextView) header.findViewById(R.id.detail_meta)).setText(sb.toString()); ((TextView) header.findViewById(R.id.detail_meta)).setText(sb.toString());
@@ -110,7 +110,7 @@ public class BrowseActivity extends BaseActivity {
private void load() { private void load() {
final Library lib = lib(); final Library lib = lib();
final Item target = item; final Item target = item;
showStatus(true, "Loading…", null); showStatus(true, str(R.string.loading), null);
run(new Task<Object[]>() { run(new Task<Object[]>() {
public Object[] work() throws Exception { public Object[] work() throws Exception {
Item full = null; Item full = null;
@@ -137,7 +137,7 @@ public class BrowseActivity extends BaseActivity {
adapter.setAlbumArtist(item.artist); adapter.setAlbumArtist(item.artist);
adapter.set(children, -1); adapter.set(children, -1);
setMeta(children); 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) { public void fail(Exception e) {
showStatus(false, msg(e), new Runnable() { public void run() { load(); } }); showStatus(false, msg(e), new Runnable() { public void run() { load(); } });
+40 -30
View File
@@ -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 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_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 TextView[] tabs;
private View tabLine; private View tabLine;
@@ -90,6 +90,14 @@ public class HomeActivity extends BaseActivity {
@Override @Override
protected void onNewIntent(Intent intent) { protected void onNewIntent(Intent intent) {
super.onNewIntent(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 // library or server may have changed in setup
if (!app.isConfigured()) { startActivity(new Intent(this, SetupActivity.class)); finish(); return; } if (!app.isConfigured()) { startActivity(new Intent(this, SetupActivity.class)); finish(); return; }
setupTopBar(app.sectionTitle(), connectionLine(), false); setupTopBar(app.sectionTitle(), connectionLine(), false);
@@ -154,33 +162,33 @@ public class HomeActivity extends BaseActivity {
int pad = Ui.dp(this, 8); int pad = Ui.dp(this, 8);
LinearLayout row1 = tileRow(); 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) { public void onClick(View v) {
final Library lib = lib(); 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(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) { public void onClick(View v) {
final Library lib = lib(); 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); homeContent.addView(row1);
LinearLayout row2 = tileRow(); 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(); } public void onClick(View v) { timeTravel(); }
})); }));
row2.addView(spacer()); 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(); } public void onClick(View v) { randomAlbum(); }
})); }));
homeContent.addView(row2); homeContent.addView(row2);
addShelf(inf, "Recently added", lib().recentlyAddedPath(20), ALBUM_SORTS[2]); addShelf(inf, str(R.string.recently_added), lib().recentlyAddedPath(20), ALBUM_SORTS[2]);
addShelf(inf, "Recently played", lib().recentlyPlayedPath(20), null); addShelf(inf, str(R.string.recently_played), lib().recentlyPlayedPath(20), null);
homeContent.setPadding(0, pad / 2, 0, pad); 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)); head.addView(h, new LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f));
if (seeAllSort != null) { if (seeAllSort != null) {
TextView all = (TextView) inf.inflate(R.layout.row_header, 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.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.setPadding(Ui.dp(this, 10), Ui.dp(this, 12), Ui.dp(this, 10), Ui.dp(this, 4));
all.setBackgroundResource(R.drawable.bg_pressable); 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); strip.setPadding(Ui.dp(this, 6), 0, Ui.dp(this, 6), 0);
hs.addView(strip); hs.addView(strip);
final TextView loading = new TextView(this); final TextView loading = new TextView(this);
loading.setText("Loading…"); loading.setText(R.string.loading);
loading.setTextColor(getResources().getColor(R.color.text2)); loading.setTextColor(getResources().getColor(R.color.text2));
loading.setTextSize(12); loading.setTextSize(12);
loading.setPadding(Ui.dp(this, 6), Ui.dp(this, 30), 0, Ui.dp(this, 30)); 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 List<Item> work() throws Exception { return lib.server.items(path); }
public void done(List<Item> items) { public void done(List<Item> items) {
strip.removeAllViews(); 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(); LayoutInflater li = getLayoutInflater();
int art = Ui.dp(HomeActivity.this, 88); int art = Ui.dp(HomeActivity.this, 88);
for (int i = 0; i < items.size(); i++) { for (int i = 0; i < items.size(); i++) {
@@ -266,7 +274,7 @@ public class HomeActivity extends BaseActivity {
} }
} }
public void fail(Exception e) { 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() { loading.setOnClickListener(new View.OnClickListener() {
public void onClick(View v) { loaded[TAB_HOME] = false; buildHome(); loaded[TAB_HOME] = true; } 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() { private void timeTravel() {
final Library lib = lib(); final Library lib = lib();
toast("Finding decades…"); toast(str(R.string.finding_decades));
run(new Task<List<Integer>>() { run(new Task<List<Integer>>() {
public List<Integer> work() throws Exception { return lib.decades(); } public List<Integer> work() throws Exception { return lib.decades(); }
public void done(final List<Integer> 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]; String[] labels = new String[decades.size() + 1];
labels[0] = "Surprise me"; labels[0] = str(R.string.surprise_me);
for (int i = 0; i < decades.size(); i++) labels[i + 1] = decades.get(i) + "s"; 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("Time travel radio") new AlertDialog.Builder(HomeActivity.this).setTitle(R.string.time_travel_title)
.setItems(labels, new DialogInterface.OnClickListener() { .setItems(labels, new DialogInterface.OnClickListener() {
public void onClick(DialogInterface d, int which) { public void onClick(DialogInterface d, int which) {
final int dec = which == 0 ? decades.get(new Random().nextInt(decades.size())).intValue() final int dec = which == 0 ? decades.get(new Random().nextInt(decades.size())).intValue()
: decades.get(which - 1).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); } public List<Item> work() throws Exception { return lib.timeTravelRadio(dec); }
}); });
} }
@@ -304,7 +312,7 @@ public class HomeActivity extends BaseActivity {
run(new Task<List<Item>>() { run(new Task<List<Item>>() {
public List<Item> work() throws Exception { return lib.server.items(lib.albumsPath("random", 0, 1)); } public List<Item> work() throws Exception { return lib.server.items(lib.albumsPath("random", 0, 1)); }
public void done(List<Item> l) { 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); Item a = l.get(0);
playContainer(a, false, 0); playContainer(a, false, 0);
open(a, null, 0, null); open(a, null, 0, null);
@@ -322,7 +330,7 @@ public class HomeActivity extends BaseActivity {
private void loadArtists(final int from) { private void loadArtists(final int from) {
final Library lib = lib(); 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[]>() { run(new Task<Object[]>() {
public Object[] work() throws Exception { public Object[] work() throws Exception {
Json.Obj mc = lib.server.get(lib.artistsPath(from, 100)); 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]; List<Item> l = (List<Item>) r[0];
int total = ((Integer) r[1]).intValue(); int total = ((Integer) r[1]).intValue();
if (from == 0) artists.set(l, total); else artists.append(l, total); 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) { public void fail(Exception e) {
artists.loadFailed(); artists.loadFailed();
@@ -347,7 +355,7 @@ public class HomeActivity extends BaseActivity {
private void loadAlbums(final int from) { private void loadAlbums(final int from) {
final Library lib = lib(); final Library lib = lib();
final String sort = ALBUM_SORTS[Math.min(app.prefs.getInt("albumSort", 0), ALBUM_SORTS.length - 1)]; 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[]>() { run(new Task<Object[]>() {
public Object[] work() throws Exception { public Object[] work() throws Exception {
Json.Obj mc = lib.server.get(lib.albumsPath(sort, from, 100)); 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]; List<Item> l = (List<Item>) r[0];
int total = ((Integer) r[1]).intValue(); int total = ((Integer) r[1]).intValue();
if (from == 0) { albums.set(l, total); albumsList.setSelection(0); } else albums.append(l, total); 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) { public void fail(Exception e) {
albums.loadFailed(); albums.loadFailed();
@@ -371,7 +379,7 @@ public class HomeActivity extends BaseActivity {
private void loadPlaylists() { private void loadPlaylists() {
final Library lib = lib(); final Library lib = lib();
showStatus(true, "Loading playlists…", null); showStatus(true, str(R.string.loading_playlists), null);
run(new Task<List<Item>>() { run(new Task<List<Item>>() {
public List<Item> work() throws Exception { return lib.server.items(lib.playlistsPath()); } public List<Item> work() throws Exception { return lib.server.items(lib.playlistsPath()); }
public void done(List<Item> l) { public void done(List<Item> l) {
@@ -379,7 +387,7 @@ public class HomeActivity extends BaseActivity {
List<Item> audio = new ArrayList<Item>(); 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)); for (int i = 0; i < l.size(); i++) if (l.get(i).type == Item.PLAYLIST) audio.add(l.get(i));
playlists.set(audio, -1); 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) { public void fail(Exception e) {
if (tab == TAB_PLAYLISTS) showStatus(false, msg(e), new Runnable() { public void run() { loadPlaylists(); } }); if (tab == TAB_PLAYLISTS) showStatus(false, msg(e), new Runnable() { public void run() { loadPlaylists(); } });
@@ -392,8 +400,8 @@ public class HomeActivity extends BaseActivity {
@Override @Override
public boolean onCreateOptionsMenu(Menu menu) { public boolean onCreateOptionsMenu(Menu menu) {
super.onCreateOptionsMenu(menu); super.onCreateOptionsMenu(menu);
menu.add(0, 10, 0, "Sort albums"); menu.add(0, 10, 0, R.string.sort_albums);
menu.add(0, 11, 0, "Refresh"); menu.add(0, 11, 0, R.string.refresh);
return true; return true;
} }
@@ -407,8 +415,10 @@ public class HomeActivity extends BaseActivity {
@Override @Override
public boolean onOptionsItemSelected(MenuItem item) { public boolean onOptionsItemSelected(MenuItem item) {
if (item.getItemId() == 10) { if (item.getItemId() == 10) {
new AlertDialog.Builder(this).setTitle("Sort albums by") String[] sortLabels = new String[ALBUM_SORT_LABELS.length];
.setSingleChoiceItems(ALBUM_SORT_LABELS, app.prefs.getInt("albumSort", 0), new DialogInterface.OnClickListener() { 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) { public void onClick(DialogInterface d, int which) {
d.dismiss(); d.dismiss();
app.prefs.edit().putInt("albumSort", which).commit(); app.prefs.edit().putInt("albumSort", which).commit();
+9 -8
View File
@@ -87,7 +87,7 @@ public class NowPlayingActivity extends BaseActivity {
public void onClick(View v) { public void onClick(View v) {
PlayQueue.get().cycleRepeat(); PlayQueue.get().cycleRepeat();
int r = PlayQueue.get().repeat(); 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); 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(); Item it = PlayQueue.get().current();
PlayQueue q = PlayQueue.get(); PlayQueue q = PlayQueue.get();
if (it == null) { if (it == null) {
title.setText("Nothing playing"); title.setText(R.string.nothing_playing);
sub.setText("Pick something from your library"); sub.setText(R.string.pick_something);
from.setText(""); from.setText("");
art.setImageResource(R.drawable.art_placeholder); art.setImageResource(R.drawable.art_placeholder);
shownThumb = null; shownThumb = null;
@@ -170,7 +170,7 @@ public class NowPlayingActivity extends BaseActivity {
loveGlyph.setType(it.loved() ? Glyph.HEART : Glyph.HEART_OUTLINE); loveGlyph.setType(it.loved() ? Glyph.HEART : Glyph.HEART_OUTLINE);
loveGlyph.setColor(it.loved() ? accent : white); loveGlyph.setColor(it.loved() ? accent : white);
albumLink.setVisibility(it.albumKey.length() > 0 ? View.VISIBLE : View.INVISIBLE); albumLink.setVisibility(it.albumKey.length() > 0 ? View.VISIBLE : View.INVISIBLE);
albumLink.setText("Album ›"); albumLink.setText(R.string.album_link);
if (!it.thumb.equals(shownThumb)) { if (!it.thumb.equals(shownThumb)) {
shownThumb = it.thumb; shownThumb = it.thumb;
@@ -203,9 +203,9 @@ public class NowPlayingActivity extends BaseActivity {
private void updateSleep() { private void updateSleep() {
long at = PlayerService.sleepAt(); 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 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) { private void setBackdrop(int color) {
@@ -216,8 +216,9 @@ public class NowPlayingActivity extends BaseActivity {
private void sleepDialog() { private void sleepDialog() {
final int[] mins = {0, 15, 30, 45, 60, 90, -1}; 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"}; String[] labels = {str(R.string.sleep_off), str(R.string.sleep_15), str(R.string.sleep_30), str(R.string.sleep_45),
new AlertDialog.Builder(this).setTitle("Sleep timer") 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() { .setItems(labels, new DialogInterface.OnClickListener() {
public void onClick(DialogInterface d, int which) { public void onClick(DialogInterface d, int which) {
startService(new Intent(NowPlayingActivity.this, PlayerService.class) startService(new Intent(NowPlayingActivity.this, PlayerService.class)
+20 -13
View File
@@ -146,6 +146,7 @@ public class PlayerService extends Service implements MediaPlayer.OnPreparedList
@Override @Override
public void onCreate() { public void onCreate() {
super.onCreate(); super.onCreate();
App.applyLanguage(this);
instance = this; instance = this;
audio = (AudioManager) getSystemService(AUDIO_SERVICE); audio = (AudioManager) getSystemService(AUDIO_SERVICE);
audio.registerMediaButtonEventReceiver(new ComponentName(this, MediaButtonReceiver.class)); audio.registerMediaButtonEventReceiver(new ComponentName(this, MediaButtonReceiver.class));
@@ -261,7 +262,7 @@ public class PlayerService extends Service implements MediaPlayer.OnPreparedList
offsetMs = 0; offsetMs = 0;
lastPos = posMs; lastPos = posMs;
playWhenReady = play; playWhenReady = play;
setState(BUFFERING, "Connecting…"); setState(BUFFERING, getString(R.string.connecting));
goForeground(); goForeground();
if (play) requestFocus(); if (play) requestFocus();
final boolean download = forceDownload || app.downloadFirst(); final boolean download = forceDownload || app.downloadFirst();
@@ -297,8 +298,13 @@ public class PlayerService extends Service implements MediaPlayer.OnPreparedList
} catch (final Throwable e) { } catch (final Throwable e) {
if (!(e instanceof Exception)) App.logError("player", e); if (!(e instanceof Exception)) App.logError("player", e);
final String m = com.minamp.net.Threads.describe(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() { 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"); File f = new File(base, "play-" + (g % 2) + ".tmp");
Http.Response r = null; Http.Response r = null;
String why = null; String why = null;
int lastCode = 0;
for (int i = 0; i < urls.size(); i++) { 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); r = Http.get(urls.get(i), App.get().server != null ? App.get().server.mediaHeaders() : Plex.headers(null), 30000);
if (r.code == 200) { if (r.code == 200) {
@@ -328,12 +335,13 @@ public class PlayerService extends Service implements MediaPlayer.OnPreparedList
} }
break; 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"); String body = new String(r.readAll(), "UTF-8");
if (body.trim().length() > 0) why += ": " + Plex.snippet(body); if (body.trim().length() > 0) why += ": " + Plex.snippet(body);
lastCode = r.code;
r = null; r = null;
} }
if (r == null) throw new IOException(why); if (r == null) throw new com.minamp.net.RefusedException(lastCode, why);
InputStream in = r.body; InputStream in = r.body;
FileOutputStream out = new FileOutputStream(f); FileOutputStream out = new FileOutputStream(f);
try { try {
@@ -349,7 +357,7 @@ public class PlayerService extends Service implements MediaPlayer.OnPreparedList
lastUi = got; lastUi = got;
final int pct = (int) Math.min(99, got * 100 / total); final int pct = (int) Math.min(99, got * 100 / total);
final int fg = g; 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 { } finally {
@@ -400,7 +408,7 @@ public class PlayerService extends Service implements MediaPlayer.OnPreparedList
mp.setDataSource(proxy.register(routeId, urls, ext, transcoding, mp.setDataSource(proxy.register(routeId, urls, ext, transcoding,
App.get().server != null ? App.get().server.mediaHeaders() : null)); App.get().server != null ? App.get().server.mediaHeaders() : null));
} }
setState(BUFFERING, "Buffering…"); setState(BUFFERING, getString(R.string.buffering));
mp.prepareAsync(); mp.prepareAsync();
} catch (Exception e) { } catch (Exception e) {
trackFailed(message(e)); trackFailed(message(e));
@@ -433,7 +441,7 @@ public class PlayerService extends Service implements MediaPlayer.OnPreparedList
Item it = track; Item it = track;
// A stream that ends well before the track does means the network dropped. // 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) { if (it != null && it.duration > 0 && pos > 0 && pos < it.duration - 15000 && !fromFile) {
trackFailed("Stream interrupted"); trackFailed(getString(R.string.stream_interrupted));
return; return;
} }
if (it != null) { if (it != null) {
@@ -443,7 +451,7 @@ public class PlayerService extends Service implements MediaPlayer.OnPreparedList
if (sleepEndOfTrack) { if (sleepEndOfTrack) {
sleepEndOfTrack = false; sleepEndOfTrack = false;
PlayQueue.get().advance(false); PlayQueue.get().advance(false);
stopAll("Sleep timer"); stopAll(getString(R.string.sleep_timer));
return; return;
} }
if (PlayQueue.get().advance(false)) { 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) { public boolean onError(MediaPlayer p, int what, int extra) {
if (p != mp) return true; if (p != mp) return true;
trackFailed("Playback error (" + what + "/" + extra + ")"); trackFailed(getString(R.string.playback_error, what, extra));
return true; return true;
} }
private void trackFailed(String msg) { private void trackFailed(String msg) {
long pos = Math.max(currentPosition(), lastPos); long pos = Math.max(currentPosition(), lastPos);
if (refusal != null) msg = refusal; // the server's own reason beats "Playback error (1/-1004)" 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 boolean permanent = refusalCode >= 400 && refusalCode < 500 && refusalCode != 408 && refusalCode != 429;
|| (msg != null && msg.startsWith("Server refused the stream (4"));
refusal = null; refusal = null;
refusalCode = 0; refusalCode = 0;
retries = permanent ? 99 : retries + 1; retries = permanent ? 99 : retries + 1;
@@ -593,7 +600,7 @@ public class PlayerService extends Service implements MediaPlayer.OnPreparedList
if (sleepAt > 0 && now >= sleepAt) { if (sleepAt > 0 && now >= sleepAt) {
sleepAt = 0; sleepAt = 0;
pause(); pause();
toast("Sleep timer: paused"); toast(getString(R.string.sleep_paused));
} }
if (state == PLAYING && track != null) { if (state == PLAYING && track != null) {
long pos = currentPosition(); 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; n.flags |= Notification.FLAG_ONGOING_EVENT | Notification.FLAG_NO_CLEAR;
Intent open = new Intent(this, NowPlayingActivity.class).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); Intent open = new Intent(this, NowPlayingActivity.class).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
PendingIntent pi = PendingIntent.getActivity(this, 0, open, PendingIntent.FLAG_UPDATE_CURRENT); 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); n.setLatestEventInfo(this, it.title, line, pi);
if (state == PAUSED || state == IDLE) { if (state == PAUSED || state == IDLE) {
// let Android reclaim us while paused, but keep the notification as a shortcut back // let Android reclaim us while paused, but keep the notification as a shortcut back
+7 -7
View File
@@ -18,11 +18,11 @@ public class QueueActivity extends BaseActivity {
protected void onCreate(Bundle b) { protected void onCreate(Bundle b) {
super.onCreate(b); super.onCreate(b);
setContentView(R.layout.activity_queue); 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() { barAction(R.id.btn_action, Glyph.SHUFFLE, new View.OnClickListener() {
public void onClick(View v) { public void onClick(View v) {
PlayQueue.get().setShuffle(!PlayQueue.get().shuffle()); 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() { 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) { public boolean onItemLongClick(AdapterView<?> p, View v, final int pos, long id) {
final Item it = PlayQueue.get().at(pos); final Item it = PlayQueue.get().at(pos);
final boolean isCurrent = pos == PlayQueue.get().index(); final boolean isCurrent = pos == PlayQueue.get().index();
String[] opts = isCurrent ? new String[] {"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[] {"Play next", "Remove from queue", "Go to album", "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) new AlertDialog.Builder(QueueActivity.this).setTitle(it.title)
.setItems(opts, new DialogInterface.OnClickListener() { .setItems(opts, new DialogInterface.OnClickListener() {
public void onClick(DialogInterface d, int w) { public void onClick(DialogInterface d, int w) {
@@ -73,14 +73,14 @@ public class QueueActivity extends BaseActivity {
PlayQueue q = PlayQueue.get(); PlayQueue q = PlayQueue.get();
adapter.set(q.items(), -1); adapter.set(q.items(), -1);
adapter.setHighlight(q.index()); adapter.setHighlight(q.index());
setBarSub(q.size() + (q.size() == 1 ? " track" : " tracks") + (q.title().length() > 0 ? " · " + q.title() : "")); 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, "The queue is empty", null); else hideStatus(); 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)); if (scroll && q.index() > 0) list.setSelection(Math.max(0, q.index() - 1));
} }
private void clearDialog() { private void clearDialog() {
new AlertDialog.Builder(this) 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) { public void onClick(DialogInterface d, int w) {
PlayQueue q = PlayQueue.get(); PlayQueue q = PlayQueue.get();
if (w == 0) { if (w == 0) {
+5 -5
View File
@@ -43,7 +43,7 @@ public class SearchActivity extends BaseActivity {
public void onItemClick(AdapterView<?> p, View v, int pos, long id) { public void onItemClick(AdapterView<?> p, View v, int pos, long id) {
Item it = adapter.getItem(pos); Item it = adapter.getItem(pos);
hideKeyboard(); hideKeyboard();
open(it, null, 0, "Search"); open(it, null, 0, str(R.string.search));
} }
}); });
list.setOnItemLongClickListener(new AdapterView.OnItemLongClickListener() { list.setOnItemLongClickListener(new AdapterView.OnItemLongClickListener() {
@@ -75,7 +75,7 @@ public class SearchActivity extends BaseActivity {
return false; return false;
} }
}); });
showStatus(false, "Search " + app.sectionTitle(), null); showStatus(false, str(R.string.search_in, app.sectionTitle()), null);
} }
@Override @Override
@@ -93,19 +93,19 @@ public class SearchActivity extends BaseActivity {
private void search(boolean force) { private void search(boolean force) {
final String q = box.getText().toString().trim(); 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; if (!force && q.equals(lastQuery)) return;
lastQuery = q; lastQuery = q;
final int my = ++seq; final int my = ++seq;
final Library lib = lib(); final Library lib = lib();
showStatus(true, "Searching…", null); showStatus(true, str(R.string.searching), null);
run(new Task<List<Item>>() { run(new Task<List<Item>>() {
public List<Item> work() throws Exception { return lib.search(q); } public List<Item> work() throws Exception { return lib.search(q); }
public void done(List<Item> r) { public void done(List<Item> r) {
if (my != seq) return; if (my != seq) return;
adapter.set(r, -1); adapter.set(r, -1);
list.setSelection(0); 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) { public void fail(Exception e) {
if (my != seq) return; if (my != seq) return;
+23 -23
View File
@@ -103,7 +103,7 @@ public class SetupActivity extends BaseActivity {
private void startPin() { private void startPin() {
clearPendingPin(); 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; final int my = ++pinSeq;
run(new Task<Plex.Pin>() { run(new Task<Plex.Pin>() {
public Plex.Pin work() throws Exception { return Plex.createPin(); } public Plex.Pin work() throws Exception { return Plex.createPin(); }
@@ -117,7 +117,7 @@ public class SetupActivity extends BaseActivity {
public void fail(Exception e) { public void fail(Exception e) {
if (my != pinSeq) return; if (my != pinSeq) return;
show(welcome); 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(); StringBuilder spaced = new StringBuilder();
for (int i = 0; i < code.length(); i++) { if (i > 0) spaced.append(' '); spaced.append(code.charAt(i)); } for (int i = 0; i < code.length(); i++) { if (i > 0) spaced.append(' '); spaced.append(code.charAt(i)); }
pinCode.setText(spaced.toString()); pinCode.setText(spaced.toString());
pinStatus.setText("Waiting for you to link\u2026"); pinStatus.setText(R.string.pin_waiting);
show(pinPage); show(pinPage);
int my = ++pinSeq; int my = ++pinSeq;
checkPinNow(my); checkPinNow(my);
@@ -170,7 +170,7 @@ public class SetupActivity extends BaseActivity {
if (my != pinSeq || destroyed || pinCheckRunning) return; if (my != pinSeq || destroyed || pinCheckRunning) return;
long age = System.currentTimeMillis() - pinStarted; long age = System.currentTimeMillis() - pinStarted;
if (age > 14 * 60 * 1000) { if (age > 14 * 60 * 1000) {
pinStatus.setText("Code expired \u2014 press Cancel and sign in again"); pinStatus.setText(R.string.pin_expired);
clearPendingPin(); clearPendingPin();
return; return;
} }
@@ -183,11 +183,11 @@ public class SetupActivity extends BaseActivity {
pinChecks++; pinChecks++;
if (token == null) { if (token == null) {
long left = (14 * 60 * 1000 - (System.currentTimeMillis() - pinStarted)) / 60000; 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); schedulePinCheck(my);
return; return;
} }
pinStatus.setText("Linked!"); pinStatus.setText(R.string.pin_linked);
pinSeq++; pinSeq++;
clearPendingPin(); clearPendingPin();
signedIn(token); signedIn(token);
@@ -196,14 +196,14 @@ public class SetupActivity extends BaseActivity {
pinCheckRunning = false; pinCheckRunning = false;
if (my != pinSeq) return; if (my != pinSeq) return;
pinChecks++; pinChecks++;
pinStatus.setText("Check " + pinChecks + " failed: " + msg(e) + " \u2014 retrying"); pinStatus.setText(str(R.string.pin_check_failed, pinChecks, msg(e)));
schedulePinCheck(my); schedulePinCheck(my);
} }
}); });
} }
private void signedIn(final String account) { 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(); app.prefs.edit().putString("account", account).commit();
run(new Task<String>() { run(new Task<String>() {
public String work() throws Exception { return Plex.username(account); } public String work() throws Exception { return Plex.username(account); }
@@ -213,18 +213,18 @@ public class SetupActivity extends BaseActivity {
} }
private void loadServers(final String account) { private void loadServers(final String account) {
showBusy("Finding your servers…"); showBusy(str(R.string.finding_servers));
run(new Task<List<Server>>() { run(new Task<List<Server>>() {
public List<Server> work() throws Exception { return Plex.servers(account); } public List<Server> work() throws Exception { return Plex.servers(account); }
public void done(final List<Server> servers) { public void done(final List<Server> servers) {
if (servers.isEmpty()) { if (servers.isEmpty()) {
show(welcome); show(welcome);
toast("No Plex Media Servers on this account"); toast(str(R.string.no_servers));
return; return;
} }
if (servers.size() == 1) { pickServer(servers.get(0), null); return; } if (servers.size() == 1) { pickServer(servers.get(0), null); return; }
chooseTitle.setText("Choose a server"); chooseTitle.setText(R.string.choose_server);
chooseSub.setText("Signed in as " + app.prefs.getString("username", "you")); chooseSub.setText(str(R.string.signed_in_as, app.prefs.getString("username", str(R.string.you))));
chooseList.removeAllViews(); chooseList.removeAllViews();
for (int i = 0; i < servers.size(); i++) { for (int i = 0; i < servers.size(); i++) {
final Server s = servers.get(i); final Server s = servers.get(i);
@@ -233,7 +233,7 @@ public class SetupActivity extends BaseActivity {
int kind = s.connections.get(k).kind; int kind = s.connections.get(k).kind;
if (kind == Server.LOCAL || kind == Server.LOCAL_HTTP) local++; else remote++; 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() { addChoice(s.name, sub, new View.OnClickListener() {
public void onClick(View v) { pickServer(s, new Runnable() { public void run() { done(servers); } }); } 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) { public void fail(Exception e) {
show(welcome); 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) { 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>() { run(new Task<Server.Conn>() {
public Server.Conn work() throws Exception { return s.pick(); } public Server.Conn work() throws Exception { return s.pick(); }
public void done(Server.Conn c) { public void done(Server.Conn c) {
@@ -275,12 +275,12 @@ public class SetupActivity extends BaseActivity {
private void connectManual() { private void connectManual() {
String url = ((EditText) findViewById(R.id.edit_url)).getText().toString().trim(); String url = ((EditText) findViewById(R.id.edit_url)).getText().toString().trim();
String token = ((EditText) findViewById(R.id.edit_token)).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(); final Server s = new Server();
s.name = "Plex server"; s.name = str(R.string.plex_server);
s.token = token; s.token = token;
s.add(Server.normalizeManual(url), Server.MANUAL); s.add(Server.normalizeManual(url), Server.MANUAL);
showBusy("Connecting…"); showBusy(str(R.string.connecting));
run(new Task<String>() { run(new Task<String>() {
public String work() throws Exception { public String work() throws Exception {
s.pick(); s.pick();
@@ -294,7 +294,7 @@ public class SetupActivity extends BaseActivity {
public void fail(Exception e) { public void fail(Exception e) {
show(manual); show(manual);
boolean auth = e instanceof Plex.PlexException && ((Plex.PlexException) e).code == 401; 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 // ------------------------------------------------------------------ libraries
private void loadSections(final Server s, final Runnable back) { private void loadSections(final Server s, final Runnable back) {
showBusy("Loading libraries…"); showBusy(str(R.string.loading_libraries));
run(new Task<List<Server.Section>>() { run(new Task<List<Server.Section>>() {
public List<Server.Section> work() throws Exception { return s.musicSections(); } public List<Server.Section> work() throws Exception { return s.musicSections(); }
public void done(List<Server.Section> secs) { public void done(List<Server.Section> secs) {
if (secs.isEmpty()) { if (secs.isEmpty()) {
if (back != null) back.run(); else show(welcome); 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; return;
} }
if (secs.size() == 1) { finishWith(s, secs.get(0)); 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); chooseSub.setText(s.name);
chooseList.removeAllViews(); chooseList.removeAllViews();
for (int i = 0; i < secs.size(); i++) { for (int i = 0; i < secs.size(); i++) {
final Server.Section sec = secs.get(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); } public void onClick(View v) { finishWith(s, sec); }
}); });
} }
+3 -2
View File
@@ -43,8 +43,9 @@ public final class Ui {
public static String longDuration(long ms) { public static String longDuration(long ms) {
long min = (ms + 30000) / 60000; long min = (ms + 30000) / 60000;
if (min < 60) return min + " min"; App app = App.get();
return (min / 60) + " h " + (min % 60) + " min"; 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. */ /** Average colour of a bitmap, nudged towards something vivid but dark enough for white text. */
+5 -4
View File
@@ -7,6 +7,7 @@ import java.util.Date;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
import com.minamp.util.Tr;
import com.minamp.bc.asn1.ASN1Encoding; import com.minamp.bc.asn1.ASN1Encoding;
import com.minamp.bc.asn1.ASN1ObjectIdentifier; import com.minamp.bc.asn1.ASN1ObjectIdentifier;
import com.minamp.bc.asn1.ASN1OctetString; import com.minamp.bc.asn1.ASN1OctetString;
@@ -69,7 +70,7 @@ public final class CertVerifier {
synchronized (verifiedChains) { known = verifiedChains.contains(chainKey); } synchronized (verifiedChains) { known = verifiedChains.contains(chainKey); }
if (known) { if (known) {
Certificate leaf = Certificate.getInstance(chainDer.get(0)); 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()); if (new Date().getTime() > 1704067200000L) checkDates(leaf, new Date());
return; return;
} }
@@ -83,7 +84,7 @@ public final class CertVerifier {
Certificate leaf = chain.get(0); Certificate leaf = chain.get(0);
if (!hostMatches(leaf, host)) { 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(); Date now = new Date();
@@ -115,7 +116,7 @@ public final class CertVerifier {
if (!isCa(cand)) continue; if (!isCa(cand)) continue;
if (signatureValid(current, cand)) { next = cand; break; } 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; current = next;
} }
throw new IOException("Certificate chain too long"); throw new IOException("Certificate chain too long");
@@ -125,7 +126,7 @@ public final class CertVerifier {
Date from = c.getStartDate().getDate(); Date from = c.getStartDate().getDate();
Date to = c.getEndDate().getDate(); Date to = c.getEndDate().getDate();
if (now.before(from) || now.after(to)) { 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)"));
} }
} }
+13
View File
@@ -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;
}
}
+3 -6
View File
@@ -197,10 +197,6 @@ public final class StreamProxy implements Runnable {
out.flush(); 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. */ /** 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 { 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) {} try { body = "HEAD".equals(method) ? "" : new String(up.readAll(), "UTF-8"); } catch (IOException ignored) {}
up.close(); up.close();
code = up.code; 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) { 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); DiagLog.log("stream variant " + i + " refused: " + refusal);
} }
+3 -2
View File
@@ -117,8 +117,9 @@ public final class Item {
switch (type) { switch (type) {
case TRACK: return artist + (album.length() > 0 ? " · " + album : ""); case TRACK: return artist + (album.length() > 0 ? " · " + album : "");
case ALBUM: return artist + (year > 0 ? " · " + year : ""); case ALBUM: return artist + (year > 0 ? " · " + year : "");
case PLAYLIST: return leafCount + (leafCount == 1 ? " track" : " tracks"); case PLAYLIST: return leafCount == 1 ? com.minamp.util.Tr.t("n_tracks_one", "%1$d track", leafCount)
case ARTIST: return "Artist"; : com.minamp.util.Tr.t("n_tracks", "%1$d tracks", leafCount);
case ARTIST: return com.minamp.util.Tr.t("artist", "Artist");
default: return ""; default: return "";
} }
} }
+1 -1
View File
@@ -147,7 +147,7 @@ public final class Library {
t.bitrate = full.bitrate; t.partSize = full.partSize; t.bitrate = full.bitrate; t.partSize = full.partSize;
if (t.duration == 0) t.duration = full.duration; 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) { public static void shuffle(List<Item> l) {
+1 -1
View File
@@ -49,7 +49,7 @@ public final class Plex {
static Json.Obj fetchJson(String method, String url, String token, int timeout) throws IOException { 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); Http.Response r = Http.request(method, url, headers(token), "POST".equals(method) ? new byte[0] : null, timeout);
String body = r.readString(); 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) { if (r.code < 200 || r.code >= 300) {
String snippet = snippet(body); String snippet = snippet(body);
com.minamp.net.DiagLog.log(" server said: " + snippet); com.minamp.net.DiagLog.log(" server said: " + snippet);
+7 -5
View File
@@ -7,6 +7,7 @@ import java.util.Comparator;
import java.util.List; import java.util.List;
import com.minamp.net.Http; import com.minamp.net.Http;
import com.minamp.util.Tr;
import com.minamp.util.Json; import com.minamp.util.Json;
/** /**
@@ -104,10 +105,10 @@ public final class Server {
public static String kindLabel(int kind) { public static String kindLabel(int kind) {
switch (kind) { switch (kind) {
case LOCAL: case LOCAL_HTTP: return "Home network"; case LOCAL: case LOCAL_HTTP: return Tr.t("kind_home", "Home network");
case MANUAL: return "Direct"; case MANUAL: return Tr.t("kind_direct", "Direct");
case REMOTE: case REMOTE_HTTP: return "Remote"; case REMOTE: case REMOTE_HTTP: return Tr.t("kind_remote", "Remote");
case RELAY: return "Plex Relay"; case RELAY: return Tr.t("kind_relay", "Plex Relay");
default: return ""; default: return "";
} }
} }
@@ -240,7 +241,8 @@ public final class Server {
if (err == null) return choose(c); if (err == null) return choose(c);
if (firstError == null) firstError = kindLabel(c.kind) + ": " + err; 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 // ------------------------------------------------------------------ requests
+25
View File
@@ -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);
}
}
+273
View File
@@ -0,0 +1,273 @@
"""Single source of truth for UI text: key -> (English, Galician).
Generates res/values/strings.xml (Galician, the default) and res/values-en/strings.xml (English).
Keys starting with tr_ are looked up by name from the pure-Java layers (com.minamp.util.Tr).
Run: python3 tools/strings.py
"""
import os
S = {
"app_name": ("Minamp", "Minamp"),
# tabs / headers
"tab_home": ("HOME", "INICIO"),
"tab_artists": ("ARTISTS", "ARTISTAS"),
"tab_albums": ("ALBUMS", "ÁLBUMS"),
"tab_playlists": ("PLAYLISTS", "LISTAS"),
"playing_from": ("PLAYING FROM", "REPRODUCINDO DESDE"),
"see_all": ("SEE ALL ›", "VER TODO ›"),
"album_link": ("Album ›", "Álbum ›"),
# setup
"tagline": ("Your Plex music, anywhere,\non a very small phone.", "A túa música de Plex, onde queiras,\nnun teléfono moi pequeno."),
"sign_in": ("Sign in with Plex", "Iniciar sesión con Plex"),
"connect_address": ("Connect to a server address", "Conectar a un enderezo de servidor"),
"connection_log": ("Connection log", "Rexistro de conexión"),
"pin_open": ("On a computer or a newer phone, open", "Nun ordenador ou nun teléfono máis novo, abre"),
"pin_enter": ("and enter this code:", "e escribe este código:"),
"pin_waiting": ("Waiting for you to link…", "Agardando a que vincules a conta…"),
"pin_check_now": ("I've entered it — check now", "Xa o escribín: comprobar agora"),
"cancel": ("Cancel", "Cancelar"),
"server_address": ("Server address", "Enderezo do servidor"),
"server_address_help": ("e.g. 192.168.1.20 or https://plex.example.com. Signing in with Plex is easier and finds remote addresses for you.",
"p. ex. 192.168.1.20 ou https://plex.exemplo.com. Iniciar sesión con Plex é máis doado e atopa por ti os enderezos remotos."),
"address_hint": ("Address", "Enderezo"),
"token_hint": ("X-Plex-Token (optional on your LAN)", "X-Plex-Token (opcional na rede da casa)"),
"connect": ("Connect", "Conectar"),
"back": ("Back", "Volver"),
"contacting_plextv": ("Contacting plex.tv…\n(the first secure connection takes a few seconds on this phone)",
"Contactando con plex.tv…\n(a primeira conexión segura tarda uns segundos neste teléfono)"),
"cant_reach_plextv": ("Couldn't reach plex.tv: %1$s", "Non se puido contactar con plex.tv: %1$s"),
"pin_expired": ("Code expired — press Cancel and sign in again", "O código caducou: preme Cancelar e inicia sesión de novo"),
"pin_not_yet": ("Not linked yet · checked %1$d× · %2$d min left", "Aínda sen vincular · comprobado %1$d× · quedan %2$d min"),
"pin_linked": ("Linked!", "Vinculado!"),
"pin_check_failed": ("Check %1$d failed: %2$s — retrying", "Fallou a comprobación %1$d: %2$s; tentando de novo"),
"signed_in_finding": ("Signed in. Finding your servers…", "Sesión iniciada. Buscando os teus servidores…"),
"finding_servers": ("Finding your servers…", "Buscando os teus servidores…"),
"no_servers": ("No Plex Media Servers on this account", "Esta conta non ten ningún Plex Media Server"),
"choose_server": ("Choose a server", "Escolle un servidor"),
"signed_in_as": ("Signed in as %1$s", "Sesión iniciada como %1$s"),
"you": ("you", "ti"),
"server_yours": ("Yours", "Teu"),
"server_shared": ("Shared with you", "Compartido contigo"),
"server_home_plus": ("home + ", "casa + "),
"server_remote": ("remote", "remoto"),
"server_local_only": ("local only", "só local"),
"cant_list_servers": ("Couldn't list servers: %1$s", "Non se puideron listar os servidores: %1$s"),
"connecting_to": ("Connecting to %1$s…\nTrying home network, then remote, then relay.",
"Conectando con %1$s…\nProbando a rede da casa, despois a remota e despois o relay."),
"enter_address": ("Enter your server's address", "Escribe o enderezo do teu servidor"),
"plex_server": ("Plex server", "Servidor Plex"),
"connecting": ("Connecting…", "Conectando…"),
"server_wants_token": ("The server wants a token (or sign in with Plex instead)", "O servidor pide un token (ou inicia sesión con Plex)"),
"loading_libraries": ("Loading libraries…", "Cargando bibliotecas…"),
"no_music_libraries": ("%1$s has no music libraries", "%1$s non ten bibliotecas de música"),
"choose_library": ("Choose a music library", "Escolle unha biblioteca de música"),
"music": ("Music", "Música"),
# common
"ok": ("OK", "Aceptar"),
"close": ("Close", "Pechar"),
"loading": ("Loading…", "Cargando…"),
"try_again": ("Try again", "Tentar de novo"),
"nothing_here": ("Nothing here", "Non hai nada aquí"),
"nothing_here_yet": ("Nothing here yet", "Aínda non hai nada"),
"cant_load_tap": ("Couldn't load: %1$s (tap to retry)", "Non se puido cargar: %1$s (toca para tentar de novo)"),
"on": ("On", "Activado"),
"off": ("Off", "Desactivado"),
"error_last_time": ("Minamp hit an error last time", "Minamp tivo un erro a última vez"),
"min": ("%1$d min", "%1$d min"),
"h_min": ("%1$d h %2$d min", "%1$d h %2$d min"),
# items / counts
"artist": ("Artist", "Artista"),
"album": ("Album", "Álbum"),
"playlist": ("Playlist", "Lista de reprodución"),
"n_albums_one": ("%1$d album", "%1$d álbum"),
"n_albums": ("%1$d albums", "%1$d álbums"),
"n_tracks_one": ("%1$d track", "%1$d pista"),
"n_tracks": ("%1$d tracks", "%1$d pistas"),
# actions
"play": ("Play", "Reproducir"),
"play_all": ("Play all", "Reproducir todo"),
"shuffle": ("Shuffle", "Aleatorio"),
"artist_radio": ("Artist radio", "Radio do artista"),
"artist_radio_shuffle": ("Artist radio (shuffle)", "Radio do artista (aleatorio)"),
"play_next": ("Play next", "Reproducir a continuación"),
"add_to_queue": ("Add to queue", "Engadir á cola"),
"go_to_album": ("Go to album", "Ir ao álbum"),
"go_to_artist": ("Go to artist", "Ir ao artista"),
"love": ("Add to favourites", "Engadir a favoritas"),
"unlove": ("Remove from favourites", "Quitar de favoritas"),
"loved": ("Added to favourites", "Engadida a favoritas"),
"unloved": ("Removed from favourites", "Quitada de favoritas"),
"cant_save_rating": ("Couldn't save rating: %1$s", "Non se puido gardar a valoración: %1$s"),
"nothing_to_play": ("Nothing to play", "Non hai nada que reproducir"),
"shuffling_x": ("Shuffling %1$s", "Reproducindo ao chou %1$s"),
"playing_x": ("Playing %1$s", "Reproducindo %1$s"),
"adding_x": ("Adding %1$s…", "Engadindo %1$s…"),
"no_tracks": ("No tracks", "Non hai pistas"),
"playing_next": ("Playing next", "Soará a continuación"),
"added_to_queue": ("Added to queue", "Engadido á cola"),
"starting_x": ("Starting %1$s…", "Iniciando %1$s…"),
"nothing_found_for": ("Nothing found for %1$s", "Non se atopou nada para %1$s"),
# menu / settings
"now_playing": ("Now playing", "En reprodución"),
"search": ("Search", "Buscar"),
"settings": ("Settings", "Axustes"),
"not_connected_yet": ("not connected yet", "aínda sen conectar"),
"secure": (" (secure)", " (cifrada)"),
"unencrypted": (" (unencrypted)", " (sen cifrar)"),
"set_quality_home": ("Quality at home: %1$s", "Calidade na casa: %1$s"),
"set_quality_away": ("Quality away / mobile data: %1$s", "Calidade fóra / datos móbiles: %1$s"),
"set_download_first": ("Download tracks before playing: %1$s", "Descargar as pistas antes de reproducilas: %1$s"),
"set_secure_home": ("Encrypt at home too (slower): %1$s", "Cifrar tamén na casa (máis lento): %1$s"),
"set_library": ("Library: %1$s", "Biblioteca: %1$s"),
"set_server": ("Server: %1$s", "Servidor: %1$s"),
"set_connection": ("Connection: %1$s", "Conexión: %1$s"),
"set_log": ("Connection log & streaming test", "Rexistro de conexión e proba de reprodución"),
"set_language": ("Language: %1$s", "Idioma: %1$s"),
"sign_out": ("Sign out", "Pechar sesión"),
"quality_home_title": ("Quality at home", "Calidade na casa"),
"quality_away_title": ("Quality away / mobile", "Calidade fóra / datos móbiles"),
"quality_original": ("Original", "Orixinal"),
"download_first_toast": ("Download before playing: %1$s", "Descargar antes de reproducir: %1$s"),
"https_home_on": ("Using HTTPS at home (each new connection is slower on this phone)",
"Usando HTTPS na casa (cada conexión nova é máis lenta neste teléfono)"),
"https_home_off": ("Using plain HTTP at home (faster)", "Usando HTTP simple na casa (máis rápido)"),
"reconnecting": ("Reconnecting…", "Reconectando…"),
"connected_x": ("Connected: %1$s", "Conectado: %1$s"),
"applies_next_track": ("Applies from the next track", "Aplícase desde a seguinte pista"),
"sign_out_confirm": ("Sign out and forget this server?", "Pechar sesión e esquecer este servidor?"),
"language_title": ("Language", "Idioma"),
# diagnostics
"log_empty": ("Nothing logged yet.", "Aínda non hai nada no rexistro."),
"save_to_sd": ("Save to SD card", "Gardar na tarxeta SD"),
"test_streaming": ("Test streaming", "Probar reprodución"),
"no_sd": ("No SD card available", "Non hai tarxeta SD dispoñible"),
"saved_log": ("Saved as Minamp-log.txt on the SD card", "Gardado como Minamp-log.txt na tarxeta SD"),
"cant_save": ("Couldn't save: %1$s", "Non se puido gardar: %1$s"),
"testing": ("Testing… (about 30 s)", "Probando… (uns 30 s)"),
# home
"library_radio": ("Library radio", "Radio da biblioteca"),
"library_radio_sub": ("Shuffle everything", "Todo ao chou"),
"loved_radio": ("Favourites radio", "Radio de favoritas"),
"loved_radio_sub": ("Your top rated", "As mellor valoradas"),
"time_travel": ("Time travel", "Viaxe no tempo"),
"time_travel_sub": ("Pick a decade", "Escolle unha década"),
"random_album": ("Random album", "Álbum ao chou"),
"random_album_sub": ("Feeling lucky", "Proba sorte"),
"recently_added": ("Recently added", "Engadidos recentemente"),
"recently_played": ("Recently played", "Escoitados recentemente"),
"finding_decades": ("Finding decades…", "Buscando décadas…"),
"no_years": ("No release years in this library", "Esta biblioteca non ten anos de publicación"),
"surprise_me": ("Surprise me", "Sorpréndeme"),
"decade_x": ("%1$ds", "Década de %1$d"),
"time_travel_title": ("Time travel radio", "Radio viaxe no tempo"),
"decade_radio_x": ("%1$ds radio", "Radio da década de %1$d"),
"no_albums": ("No albums", "Non hai álbums"),
"loading_artists": ("Loading artists…", "Cargando artistas…"),
"no_artists_library": ("No artists in this library", "Esta biblioteca non ten artistas"),
"loading_albums": ("Loading albums…", "Cargando álbums…"),
"no_albums_library": ("No albums in this library", "Esta biblioteca non ten álbums"),
"loading_playlists": ("Loading playlists…", "Cargando listas…"),
"no_playlists": ("No music playlists yet", "Aínda non hai listas de música"),
"sort_albums": ("Sort albums", "Ordenar álbums"),
"refresh": ("Refresh", "Actualizar"),
"sort_albums_by": ("Sort albums by", "Ordenar álbums por"),
"sort_title": ("Title", "Título"),
"sort_artist": ("Artist", "Artista"),
"sort_added": ("Recently added", "Engadidos recentemente"),
"sort_year": ("Year (newest)", "Ano (máis recentes)"),
"sort_played": ("Most played", "Máis escoitados"),
# now playing
"repeat_queue": ("Repeat queue", "Repetir a cola"),
"repeat_track": ("Repeat track", "Repetir a pista"),
"repeat_off": ("Repeat off", "Sen repetición"),
"nothing_playing": ("Nothing playing", "Non se está a reproducir nada"),
"pick_something": ("Pick something from your library", "Escolle algo da túa biblioteca"),
"end_of_track": ("End of track", "Fin da pista"),
"sleep": ("Sleep", "Temporizador"),
"sleep_timer": ("Sleep timer", "Temporizador de apagado"),
"sleep_off": ("Off", "Desactivado"),
"sleep_15": ("15 minutes", "15 minutos"),
"sleep_30": ("30 minutes", "30 minutos"),
"sleep_45": ("45 minutes", "45 minutos"),
"sleep_60": ("1 hour", "1 hora"),
"sleep_90": ("1½ hours", "1½ horas"),
"sleep_eot": ("End of this track", "Ao rematar esta pista"),
"sleep_paused": ("Sleep timer: paused", "Temporizador de apagado: en pausa"),
# playback service
"buffering": ("Buffering…", "Cargando…"),
"downloading_pct": ("Downloading %1$d%%", "Descargando %1$d%%"),
"stream_interrupted": ("Stream interrupted", "Interrompeuse a transmisión"),
"playback_error": ("Playback error (%1$d/%2$d)", "Erro de reprodución (%1$d/%2$d)"),
"paused_dot": ("Paused · ", "En pausa · "),
# queue
"up_next": ("Up next", "A continuación"),
"shuffle_on": ("Shuffle on", "Aleatorio activado"),
"shuffle_off": ("Shuffle off", "Aleatorio desactivado"),
"remove_from_queue": ("Remove from queue", "Quitar da cola"),
"queue_empty": ("The queue is empty", "A cola está baleira"),
"clear_upcoming": ("Clear upcoming tracks", "Borrar as pistas seguintes"),
"clear_everything": ("Clear everything", "Borralo todo"),
# search
"search_hint": ("Artists, albums, tracks", "Artistas, álbums, cancións"),
"search_in": ("Search %1$s", "Buscar en %1$s"),
"searching": ("Searching…", "Buscando…"),
"nothing_found_q": ("Nothing found for “%1$s”", "Non se atopou nada para “%1$s”"),
# pure-Java layers (looked up by name through com.minamp.util.Tr)
"tr_kind_home": ("Home network", "Rede da casa"),
"tr_kind_direct": ("Direct", "Directa"),
"tr_kind_remote": ("Remote", "Remota"),
"tr_kind_relay": ("Plex Relay", "Plex Relay"),
"tr_cant_reach": ("Can't reach %1$s", "Non se pode conectar con %1$s"),
"tr_server": ("server", "servidor"),
"tr_not_authorized": ("Not authorized (401) on %1$s", "Sen autorización (401) en %1$s"),
"tr_refused": ("Server refused the stream (%1$d)", "O servidor rexeitou a transmisión (%1$d)"),
"tr_file_503": ("Plex couldn't open this file (503). Is the drive or folder with your music available to the server?",
"Plex non puido abrir este ficheiro (503). Está dispoñible para o servidor o disco ou cartafol coa túa música?"),
"tr_cert_mismatch": ("Certificate does not match %1$s", "O certificado non corresponde a %1$s"),
"tr_cert_untrusted": ("Untrusted certificate chain for %1$s", "Cadea de certificados non fiable para %1$s"),
"tr_cert_dates": ("Certificate expired or not yet valid (check the phone's date)",
"Certificado caducado ou aínda non válido (comproba a data do teléfono)"),
"tr_no_media": ("Track has no playable media", "A pista non ten ningún ficheiro reproducible"),
"tr_artist": ("Artist", "Artista"),
"tr_n_tracks_one": ("%1$d track", "%1$d pista"),
"tr_n_tracks": ("%1$d tracks", "%1$d pistas"),
}
def esc(s):
s = s.replace("\\", "\\\\").replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;")
s = s.replace("'", "\\'").replace('"', '\\"').replace("\n", "\\n")
if s.startswith("@") or s.startswith("?"):
s = "\\" + s
return s
def write(path, idx):
os.makedirs(os.path.dirname(path), exist_ok=True)
with open(path, "w", encoding="utf-8") as f:
f.write('<?xml version="1.0" encoding="utf-8"?>\n<!-- generated by tools/strings.py: edit that file instead -->\n<resources>\n')
for k in sorted(S):
v = S[k][idx]
fmt = ' formatted="false"' if ("%" in v and "%1$" not in v and "%%" not in v) else ""
f.write(' <string name="%s"%s>%s</string>\n' % (k, fmt, esc(v)))
f.write("</resources>\n")
if __name__ == "__main__":
root = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "res")
write(os.path.join(root, "values", "strings.xml"), 1) # Galician: default
write(os.path.join(root, "values-en", "strings.xml"), 0) # English
print("%d strings" % len(S))