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