MiniABS 1.2: Galician interface, larger text and touch targets

- Translate the interface to Galician (tools/translate_gl.py), including
  Galician month abbreviations for dates, which Android 2.3 lacks
- Resize for the Galaxy Mini's 240x320 screen: larger text, rows, tabs and
  buttons; all sizes now live in res/values/dimens.xml
- Redraw play/pause/refresh icons for every density; slightly brighter
  secondary text colours

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
xockoandClaude Opus 5.5 committed 2026-10-14 21:29:17 +02:00
1 parent b7e43d7d61
commit 275bee3c11
51 files changed
+704 -372

No files matched your search

+3 -3
View File
@@ -1,8 +1,8 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.miniabs" package="com.miniabs"
android:versionCode="2" android:versionCode="3"
android:versionName="1.1" android:versionName="1.2"
android:installLocation="auto"> android:installLocation="auto">
<uses-sdk android:minSdkVersion="8" android:targetSdkVersion="10" /> <uses-sdk android:minSdkVersion="8" android:targetSdkVersion="10" />
@@ -54,7 +54,7 @@
<activity <activity
android:name=".SettingsActivity" android:name=".SettingsActivity"
android:label="Settings" android:label="Axustes"
android:theme="@android:style/Theme" android:theme="@android:style/Theme"
android:screenOrientation="portrait" android:screenOrientation="portrait"
android:configChanges="keyboardHidden|orientation" /> android:configChanges="keyboardHidden|orientation" />
+3 -1
View File
@@ -1,6 +1,6 @@
# MiniABS # MiniABS
A tiny Audiobookshelf **podcast** client for the Samsung Galaxy Mini (GT-S5570) and other Android 2.2+ phones. Native Java, no libraries, ~100 KB. A tiny Audiobookshelf **podcast** client for the Samsung Galaxy Mini (GT-S5570) and other Android 2.2+ phones. Native Java, no libraries, ~130 KB. The interface is in Galician, with large text and touch targets sized for the Mini's 240×320 screen.
## Install ## Install
@@ -91,6 +91,8 @@ Needs a JDK, `aapt`, `dalvik-exchange` (dx), `zipalign`, `apksigner` (Ubuntu: `a
Compiling against the API 8 jar guarantees no newer Android APIs are used. The APK is signed with `miniabs.keystore` (password `miniabs`); keep it to install updates over the top. Compiling against the API 8 jar guarantees no newer Android APIs are used. The APK is signed with `miniabs.keystore` (password `miniabs`); keep it to install updates over the top.
All sizes (text, rows, buttons, covers) are in `res/values/dimens.xml`; change them there and rebuild.
`tools/gen_roots.py` regenerates the bundled root certificates from a system CA store. `tools/gen_roots.py` regenerates the bundled root certificates from a system CA store.
`test/run_tests.sh` runs the API, JSON, progress, HTTPS-trust and streaming-proxy code on the desktop JVM against a mock Audiobookshelf server (`test/mock_abs.py`), over plain HTTP and over TLS with good, expired, wrong-host, forged and self-signed certificates (`test/make_pki.sh`). `test/run_tests.sh` runs the API, JSON, progress, HTTPS-trust and streaming-proxy code on the desktop JVM against a mock Audiobookshelf server (`test/mock_abs.py`), over plain HTTP and over TLS with good, expired, wrong-host, forged and self-signed certificates (`test/make_pki.sh`).
Binary file not shown.

Before

Width:  |  Height:  |  Size: 499 B

After

Width:  |  Height:  |  Size: 513 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 346 B

After

Width:  |  Height:  |  Size: 510 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 650 B

After

Width:  |  Height:  |  Size: 994 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 880 B

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 816 B

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 381 B

After

Width:  |  Height:  |  Size: 365 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 349 B

After

Width:  |  Height:  |  Size: 413 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 470 B

After

Width:  |  Height:  |  Size: 656 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 527 B

After

Width:  |  Height:  |  Size: 656 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 377 B

After

Width:  |  Height:  |  Size: 446 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 408 B

After

Width:  |  Height:  |  Size: 312 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 413 B

After

Width:  |  Height:  |  Size: 486 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 513 B

After

Width:  |  Height:  |  Size: 760 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 656 B

After

Width:  |  Height:  |  Size: 834 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 463 B

After

Width:  |  Height:  |  Size: 699 B

+2 -1
View File
@@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="oval"> <shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="oval">
<solid android:color="@color/accent" /> <solid android:color="@color/accent" />
<size android:width="14dp" android:height="14dp" /> <stroke android:width="2dp" android:color="@color/bg" />
<size android:width="@dimen/seek_thumb" android:height="@dimen/seek_thumb" />
</shape> </shape>
+8 -8
View File
@@ -10,7 +10,7 @@
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="MiniABS" android:text="MiniABS"
android:textSize="15sp" android:textSize="@dimen/header_text"
android:textStyle="bold" android:textStyle="bold"
android:textColor="@color/accent" /> android:textColor="@color/accent" />
@@ -20,8 +20,8 @@
android:layout_height="fill_parent" android:layout_height="fill_parent"
android:layout_weight="1" android:layout_weight="1"
android:gravity="center_vertical" android:gravity="center_vertical"
android:paddingLeft="8dp" android:paddingLeft="10dp"
android:textSize="11sp" android:textSize="@dimen/header_sub"
android:textColor="@color/sub" android:textColor="@color/sub"
android:singleLine="true" android:singleLine="true"
android:ellipsize="end" android:ellipsize="end"
@@ -29,7 +29,7 @@
<ImageButton <ImageButton
android:id="@+id/refresh" android:id="@+id/refresh"
android:layout_width="36dp" android:layout_width="@dimen/header_h"
android:layout_height="fill_parent" android:layout_height="fill_parent"
android:background="@drawable/row_sel" android:background="@drawable/row_sel"
android:src="@drawable/ic_refresh" /> android:src="@drawable/ic_refresh" />
@@ -37,12 +37,12 @@
<LinearLayout <LinearLayout
android:layout_width="fill_parent" android:layout_width="fill_parent"
android:layout_height="32dp" android:layout_height="@dimen/tab_h"
android:orientation="horizontal"> android:orientation="horizontal">
<TextView android:id="@+id/tab_latest" style="@style/Tab" android:text="Latest" /> <TextView android:id="@+id/tab_latest" style="@style/Tab" android:text="Últimos" />
<TextView android:id="@+id/tab_shows" style="@style/Tab" android:text="Shows" /> <TextView android:id="@+id/tab_shows" style="@style/Tab" android:text="Programas" />
<TextView android:id="@+id/tab_saved" style="@style/Tab" android:text="Saved" /> <TextView android:id="@+id/tab_saved" style="@style/Tab" android:text="Gardados" />
</LinearLayout> </LinearLayout>
<include layout="@layout/list_area" /> <include layout="@layout/list_area" />
+24 -21
View File
@@ -15,50 +15,50 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="horizontal" android:orientation="horizontal"
android:gravity="center_vertical" android:gravity="center_vertical"
android:layout_marginBottom="4dp"> android:layout_marginBottom="6dp">
<ImageView <ImageView
android:layout_width="36dp" android:layout_width="48dp"
android:layout_height="36dp" android:layout_height="48dp"
android:src="@drawable/ic_launcher" /> android:src="@drawable/ic_launcher" />
<LinearLayout <LinearLayout
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="vertical" android:orientation="vertical"
android:paddingLeft="8dp"> android:paddingLeft="10dp">
<TextView <TextView
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="MiniABS" android:text="MiniABS"
android:textSize="18sp" android:textSize="@dimen/login_title"
android:textStyle="bold" android:textStyle="bold"
android:textColor="@color/accent" /> android:textColor="@color/accent" />
<TextView <TextView
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="Audiobookshelf podcasts" android:text="Podcasts de Audiobookshelf"
android:textSize="11sp" android:textSize="@dimen/login_tip"
android:textColor="@color/sub" /> android:textColor="@color/sub" />
</LinearLayout> </LinearLayout>
</LinearLayout> </LinearLayout>
<TextView style="@style/Label" android:text="Server address" /> <TextView style="@style/Label" android:text="Enderezo do servidor" />
<EditText <EditText
android:id="@+id/server" android:id="@+id/server"
style="@style/Field" style="@style/Field"
android:hint="http://192.168.1.10:13378" android:hint="http://192.168.1.10:13378"
android:inputType="textUri" /> android:inputType="textUri" />
<TextView android:id="@+id/user_label" style="@style/Label" android:text="Username" /> <TextView android:id="@+id/user_label" style="@style/Label" android:text="Usuario" />
<EditText <EditText
android:id="@+id/user" android:id="@+id/user"
style="@style/Field" style="@style/Field"
android:inputType="text" /> android:inputType="text" />
<TextView android:id="@+id/pass_label" style="@style/Label" android:text="Password" /> <TextView android:id="@+id/pass_label" style="@style/Label" android:text="Contrasinal" />
<EditText <EditText
android:id="@+id/pass" android:id="@+id/pass"
style="@style/Field" style="@style/Field"
@@ -66,36 +66,39 @@
<CheckBox <CheckBox
android:id="@+id/use_key" android:id="@+id/use_key"
android:layout_width="wrap_content" android:layout_width="fill_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:textSize="12sp" android:layout_marginTop="4dp"
android:text="Use an API key instead" /> android:minHeight="48dp"
android:textSize="@dimen/login_check"
android:text="Usar unha chave da API" />
<Button <Button
android:id="@+id/connect" android:id="@+id/connect"
android:layout_width="fill_parent" android:layout_width="fill_parent"
android:layout_height="42dp" android:layout_height="@dimen/login_btn_h"
android:layout_marginTop="6dp" android:layout_marginTop="6dp"
android:background="@drawable/btn_accent" android:background="@drawable/btn_accent"
android:text="Connect" android:text="Conectar"
android:textStyle="bold" android:textStyle="bold"
android:textSize="14sp" android:textSize="@dimen/login_btn_text"
android:textColor="#121519" /> android:textColor="#121519" />
<TextView <TextView
android:id="@+id/status" android:id="@+id/status"
android:layout_width="fill_parent" android:layout_width="fill_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="8dp" android:layout_marginTop="10dp"
android:textSize="12sp" android:textSize="@dimen/login_status"
android:textColor="@color/accent" /> android:textColor="@color/accent" />
<TextView <TextView
android:layout_width="fill_parent" android:layout_width="fill_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="8dp" android:layout_marginTop="10dp"
android:textSize="10sp" android:textSize="@dimen/login_tip"
android:textColor="@color/dim" android:textColor="@color/dim"
android:text="At home, use your server's local http:// address. For https://, Android 2.3 needs the server to offer TLS 1.0 on a separate port (see README). If the certificate isn't recognised, you'll be shown its fingerprint to check." /> android:lineSpacingExtra="2dp"
android:text="Na casa, usa o enderezo local http:// do servidor. Para https://, Android 2.3 precisa que o servidor ofreza TLS 1.0 nun porto á parte (ve o README). Se o certificado non se recoñece, amosaráseche a súa pegada dixital para que a comprobes." />
</LinearLayout> </LinearLayout>
</ScrollView> </ScrollView>
+37 -33
View File
@@ -11,7 +11,9 @@
android:layout_height="fill_parent" android:layout_height="fill_parent"
android:layout_weight="1" android:layout_weight="1"
android:gravity="center_vertical" android:gravity="center_vertical"
android:textSize="12sp" android:paddingRight="10dp"
android:textSize="@dimen/header_sub"
android:textStyle="bold"
android:textColor="@color/accent" android:textColor="@color/accent"
android:singleLine="true" android:singleLine="true"
android:ellipsize="end" android:ellipsize="end"
@@ -27,9 +29,9 @@
<ImageView <ImageView
android:id="@+id/cover" android:id="@+id/cover"
android:layout_width="84dp" android:layout_width="@dimen/cover_player"
android:layout_height="84dp" android:layout_height="@dimen/cover_player"
android:layout_marginRight="8dp" android:layout_marginRight="10dp"
android:scaleType="centerCrop" android:scaleType="centerCrop"
android:background="@color/surface" /> android:background="@color/surface" />
@@ -43,7 +45,7 @@
android:id="@+id/title" android:id="@+id/title"
android:layout_width="fill_parent" android:layout_width="fill_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:textSize="14sp" android:textSize="@dimen/player_title"
android:textStyle="bold" android:textStyle="bold"
android:textColor="@color/text" android:textColor="@color/text"
android:maxLines="3" android:maxLines="3"
@@ -53,8 +55,8 @@
android:id="@+id/date" android:id="@+id/date"
android:layout_width="fill_parent" android:layout_width="fill_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="2dp" android:layout_marginTop="3dp"
android:textSize="10sp" android:textSize="@dimen/player_meta"
android:textColor="@color/sub" android:textColor="@color/sub"
android:singleLine="true" /> android:singleLine="true" />
@@ -63,9 +65,10 @@
android:layout_width="fill_parent" android:layout_width="fill_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="2dp" android:layout_marginTop="2dp"
android:textSize="10sp" android:textSize="@dimen/player_meta"
android:textColor="@color/fresh" android:textColor="@color/fresh"
android:maxLines="2" /> android:maxLines="2"
android:ellipsize="end" />
</LinearLayout> </LinearLayout>
</LinearLayout> </LinearLayout>
@@ -73,28 +76,29 @@
android:id="@+id/seek" android:id="@+id/seek"
android:layout_width="fill_parent" android:layout_width="fill_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:paddingLeft="14dp" android:paddingLeft="18dp"
android:paddingRight="14dp" android:paddingRight="18dp"
android:minHeight="4dp" android:paddingTop="6dp"
android:maxHeight="4dp" android:paddingBottom="6dp"
android:minHeight="@dimen/seek_track"
android:maxHeight="@dimen/seek_track"
android:progressDrawable="@drawable/seek_track" android:progressDrawable="@drawable/seek_track"
android:thumb="@drawable/seek_thumb" android:thumb="@drawable/seek_thumb"
android:thumbOffset="7dp" android:thumbOffset="13dp"
android:max="1000" /> android:max="1000" />
<RelativeLayout <RelativeLayout
android:layout_width="fill_parent" android:layout_width="fill_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:paddingLeft="10dp" android:paddingLeft="12dp"
android:paddingRight="10dp" android:paddingRight="12dp">
android:paddingTop="2dp">
<TextView <TextView
android:id="@+id/pos" android:id="@+id/pos"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_alignParentLeft="true" android:layout_alignParentLeft="true"
android:textSize="11sp" android:textSize="@dimen/player_time"
android:textColor="@color/sub" /> android:textColor="@color/sub" />
<TextView <TextView
@@ -102,7 +106,7 @@
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_alignParentRight="true" android:layout_alignParentRight="true"
android:textSize="11sp" android:textSize="@dimen/player_time"
android:textColor="@color/sub" /> android:textColor="@color/sub" />
</RelativeLayout> </RelativeLayout>
@@ -112,33 +116,33 @@
android:orientation="horizontal" android:orientation="horizontal"
android:gravity="center" android:gravity="center"
android:paddingTop="4dp" android:paddingTop="4dp"
android:paddingBottom="6dp"> android:paddingBottom="8dp">
<Button <Button
android:id="@+id/back" android:id="@+id/back"
android:layout_width="58dp" android:layout_width="@dimen/skip_w"
android:layout_height="38dp" android:layout_height="@dimen/skip_h"
android:background="@drawable/btn_flat" android:background="@drawable/btn_flat"
android:textColor="@color/text" android:textColor="@color/text"
android:textSize="13sp" android:textSize="@dimen/skip_text"
android:textStyle="bold" /> android:textStyle="bold" />
<ImageButton <ImageButton
android:id="@+id/play" android:id="@+id/play"
android:layout_width="56dp" android:layout_width="@dimen/play_size"
android:layout_height="56dp" android:layout_height="@dimen/play_size"
android:layout_marginLeft="16dp" android:layout_marginLeft="14dp"
android:layout_marginRight="16dp" android:layout_marginRight="14dp"
android:background="@drawable/btn_round" android:background="@drawable/btn_round"
android:src="@drawable/ic_play" /> android:src="@drawable/ic_play" />
<Button <Button
android:id="@+id/fwd" android:id="@+id/fwd"
android:layout_width="58dp" android:layout_width="@dimen/skip_w"
android:layout_height="38dp" android:layout_height="@dimen/skip_h"
android:background="@drawable/btn_flat" android:background="@drawable/btn_flat"
android:textColor="@color/text" android:textColor="@color/text"
android:textSize="13sp" android:textSize="@dimen/skip_text"
android:textStyle="bold" /> android:textStyle="bold" />
</LinearLayout> </LinearLayout>
@@ -154,9 +158,9 @@
android:id="@+id/desc" android:id="@+id/desc"
android:layout_width="fill_parent" android:layout_width="fill_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:padding="8dp" android:padding="10dp"
android:textSize="12sp" android:textSize="@dimen/player_desc"
android:textColor="@color/sub" android:textColor="@color/sub"
android:lineSpacingExtra="2dp" /> android:lineSpacingExtra="3dp" />
</ScrollView> </ScrollView>
</LinearLayout> </LinearLayout>
+7 -11
View File
@@ -9,14 +9,14 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="horizontal" android:orientation="horizontal"
android:background="@color/header" android:background="@color/header"
android:padding="6dp" android:padding="8dp"
android:gravity="center_vertical"> android:gravity="center_vertical">
<ImageView <ImageView
android:id="@+id/cover" android:id="@+id/cover"
android:layout_width="44dp" android:layout_width="@dimen/cover_podcast"
android:layout_height="44dp" android:layout_height="@dimen/cover_podcast"
android:layout_marginRight="8dp" android:layout_marginRight="10dp"
android:scaleType="centerCrop" android:scaleType="centerCrop"
android:background="@color/surface" /> android:background="@color/surface" />
@@ -30,7 +30,7 @@
android:id="@+id/title" android:id="@+id/title"
android:layout_width="fill_parent" android:layout_width="fill_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:textSize="14sp" android:textSize="@dimen/podcast_title"
android:textStyle="bold" android:textStyle="bold"
android:textColor="@color/text" android:textColor="@color/text"
android:maxLines="2" android:maxLines="2"
@@ -38,12 +38,8 @@
<TextView <TextView
android:id="@+id/subtitle" android:id="@+id/subtitle"
android:layout_width="fill_parent" style="@style/RowMeta"
android:layout_height="wrap_content" android:layout_marginTop="2dp" />
android:textSize="10sp"
android:textColor="@color/sub"
android:singleLine="true"
android:ellipsize="end" />
</LinearLayout> </LinearLayout>
</LinearLayout> </LinearLayout>
+3 -2
View File
@@ -13,13 +13,14 @@
android:layout_gravity="center" android:layout_gravity="center"
android:gravity="center" android:gravity="center"
android:padding="20dp" android:padding="20dp"
android:textSize="13sp" android:textSize="@dimen/text_empty"
android:textColor="@color/sub" android:textColor="@color/sub"
android:lineSpacingExtra="2dp"
android:visibility="gone" /> android:visibility="gone" />
<ProgressBar <ProgressBar
android:id="@+id/loading" android:id="@+id/loading"
style="?android:attr/progressBarStyleSmall" style="?android:attr/progressBarStyle"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="center" android:layout_gravity="center"
+8 -7
View File
@@ -11,13 +11,13 @@
android:id="@+id/mini_progress" android:id="@+id/mini_progress"
style="?android:attr/progressBarStyleHorizontal" style="?android:attr/progressBarStyleHorizontal"
android:layout_width="fill_parent" android:layout_width="fill_parent"
android:layout_height="2dp" android:layout_height="@dimen/mini_progress"
android:progressDrawable="@drawable/thin_progress" android:progressDrawable="@drawable/thin_progress"
android:max="1000" /> android:max="1000" />
<LinearLayout <LinearLayout
android:layout_width="fill_parent" android:layout_width="fill_parent"
android:layout_height="44dp" android:layout_height="@dimen/mini_h"
android:orientation="horizontal" android:orientation="horizontal"
android:gravity="center_vertical"> android:gravity="center_vertical">
@@ -28,14 +28,14 @@
android:layout_weight="1" android:layout_weight="1"
android:orientation="vertical" android:orientation="vertical"
android:gravity="center_vertical" android:gravity="center_vertical"
android:paddingLeft="8dp" android:paddingLeft="10dp"
android:background="@drawable/row_sel"> android:background="@drawable/row_sel">
<TextView <TextView
android:id="@+id/mini_title" android:id="@+id/mini_title"
android:layout_width="fill_parent" android:layout_width="fill_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:textSize="13sp" android:textSize="@dimen/mini_title"
android:textColor="@color/text" android:textColor="@color/text"
android:singleLine="true" android:singleLine="true"
android:ellipsize="end" /> android:ellipsize="end" />
@@ -44,7 +44,8 @@
android:id="@+id/mini_sub" android:id="@+id/mini_sub"
android:layout_width="fill_parent" android:layout_width="fill_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:textSize="10sp" android:layout_marginTop="1dp"
android:textSize="@dimen/mini_sub"
android:textColor="@color/sub" android:textColor="@color/sub"
android:singleLine="true" android:singleLine="true"
android:ellipsize="end" /> android:ellipsize="end" />
@@ -52,8 +53,8 @@
<ImageButton <ImageButton
android:id="@+id/mini_play" android:id="@+id/mini_play"
android:layout_width="44dp" android:layout_width="@dimen/mini_btn"
android:layout_height="44dp" android:layout_height="@dimen/mini_btn"
android:background="@drawable/row_sel" android:background="@drawable/row_sel"
android:src="@drawable/ic_play_small" /> android:src="@drawable/ic_play_small" />
</LinearLayout> </LinearLayout>
+15 -29
View File
@@ -3,18 +3,18 @@
android:layout_width="fill_parent" android:layout_width="fill_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="horizontal" android:orientation="horizontal"
android:minHeight="48dp" android:minHeight="@dimen/row_min"
android:paddingLeft="6dp" android:paddingLeft="@dimen/row_pad"
android:paddingRight="6dp" android:paddingRight="@dimen/row_pad"
android:paddingTop="5dp" android:paddingTop="7dp"
android:paddingBottom="5dp"> android:paddingBottom="7dp">
<View <View
android:id="@+id/dot" android:id="@+id/dot"
android:layout_width="7dp" android:layout_width="@dimen/dot"
android:layout_height="7dp" android:layout_height="@dimen/dot"
android:layout_marginTop="5dp" android:layout_marginTop="6dp"
android:layout_marginRight="6dp" /> android:layout_marginRight="8dp" />
<LinearLayout <LinearLayout
android:layout_width="0dp" android:layout_width="0dp"
@@ -26,37 +26,23 @@
android:id="@+id/show" android:id="@+id/show"
android:layout_width="fill_parent" android:layout_width="fill_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:textSize="10sp" android:textSize="@dimen/text_show"
android:textColor="@color/accent" android:textColor="@color/accent"
android:singleLine="true" android:singleLine="true"
android:ellipsize="end" /> android:ellipsize="end" />
<TextView <TextView android:id="@+id/title" style="@style/RowTitle" />
android:id="@+id/title"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:textSize="13sp"
android:textColor="@color/text"
android:maxLines="2"
android:ellipsize="end" />
<ProgressBar <ProgressBar
android:id="@+id/progress" android:id="@+id/progress"
style="?android:attr/progressBarStyleHorizontal" style="?android:attr/progressBarStyleHorizontal"
android:layout_width="fill_parent" android:layout_width="fill_parent"
android:layout_height="2dp" android:layout_height="@dimen/row_progress"
android:layout_marginTop="3dp" android:layout_marginTop="4dp"
android:layout_marginBottom="1dp" android:layout_marginBottom="2dp"
android:progressDrawable="@drawable/thin_progress" android:progressDrawable="@drawable/thin_progress"
android:max="1000" /> android:max="1000" />
<TextView <TextView android:id="@+id/meta" style="@style/RowMeta" android:layout_marginTop="1dp" />
android:id="@+id/meta"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:textSize="10sp"
android:textColor="@color/sub"
android:singleLine="true"
android:ellipsize="end" />
</LinearLayout> </LinearLayout>
</LinearLayout> </LinearLayout>
+5 -4
View File
@@ -4,9 +4,10 @@
android:layout_width="fill_parent" android:layout_width="fill_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:background="@color/surface" android:background="@color/surface"
android:paddingLeft="8dp" android:paddingLeft="10dp"
android:paddingTop="4dp" android:paddingRight="10dp"
android:paddingBottom="4dp" android:paddingTop="6dp"
android:textSize="10sp" android:paddingBottom="6dp"
android:textSize="@dimen/text_section"
android:textStyle="bold" android:textStyle="bold"
android:textColor="@color/sub" /> android:textColor="@color/sub" />
+3 -2
View File
@@ -2,7 +2,8 @@
<TextView xmlns:android="http://schemas.android.com/apk/res/android" <TextView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/title" android:id="@+id/title"
android:layout_width="fill_parent" android:layout_width="fill_parent"
android:layout_height="44dp" android:layout_height="@dimen/row_min"
android:gravity="center" android:gravity="center"
android:textSize="13sp" android:textSize="@dimen/text_more"
android:textStyle="bold"
android:textColor="@color/accent" /> android:textColor="@color/accent" />
+7 -21
View File
@@ -3,15 +3,15 @@
android:layout_width="fill_parent" android:layout_width="fill_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="horizontal" android:orientation="horizontal"
android:minHeight="48dp" android:minHeight="@dimen/row_min"
android:padding="5dp" android:padding="7dp"
android:gravity="center_vertical"> android:gravity="center_vertical">
<ImageView <ImageView
android:id="@+id/cover" android:id="@+id/cover"
android:layout_width="40dp" android:layout_width="@dimen/cover_row"
android:layout_height="40dp" android:layout_height="@dimen/cover_row"
android:layout_marginRight="8dp" android:layout_marginRight="10dp"
android:scaleType="centerCrop" android:scaleType="centerCrop"
android:background="@color/surface" /> android:background="@color/surface" />
@@ -21,22 +21,8 @@
android:layout_weight="1" android:layout_weight="1"
android:orientation="vertical"> android:orientation="vertical">
<TextView <TextView android:id="@+id/title" style="@style/RowTitle" />
android:id="@+id/title"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:textSize="13sp"
android:textColor="@color/text"
android:maxLines="2"
android:ellipsize="end" />
<TextView <TextView android:id="@+id/meta" style="@style/RowMeta" android:layout_marginTop="2dp" />
android:id="@+id/meta"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:textSize="10sp"
android:textColor="@color/sub"
android:singleLine="true"
android:ellipsize="end" />
</LinearLayout> </LinearLayout>
</LinearLayout> </LinearLayout>
+6 -6
View File
@@ -1,12 +1,12 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<string-array name="skip_labels"> <string-array name="skip_labels">
<item>5 seconds</item> <item>5 segundos</item>
<item>10 seconds</item> <item>10 segundos</item>
<item>15 seconds</item> <item>15 segundos</item>
<item>30 seconds</item> <item>30 segundos</item>
<item>45 seconds</item> <item>45 segundos</item>
<item>60 seconds</item> <item>60 segundos</item>
</string-array> </string-array>
<string-array name="skip_values"> <string-array name="skip_values">
<item>5</item> <item>5</item>
+61
View File
@@ -0,0 +1,61 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Every size in MiniABS lives here. Tuned for the Galaxy Mini (240x320, 120 dpi), where
1dp = 0.75 px: 16sp text is 12 px tall, and 48dp touch targets are 36 px (about 7 mm).
-->
<resources>
<!-- top bar and tabs -->
<dimen name="header_h">46dp</dimen>
<dimen name="header_text">19sp</dimen>
<dimen name="header_sub">14sp</dimen>
<dimen name="tab_h">46dp</dimen>
<dimen name="tab_text">16sp</dimen>
<!-- lists -->
<dimen name="row_min">66dp</dimen>
<dimen name="row_pad">9dp</dimen>
<dimen name="text_title">16sp</dimen>
<dimen name="text_meta">13sp</dimen>
<dimen name="text_show">13sp</dimen>
<dimen name="text_section">13sp</dimen>
<dimen name="text_more">17sp</dimen>
<dimen name="text_empty">16sp</dimen>
<dimen name="dot">11dp</dimen>
<dimen name="row_progress">4dp</dimen>
<dimen name="cover_row">54dp</dimen>
<!-- show header -->
<dimen name="cover_podcast">62dp</dimen>
<dimen name="podcast_title">17sp</dimen>
<!-- mini player bar -->
<dimen name="mini_h">60dp</dimen>
<dimen name="mini_title">15sp</dimen>
<dimen name="mini_sub">13sp</dimen>
<dimen name="mini_btn">60dp</dimen>
<dimen name="mini_progress">3dp</dimen>
<!-- player -->
<dimen name="cover_player">100dp</dimen>
<dimen name="player_title">17sp</dimen>
<dimen name="player_meta">13sp</dimen>
<dimen name="player_time">15sp</dimen>
<dimen name="player_desc">15sp</dimen>
<dimen name="skip_w">82dp</dimen>
<dimen name="skip_h">56dp</dimen>
<dimen name="skip_text">20sp</dimen>
<dimen name="play_size">78dp</dimen>
<dimen name="seek_thumb">26dp</dimen>
<dimen name="seek_track">7dp</dimen>
<!-- login -->
<dimen name="login_title">24sp</dimen>
<dimen name="login_label">15sp</dimen>
<dimen name="login_field">18sp</dimen>
<dimen name="login_field_h">52dp</dimen>
<dimen name="login_check">16sp</dimen>
<dimen name="login_btn_h">56dp</dimen>
<dimen name="login_btn_text">20sp</dimen>
<dimen name="login_status">15sp</dimen>
<dimen name="login_tip">13sp</dimen>
</resources>
+30 -9
View File
@@ -7,8 +7,8 @@
<color name="header">#0B0D10</color> <color name="header">#0B0D10</color>
<color name="line">#2A3038</color> <color name="line">#2A3038</color>
<color name="text">#ECEFF2</color> <color name="text">#ECEFF2</color>
<color name="sub">#9AA4AE</color> <color name="sub">#A9B2BC</color>
<color name="dim">#5C6670</color> <color name="dim">#6E7882</color>
<color name="accent">#F5A623</color> <color name="accent">#F5A623</color>
<color name="fresh">#6EC1E4</color> <color name="fresh">#6EC1E4</color>
<color name="pressed">#2C333C</color> <color name="pressed">#2C333C</color>
@@ -22,11 +22,11 @@
<style name="Header"> <style name="Header">
<item name="android:layout_width">fill_parent</item> <item name="android:layout_width">fill_parent</item>
<item name="android:layout_height">32dp</item> <item name="android:layout_height">@dimen/header_h</item>
<item name="android:background">@color/header</item> <item name="android:background">@color/header</item>
<item name="android:gravity">center_vertical</item> <item name="android:gravity">center_vertical</item>
<item name="android:paddingLeft">8dp</item> <item name="android:paddingLeft">10dp</item>
<item name="android:paddingRight">2dp</item> <item name="android:paddingRight">0dp</item>
</style> </style>
<style name="Tab"> <style name="Tab">
@@ -34,9 +34,10 @@
<item name="android:layout_height">fill_parent</item> <item name="android:layout_height">fill_parent</item>
<item name="android:layout_weight">1</item> <item name="android:layout_weight">1</item>
<item name="android:gravity">center</item> <item name="android:gravity">center</item>
<item name="android:textSize">13sp</item> <item name="android:textSize">@dimen/tab_text</item>
<item name="android:textStyle">bold</item> <item name="android:textStyle">bold</item>
<item name="android:textColor">@color/sub</item> <item name="android:textColor">@color/sub</item>
<item name="android:singleLine">true</item>
<item name="android:background">@drawable/tab_bg</item> <item name="android:background">@drawable/tab_bg</item>
</style> </style>
@@ -53,7 +54,8 @@
<style name="Field"> <style name="Field">
<item name="android:layout_width">fill_parent</item> <item name="android:layout_width">fill_parent</item>
<item name="android:layout_height">wrap_content</item> <item name="android:layout_height">wrap_content</item>
<item name="android:textSize">14sp</item> <item name="android:minHeight">@dimen/login_field_h</item>
<item name="android:textSize">@dimen/login_field</item>
<item name="android:singleLine">true</item> <item name="android:singleLine">true</item>
<item name="android:textColor">#000000</item> <item name="android:textColor">#000000</item>
</style> </style>
@@ -61,8 +63,27 @@
<style name="Label"> <style name="Label">
<item name="android:layout_width">wrap_content</item> <item name="android:layout_width">wrap_content</item>
<item name="android:layout_height">wrap_content</item> <item name="android:layout_height">wrap_content</item>
<item name="android:textSize">11sp</item> <item name="android:textSize">@dimen/login_label</item>
<item name="android:textColor">@color/sub</item> <item name="android:textColor">@color/sub</item>
<item name="android:layout_marginTop">6dp</item> <item name="android:layout_marginTop">8dp</item>
<item name="android:layout_marginBottom">2dp</item>
</style>
<style name="RowTitle">
<item name="android:layout_width">fill_parent</item>
<item name="android:layout_height">wrap_content</item>
<item name="android:textSize">@dimen/text_title</item>
<item name="android:textColor">@color/text</item>
<item name="android:maxLines">2</item>
<item name="android:ellipsize">end</item>
</style>
<style name="RowMeta">
<item name="android:layout_width">fill_parent</item>
<item name="android:layout_height">wrap_content</item>
<item name="android:textSize">@dimen/text_meta</item>
<item name="android:textColor">@color/sub</item>
<item name="android:singleLine">true</item>
<item name="android:ellipsize">end</item>
</style> </style>
</resources> </resources>
+17 -17
View File
@@ -1,56 +1,56 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"> <PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
<PreferenceCategory android:title="Playback"> <PreferenceCategory android:title="Reprodución">
<ListPreference <ListPreference
android:key="skip_back" android:key="skip_back"
android:title="Skip back" android:title="Retroceder"
android:entries="@array/skip_labels" android:entries="@array/skip_labels"
android:entryValues="@array/skip_values" android:entryValues="@array/skip_values"
android:defaultValue="15" /> android:defaultValue="15" />
<ListPreference <ListPreference
android:key="skip_fwd" android:key="skip_fwd"
android:title="Skip forward" android:title="Avanzar"
android:entries="@array/skip_labels" android:entries="@array/skip_labels"
android:entryValues="@array/skip_values" android:entryValues="@array/skip_values"
android:defaultValue="30" /> android:defaultValue="30" />
</PreferenceCategory> </PreferenceCategory>
<PreferenceCategory android:title="Downloads"> <PreferenceCategory android:title="Descargas">
<CheckBoxPreference <CheckBoxPreference
android:key="wifi_only" android:key="wifi_only"
android:title="Download on Wi-Fi only" android:title="Descargar só con wifi"
android:summary="Saves your mobile data" android:summary="Aforra datos móbiles"
android:defaultValue="true" /> android:defaultValue="true" />
<Preference <Preference
android:key="delete_all" android:key="delete_all"
android:title="Delete all downloads" /> android:title="Eliminar todas as descargas" />
</PreferenceCategory> </PreferenceCategory>
<PreferenceCategory android:title="Display"> <PreferenceCategory android:title="Aparencia">
<CheckBoxPreference <CheckBoxPreference
android:key="covers" android:key="covers"
android:title="Show cover art" android:title="Amosar portadas"
android:summary="Turn off to save data and memory" android:summary="Desactívao para aforrar datos e memoria"
android:defaultValue="true" /> android:defaultValue="true" />
</PreferenceCategory> </PreferenceCategory>
<PreferenceCategory android:title="Account"> <PreferenceCategory android:title="Conta">
<Preference <Preference
android:key="account" android:key="account"
android:title="Signed in" /> android:title="Sesión iniciada" />
<Preference <Preference
android:key="pins" android:key="pins"
android:title="Certificates you trusted" /> android:title="Certificados nos que confías" />
<Preference <Preference
android:key="logout" android:key="logout"
android:title="Log out" /> android:title="Pechar sesión" />
</PreferenceCategory> </PreferenceCategory>
<PreferenceCategory android:title="About"> <PreferenceCategory android:title="Acerca de">
<Preference <Preference
android:key="about" android:key="about"
android:title="MiniABS 1.1" android:title="MiniABS 1.2"
android:summary="A tiny Audiobookshelf podcast client for Android 2.2+" /> android:summary="Un pequeno cliente de podcasts de Audiobookshelf para Android 2.2+" />
</PreferenceCategory> </PreferenceCategory>
</PreferenceScreen> </PreferenceScreen>
+20 -20
View File
@@ -26,7 +26,7 @@ import android.os.Build;
/** Audiobookshelf REST client (API as of server 2.x, including the 2.26+ token system). */ /** Audiobookshelf REST client (API as of server 2.x, including the 2.26+ token system). */
final class Api { final class Api {
static final String VERSION = "1.1"; static final String VERSION = "1.2";
static class ApiException extends IOException { static class ApiException extends IOException {
final int code; final int code;
@@ -100,7 +100,7 @@ final class Api {
if (peer.length == 0 || !(peer[0] instanceof java.security.cert.X509Certificate) if (peer.length == 0 || !(peer[0] instanceof java.security.cert.X509Certificate)
|| !Trust.hostMatches((java.security.cert.X509Certificate) peer[0], u.getHost())) { || !Trust.hostMatches((java.security.cert.X509Certificate) peer[0], u.getHost())) {
ss.close(); ss.close();
throw new SSLException("Certificate doesn't match " + u.getHost()); throw new SSLException("O certificado non corresponde a " + u.getHost());
} }
io = ss; io = ss;
} }
@@ -123,11 +123,11 @@ final class Api {
int ch; int ch;
while ((ch = in.read()) != -1 && ch != '\n' && line.length() < 200) line.append((char) ch); while ((ch = in.read()) != -1 && ch != '\n' && line.length() < 200) line.append((char) ch);
String[] parts = line.toString().trim().split(" "); String[] parts = line.toString().trim().split(" ");
if (parts.length < 2 || !parts[0].startsWith("HTTP/")) throw new IOException("Unexpected reply from server"); if (parts.length < 2 || !parts[0].startsWith("HTTP/")) throw new IOException("Resposta inesperada do servidor");
try { try {
code = Integer.parseInt(parts[1]); code = Integer.parseInt(parts[1]);
} catch (NumberFormatException e) { } catch (NumberFormatException e) {
throw new IOException("Unexpected reply from server"); throw new IOException("Resposta inesperada do servidor");
} }
byte[] rest = new byte[1024]; byte[] rest = new byte[1024];
int total = 0, n; int total = 0, n;
@@ -144,10 +144,10 @@ final class Api {
refreshIf(c, used, 0); refreshIf(c, used, 0);
return rawPatch(c, path, body, false); return rawPatch(c, path, body, false);
} }
throw new AuthException("Your sign-in has expired. Please log in again."); throw new AuthException("A túa sesión caducou. Inicia sesión de novo.");
} }
if (code >= 400) throw new ApiException(code, ""); if (code >= 400) throw new ApiException(code, "");
if (code >= 300) throw new ApiException(code, "The server redirected; check the address in Settings."); if (code >= 300) throw new ApiException(code, "O servidor redirixiu a petición; comproba o enderezo do servidor.");
return "OK"; return "OK";
} }
@@ -216,7 +216,7 @@ final class Api {
static Object call(Context c, String method, String path, JSONObject body, int maxString, String[] deepSkip, static Object call(Context c, String method, String path, JSONObject body, int maxString, String[] deepSkip,
boolean mayRefresh) throws IOException { boolean mayRefresh) throws IOException {
Prefs p = Prefs.get(c); Prefs p = Prefs.get(c);
if (!p.loggedIn()) throw new AuthException("Please log in."); if (!p.loggedIn()) throw new AuthException("Inicia sesión.");
if ("PATCH".equals(method)) return rawPatch(c, path, body, mayRefresh); if ("PATCH".equals(method)) return rawPatch(c, path, body, mayRefresh);
if (mayRefresh) ensureFresh(c, 120); if (mayRefresh) ensureFresh(c, 120);
String used = p.token(); String used = p.token();
@@ -232,7 +232,7 @@ final class Api {
refreshIf(c, used, 0); refreshIf(c, used, 0);
return call(c, method, path, body, maxString, deepSkip, false); return call(c, method, path, body, maxString, deepSkip, false);
} }
throw new AuthException("Your sign-in has expired. Please log in again."); throw new AuthException("A túa sesión caducou. Inicia sesión de novo.");
} }
if (code >= 400) throw new ApiException(code, errorText(conn)); if (code >= 400) throw new ApiException(code, errorText(conn));
return readBody(conn, maxString, deepSkip); return readBody(conn, maxString, deepSkip);
@@ -243,7 +243,7 @@ final class Api {
static JSONObject obj(Object o) throws IOException { static JSONObject obj(Object o) throws IOException {
if (o instanceof JSONObject) return (JSONObject) o; if (o instanceof JSONObject) return (JSONObject) o;
throw new IOException("Unexpected reply from server"); throw new IOException("Resposta inesperada do servidor");
} }
// ------------------------------------------------------------------ sign-in // ------------------------------------------------------------------ sign-in
@@ -253,13 +253,13 @@ final class Api {
HttpURLConnection conn = open(c, server + "/status", "GET"); HttpURLConnection conn = open(c, server + "/status", "GET");
try { try {
int code = responseCode(conn); int code = responseCode(conn);
if (code == 404) throw new IOException("No Audiobookshelf server at this address."); if (code == 404) throw new IOException("Non hai ningún servidor de Audiobookshelf neste enderezo.");
if (code >= 400) throw new ApiException(code, errorText(conn)); if (code >= 400) throw new ApiException(code, errorText(conn));
Object o = readBody(conn, 200, null); Object o = readBody(conn, 200, null);
if (!(o instanceof JSONObject)) throw new IOException("No Audiobookshelf server at this address."); if (!(o instanceof JSONObject)) throw new IOException("Non hai ningún servidor de Audiobookshelf neste enderezo.");
JSONObject s = (JSONObject) o; JSONObject s = (JSONObject) o;
if (s.has("isInit") && !s.optBoolean("isInit", true)) { if (s.has("isInit") && !s.optBoolean("isInit", true)) {
throw new IOException("This server hasn't been set up yet. Finish setup in a browser first."); throw new IOException("Este servidor aínda non está configurado. Remata a configuración nun navegador.");
} }
return Util.str(s, "serverVersion"); return Util.str(s, "serverVersion");
} finally { } finally {
@@ -280,19 +280,19 @@ final class Api {
} }
writeBody(conn, body); writeBody(conn, body);
int code = responseCode(conn); int code = responseCode(conn);
if (code == 401) throw new ApiException(401, "Wrong username or password."); if (code == 401) throw new ApiException(401, "Usuario ou contrasinal incorrectos.");
if (code == 429) throw new ApiException(429, "Too many attempts. Wait a minute and try again."); if (code == 429) throw new ApiException(429, "Demasiados intentos. Agarda un minuto e téntao de novo.");
if (code >= 400) throw new ApiException(code, errorText(conn)); if (code >= 400) throw new ApiException(code, errorText(conn));
JSONObject o = obj(readBody(conn, 500, null)); JSONObject o = obj(readBody(conn, 500, null));
JSONObject u = o.optJSONObject("user"); JSONObject u = o.optJSONObject("user");
if (u == null) throw new IOException("Unexpected reply from server"); if (u == null) throw new IOException("Resposta inesperada do servidor");
String access = Util.str(u, "accessToken"); String access = Util.str(u, "accessToken");
String refresh = Util.str(u, "refreshToken"); String refresh = Util.str(u, "refreshToken");
String legacy = Util.str(u, "token"); String legacy = Util.str(u, "token");
Prefs p = Prefs.get(c); Prefs p = Prefs.get(c);
if (access.length() > 0) p.setAuth(user, Prefs.MODE_JWT, access, refresh); if (access.length() > 0) p.setAuth(user, Prefs.MODE_JWT, access, refresh);
else if (legacy.length() > 0) p.setAuth(user, Prefs.MODE_LEGACY, legacy, ""); else if (legacy.length() > 0) p.setAuth(user, Prefs.MODE_LEGACY, legacy, "");
else throw new IOException("The server didn't return a sign-in token."); else throw new IOException("O servidor non devolveu ningún token de sesión.");
return Util.str(o, "userDefaultLibraryId"); return Util.str(o, "userDefaultLibraryId");
} finally { } finally {
conn.disconnect(); conn.disconnect();
@@ -310,7 +310,7 @@ final class Api {
return name; return name;
} catch (IOException e) { } catch (IOException e) {
p.logout(); p.logout();
if (e instanceof AuthException) throw new ApiException(401, "That API key wasn't accepted."); if (e instanceof AuthException) throw new ApiException(401, "Non se aceptou esa chave da API.");
throw e; throw e;
} }
} }
@@ -335,7 +335,7 @@ final class Api {
private static void refreshIf(Context c, String stale, long minSeconds) throws IOException { private static void refreshIf(Context c, String stale, long minSeconds) throws IOException {
synchronized (REFRESH_LOCK) { synchronized (REFRESH_LOCK) {
Prefs p = Prefs.get(c); Prefs p = Prefs.get(c);
if (!p.canRefresh()) throw new AuthException("Your sign-in has expired. Please log in again."); if (!p.canRefresh()) throw new AuthException("A túa sesión caducou. Inicia sesión de novo.");
if (stale != null) { if (stale != null) {
if (!stale.equals(p.token())) return; if (!stale.equals(p.token())) return;
} else if (p.tokenSecondsLeft() >= minSeconds) { } else if (p.tokenSecondsLeft() >= minSeconds) {
@@ -348,12 +348,12 @@ final class Api {
writeBody(conn, null); writeBody(conn, null);
int code = responseCode(conn); int code = responseCode(conn);
if (code == 401 || code == 403) { if (code == 401 || code == 403) {
throw new AuthException("Your sign-in has expired. Please log in again."); throw new AuthException("A túa sesión caducou. Inicia sesión de novo.");
} }
if (code >= 400) throw new ApiException(code, errorText(conn)); if (code >= 400) throw new ApiException(code, errorText(conn));
JSONObject u = obj(readBody(conn, 500, null)).optJSONObject("user"); JSONObject u = obj(readBody(conn, 500, null)).optJSONObject("user");
String access = u == null ? "" : Util.str(u, "accessToken"); String access = u == null ? "" : Util.str(u, "accessToken");
if (access.length() == 0) throw new AuthException("Your sign-in has expired. Please log in again."); if (access.length() == 0) throw new AuthException("A túa sesión caducou. Inicia sesión de novo.");
p.setTokens(access, Util.str(u, "refreshToken")); p.setTokens(access, Util.str(u, "refreshToken"));
} finally { } finally {
conn.disconnect(); conn.disconnect();
+18 -18
View File
@@ -73,7 +73,7 @@ abstract class BaseActivity extends Activity {
return TrustPrompt.offer(this, new Runnable() { return TrustPrompt.offer(this, new Runnable() {
public void run() { public void run() {
if (retry != null) retry.run(); if (retry != null) retry.run();
else toast("Certificate trusted. Try again."); else toast("Certificado aceptado. Téntao de novo.");
} }
}); });
} }
@@ -130,13 +130,13 @@ abstract class BaseActivity extends Activity {
dur = ps.durationMs(); dur = ps.durationMs();
int st = ps.state(); int st = ps.state();
if (st == PlayerService.LOADING || st == PlayerService.ERROR) sub = ps.status(); if (st == PlayerService.LOADING || st == PlayerService.ERROR) sub = ps.status();
else sub = Util.join(e.show, dur > 0 ? Util.clock(dur - pos) + " left" : ""); else sub = Util.join(e.show, dur > 0 ? "quedan " + Util.clock(dur - pos) : "");
} else { } else {
double[] p = App.progress(e.id); double[] p = App.progress(e.id);
double cur = p != null ? p[0] : Math.max(0, App.localPosition(this, e.id)); double cur = p != null ? p[0] : Math.max(0, App.localPosition(this, e.id));
pos = (int) (cur * 1000); pos = (int) (cur * 1000);
dur = (int) (e.duration * 1000); dur = (int) (e.duration * 1000);
sub = Util.join(e.show, "Paused"); sub = Util.join(e.show, "En pausa");
} }
miniSub.setText(sub); miniSub.setText(sub);
miniProgress.setProgress(dur > 0 ? (int) (1000L * pos / dur) : 0); miniProgress.setProgress(dur > 0 ? (int) (1000L * pos / dur) : 0);
@@ -149,14 +149,14 @@ abstract class BaseActivity extends Activity {
protected void episodeMenu(ContextMenu menu, Episode e, boolean offerShow) { protected void episodeMenu(ContextMenu menu, Episode e, boolean offerShow) {
menu.setHeaderTitle(e.title); menu.setHeaderTitle(e.title);
menu.add(0, M_PLAY, 0, "Play"); menu.add(0, M_PLAY, 0, "Reproducir");
if (DownloadService.busyWith(e.id)) menu.add(0, M_CANCEL, 0, "Cancel download"); if (DownloadService.busyWith(e.id)) menu.add(0, M_CANCEL, 0, "Cancelar descarga");
else if (Downloads.has(this, e.id)) menu.add(0, M_DELETE, 0, "Delete download"); else if (Downloads.has(this, e.id)) menu.add(0, M_DELETE, 0, "Eliminar descarga");
else menu.add(0, M_DOWNLOAD, 0, "Download"); else menu.add(0, M_DOWNLOAD, 0, "Descargar");
double[] p = App.progress(e.id); double[] p = App.progress(e.id);
if (p != null && p[2] > 0) menu.add(0, M_UNPLAYED, 0, "Mark as unplayed"); if (p != null && p[2] > 0) menu.add(0, M_UNPLAYED, 0, "Marcar como non escoitado");
else menu.add(0, M_PLAYED, 0, "Mark as played"); else menu.add(0, M_PLAYED, 0, "Marcar como escoitado");
if (offerShow) menu.add(0, M_SHOW, 0, "Go to show"); if (offerShow) menu.add(0, M_SHOW, 0, "Ir ao programa");
} }
/** Returns true if handled. onChanged runs on the UI thread after the list should refresh. */ /** Returns true if handled. onChanged runs on the UI thread after the list should refresh. */
@@ -168,26 +168,26 @@ abstract class BaseActivity extends Activity {
case M_DOWNLOAD: case M_DOWNLOAD:
if (Prefs.get(this).wifiOnly() && !Util.onWifi(this)) { if (Prefs.get(this).wifiOnly() && !Util.onWifi(this)) {
new AlertDialog.Builder(this) new AlertDialog.Builder(this)
.setMessage("You're not on Wi-Fi. Download over mobile data this time?") .setMessage("Non estás nunha wifi. Descargar con datos móbiles esta vez?")
.setPositiveButton("Download", new DialogInterface.OnClickListener() { .setPositiveButton("Descargar", new DialogInterface.OnClickListener() {
public void onClick(DialogInterface d, int w) { public void onClick(DialogInterface d, int w) {
Prefs.get(BaseActivity.this).sp.edit().putBoolean("wifi_only", false).commit(); Prefs.get(BaseActivity.this).sp.edit().putBoolean("wifi_only", false).commit();
DownloadService.enqueue(BaseActivity.this, e); DownloadService.enqueue(BaseActivity.this, e);
toast("Downloading. Wi-Fi-only is now off (change it in Settings)."); toast("Descargando. Desactivouse «só con wifi» (podes cambialo en Axustes).");
if (onChanged != null) onChanged.run(); if (onChanged != null) onChanged.run();
} }
}) })
.setNegativeButton("Cancel", null) .setNegativeButton("Cancelar", null)
.show(); .show();
} else { } else {
DownloadService.enqueue(this, e); DownloadService.enqueue(this, e);
toast("Downloading…"); toast("Descargando…");
} }
if (onChanged != null) onChanged.run(); if (onChanged != null) onChanged.run();
return true; return true;
case M_CANCEL: case M_CANCEL:
DownloadService.cancel(e.id); DownloadService.cancel(e.id);
toast("Download cancelled"); toast("Descarga cancelada");
if (onChanged != null) onChanged.run(); if (onChanged != null) onChanged.run();
return true; return true;
case M_DELETE: case M_DELETE:
@@ -196,7 +196,7 @@ abstract class BaseActivity extends Activity {
ps.stop(); ps.stop();
} }
Downloads.delete(this, e); Downloads.delete(this, e);
toast("Download deleted"); toast("Descarga eliminada");
if (onChanged != null) onChanged.run(); if (onChanged != null) onChanged.run();
return true; return true;
case M_PLAYED: case M_PLAYED:
@@ -230,7 +230,7 @@ abstract class BaseActivity extends Activity {
} }
void done(Void r) { void done(Void r) {
toast(played ? "Marked as played" : "Marked as unplayed"); toast(played ? "Marcado como escoitado" : "Marcado como non escoitado");
} }
void failed(Exception ex) { void failed(Exception ex) {
+7 -7
View File
@@ -102,7 +102,7 @@ public class DownloadService extends IntentService {
} }
} catch (Exception ex) { } catch (Exception ex) {
if (!e.id.equals(cancelId)) { if (!e.id.equals(cancelId)) {
lastError = "Download failed: " + Util.describe(ex); lastError = "Fallou a descarga: " + Util.describe(ex);
notifyDone(e.title, lastError); notifyDone(e.title, lastError);
} }
} finally { } finally {
@@ -116,14 +116,14 @@ public class DownloadService extends IntentService {
private void download(Episode e) throws IOException { private void download(Episode e) throws IOException {
if (Prefs.get(this).wifiOnly() && !Util.onWifi(this)) { if (Prefs.get(this).wifiOnly() && !Util.onWifi(this)) {
throw new IOException("not on Wi-Fi (Settings: Download on Wi-Fi only)"); throw new IOException("non hai wifi (Axustes: Descargar só con wifi)");
} }
File dir = Downloads.dir(this); File dir = Downloads.dir(this);
if (Downloads.audio(this, e) != null) { if (Downloads.audio(this, e) != null) {
Downloads.saved(this, e); Downloads.saved(this, e);
return; return;
} }
if (e.ino.length() == 0) throw new IOException("this episode has no audio file"); if (e.ino.length() == 0) throw new IOException("este episodio non ten ficheiro de audio");
File part = new File(dir, e.id + ".part"); File part = new File(dir, e.id + ".part");
Notification n = progressNotification(e, 0); Notification n = progressNotification(e, 0);
startForeground(NOTIF_ID, n); startForeground(NOTIF_ID, n);
@@ -158,8 +158,8 @@ public class DownloadService extends IntentService {
if (code != 404 && code != 403) break; if (code != 404 && code != 403) break;
} }
if (code != 200 && code != 206) { if (code != 200 && code != 206) {
if (code == 401) throw new Api.AuthException("sign-in expired; log in again"); if (code == 401) throw new Api.AuthException("a sesión caducou; inicia sesión de novo");
if (code == 403) throw new IOException("your account isn't allowed to download"); if (code == 403) throw new IOException("a túa conta non ten permiso para descargar");
throw new Api.ApiException(code, ""); throw new Api.ApiException(code, "");
} }
try { try {
@@ -194,7 +194,7 @@ public class DownloadService extends IntentService {
} }
if (e.id.equals(cancelId)) return; if (e.id.equals(cancelId)) return;
File dest = new File(dir, e.id + e.ext); File dest = new File(dir, e.id + e.ext);
if (!part.renameTo(dest)) throw new IOException("couldn't save the file"); if (!part.renameTo(dest)) throw new IOException("non se puido gardar o ficheiro");
Downloads.saved(this, e); Downloads.saved(this, e);
percent = 100; percent = 100;
} }
@@ -216,7 +216,7 @@ public class DownloadService extends IntentService {
synchronized (queue) { synchronized (queue) {
left = queue.size(); left = queue.size();
} }
String text = "Downloading " + pct + "%" + (left > 0 ? " · " + left + " more queued" : ""); String text = "Descargando " + pct + " %" + (left > 0 ? " · " + left + " máis na cola" : "");
n.setLatestEventInfo(this, e.title, text, openApp()); n.setLatestEventInfo(this, e.title, text, openApp());
return n; return n;
} }
+3 -3
View File
@@ -28,11 +28,11 @@ final class Downloads {
static File dir(Context c) throws IOException { static File dir(Context c) throws IOException {
if (!Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())) { if (!Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())) {
throw new IOException("No SD card available. Insert or unmount it from USB to download."); throw new IOException("Non hai tarxeta SD dispoñible. Insírea ou desconéctaa do USB para descargar.");
} }
File d = c.getExternalFilesDir(Environment.DIRECTORY_PODCASTS); File d = c.getExternalFilesDir(Environment.DIRECTORY_PODCASTS);
if (d == null) throw new IOException("Can't use the SD card."); if (d == null) throw new IOException("Non se pode usar a tarxeta SD.");
if (!d.exists() && !d.mkdirs()) throw new IOException("Can't create a folder on the SD card."); if (!d.exists() && !d.mkdirs()) throw new IOException("Non se pode crear un cartafol na tarxeta SD.");
File nomedia = new File(d, ".nomedia"); File nomedia = new File(d, ".nomedia");
if (!nomedia.exists()) { if (!nomedia.exists()) {
try { try {
+21 -21
View File
@@ -202,7 +202,7 @@ public class HomeActivity extends BaseActivity {
} }
void done(Integer n) { void done(Integer n) {
if (n > 0) toast("Synced offline progress"); if (n > 0) toast("Sincronizouse o progreso gardado sen conexión");
} }
}.start(); }.start();
} }
@@ -222,7 +222,7 @@ public class HomeActivity extends BaseActivity {
found.add(new String[]{Util.str(l, "id"), Util.str(l, "name")}); found.add(new String[]{Util.str(l, "id"), Util.str(l, "name")});
} }
} }
if (found.isEmpty()) throw new IOException("This server has no podcast libraries."); if (found.isEmpty()) throw new IOException("Este servidor non ten bibliotecas de podcasts.");
podcastLibs = found; podcastLibs = found;
} }
String id = p.libraryId(); String id = p.libraryId();
@@ -273,10 +273,10 @@ public class HomeActivity extends BaseActivity {
// older servers: just skip this section // older servers: just skip this section
} }
if (!cont.isEmpty()) { if (!cont.isEmpty()) {
rows.add(Rows.Row.header("CONTINUE LISTENING")); rows.add(Rows.Row.header("CONTINUAR ESCOITANDO"));
rows.addAll(cont); rows.addAll(cont);
} }
rows.add(Rows.Row.header("LATEST EPISODES")); rows.add(Rows.Row.header("ÚLTIMOS EPISODIOS"));
} }
JSONArray eps = Api.recentEpisodes(HomeActivity.this, lib, page, PAGE).optJSONArray("episodes"); JSONArray eps = Api.recentEpisodes(HomeActivity.this, lib, page, PAGE).optJSONArray("episodes");
int n = eps == null ? 0 : eps.length(); int n = eps == null ? 0 : eps.length();
@@ -301,8 +301,8 @@ public class HomeActivity extends BaseActivity {
} }
latestPage = page; latestPage = page;
latestMore = hasMore; latestMore = hasMore;
if (hasMore) latestCache.add(Rows.Row.more("Load more")); if (hasMore) latestCache.add(Rows.Row.more("Cargar máis"));
if (tab == TAB_LATEST) show(latestCache, "No episodes yet."); if (tab == TAB_LATEST) show(latestCache, "Aínda non hai episodios.");
} }
void failed(Exception e) { void failed(Exception e) {
@@ -315,12 +315,12 @@ public class HomeActivity extends BaseActivity {
if (more) { if (more) {
toast(Util.describe(e)); toast(Util.describe(e));
if (latestCache != null && !latestCache.isEmpty()) { if (latestCache != null && !latestCache.isEmpty()) {
latestCache.get(latestCache.size() - 1).text = "Load more"; latestCache.get(latestCache.size() - 1).text = "Cargar máis";
adapter.notifyDataSetChanged(); adapter.notifyDataSetChanged();
} }
return; return;
} }
latestError = Util.describe(e) + "\n\nTap refresh to try again."; latestError = Util.describe(e) + "\n\nToca o botón de actualizar (arriba á dereita) para tentalo de novo.";
if (offerTrust(retryRefresh)) { if (offerTrust(retryRefresh)) {
if (tab == TAB_LATEST) show(null, null); if (tab == TAB_LATEST) show(null, null);
return; return;
@@ -368,7 +368,7 @@ public class HomeActivity extends BaseActivity {
if (isFinishing()) return; if (isFinishing()) return;
updateLibraryLabel(); updateLibraryLabel();
showsCache = rows; showsCache = rows;
if (tab == TAB_SHOWS) show(rows, "No podcasts in this library."); if (tab == TAB_SHOWS) show(rows, "Non hai podcasts nesta biblioteca.");
} }
void failed(Exception e) { void failed(Exception e) {
@@ -378,7 +378,7 @@ public class HomeActivity extends BaseActivity {
showError(e); showError(e);
return; return;
} }
showsError = Util.describe(e) + "\n\nTap refresh to try again."; showsError = Util.describe(e) + "\n\nToca o botón de actualizar (arriba á dereita) para tentalo de novo.";
if (offerTrust(retryRefresh)) { if (offerTrust(retryRefresh)) {
if (tab == TAB_SHOWS) show(null, null); if (tab == TAB_SHOWS) show(null, null);
return; return;
@@ -391,7 +391,7 @@ public class HomeActivity extends BaseActivity {
/** When the server can't be reached but episodes are saved, show those instead. */ /** When the server can't be reached but episodes are saved, show those instead. */
private void offlineFallback(Exception e) { private void offlineFallback(Exception e) {
if (!(e instanceof Api.ApiException) && !Downloads.list(this).isEmpty()) { if (!(e instanceof Api.ApiException) && !Downloads.list(this).isEmpty()) {
toast("Can't reach the server. Showing saved episodes."); toast("Non se pode acceder ao servidor. Amósanse os episodios gardados.");
selectTab(TAB_SAVED); selectTab(TAB_SAVED);
} else { } else {
show(null, null); show(null, null);
@@ -405,17 +405,17 @@ public class HomeActivity extends BaseActivity {
savedSignature = (cur == null ? "" : cur.id) + "/" + queued.size() + "/" + DownloadService.lastError; savedSignature = (cur == null ? "" : cur.id) + "/" + queued.size() + "/" + DownloadService.lastError;
if (DownloadService.lastError != null) rows.add(Rows.Row.header(DownloadService.lastError)); if (DownloadService.lastError != null) rows.add(Rows.Row.header(DownloadService.lastError));
if (cur != null || !queued.isEmpty()) { if (cur != null || !queued.isEmpty()) {
rows.add(Rows.Row.header("DOWNLOADING")); rows.add(Rows.Row.header("DESCARGANDO"));
if (cur != null) rows.add(Rows.Row.episode(cur)); if (cur != null) rows.add(Rows.Row.episode(cur));
for (Episode q : queued) rows.add(Rows.Row.episode(q)); for (Episode q : queued) rows.add(Rows.Row.episode(q));
} }
List<Episode> saved = Downloads.list(this); List<Episode> saved = Downloads.list(this);
if (!saved.isEmpty()) { if (!saved.isEmpty()) {
rows.add(Rows.Row.header("SAVED ON SD CARD (" + saved.size() + ")")); rows.add(Rows.Row.header("GARDADOS NA TARXETA SD (" + saved.size() + ")"));
for (Episode e : saved) rows.add(Rows.Row.episode(e)); for (Episode e : saved) rows.add(Rows.Row.episode(e));
} }
if (rows.isEmpty()) { if (rows.isEmpty()) {
show(rows, "Nothing saved yet.\n\nLong-press an episode and choose Download to listen offline."); show(rows, "Aínda non gardaches nada.\n\nMantén premido un episodio e escolle Descargar para escoitalo sen conexión.");
} else { } else {
show(rows, null); show(rows, null);
} }
@@ -436,7 +436,7 @@ public class HomeActivity extends BaseActivity {
break; break;
case Rows.MORE: case Rows.MORE:
if (!loadingLatest && latestMore) { if (!loadingLatest && latestMore) {
r.text = "Loading…"; r.text = "Cargando…";
adapter.notifyDataSetChanged(); adapter.notifyDataSetChanged();
loadLatest(true); loadLatest(true);
} }
@@ -469,12 +469,12 @@ public class HomeActivity extends BaseActivity {
private void chooseLibrary() { private void chooseLibrary() {
if (podcastLibs == null || podcastLibs.size() < 2) { if (podcastLibs == null || podcastLibs.size() < 2) {
toast(podcastLibs == null ? "Still loading libraries…" : "This is your only podcast library."); toast(podcastLibs == null ? "Aínda se están cargando as bibliotecas…" : "Esta é a túa única biblioteca de podcasts.");
return; return;
} }
String[] names = new String[podcastLibs.size()]; String[] names = new String[podcastLibs.size()];
for (int i = 0; i < names.length; i++) names[i] = podcastLibs.get(i)[1]; for (int i = 0; i < names.length; i++) names[i] = podcastLibs.get(i)[1];
new AlertDialog.Builder(this).setTitle("Podcast library") new AlertDialog.Builder(this).setTitle("Biblioteca de podcasts")
.setItems(names, new DialogInterface.OnClickListener() { .setItems(names, new DialogInterface.OnClickListener() {
public void onClick(DialogInterface d, int which) { public void onClick(DialogInterface d, int which) {
String[] l = podcastLibs.get(which); String[] l = podcastLibs.get(which);
@@ -491,10 +491,10 @@ public class HomeActivity extends BaseActivity {
@Override @Override
public boolean onCreateOptionsMenu(Menu menu) { public boolean onCreateOptionsMenu(Menu menu) {
menu.add(0, 1, 0, "Refresh").setIcon(android.R.drawable.ic_menu_rotate); menu.add(0, 1, 0, "Actualizar").setIcon(android.R.drawable.ic_menu_rotate);
menu.add(0, 2, 0, "Library").setIcon(android.R.drawable.ic_menu_agenda); menu.add(0, 2, 0, "Biblioteca").setIcon(android.R.drawable.ic_menu_agenda);
menu.add(0, 3, 0, "Now playing").setIcon(android.R.drawable.ic_media_play); menu.add(0, 3, 0, "En reprodución").setIcon(android.R.drawable.ic_media_play);
menu.add(0, 4, 0, "Settings").setIcon(android.R.drawable.ic_menu_preferences); menu.add(0, 4, 0, "Axustes").setIcon(android.R.drawable.ic_menu_preferences);
return true; return true;
} }
+10 -10
View File
@@ -88,13 +88,13 @@ final class Json {
return JSONObject.NULL; return JSONObject.NULL;
default: default:
if (c == '-' || (c >= '0' && c <= '9')) return number(c); if (c == '-' || (c >= '0' && c <= '9')) return number(c);
throw new IOException("Unexpected reply from server"); throw new IOException("Resposta inesperada do servidor");
} }
} }
private void expect(String rest) throws IOException { private void expect(String rest) throws IOException {
for (int i = 0; i < rest.length(); i++) { for (int i = 0; i < rest.length(); i++) {
if (read() != rest.charAt(i)) throw new IOException("Unexpected reply from server"); if (read() != rest.charAt(i)) throw new IOException("Resposta inesperada do servidor");
} }
} }
@@ -149,10 +149,10 @@ final class Json {
return o; return o;
} }
while (true) { while (true) {
if (c != '"') throw new IOException("Unexpected reply from server"); if (c != '"') throw new IOException("Resposta inesperada do servidor");
String key = string(512); String key = string(512);
c = skipWs(); c = skipWs();
if (c != ':') throw new IOException("Unexpected reply from server"); if (c != ':') throw new IOException("Resposta inesperada do servidor");
c = skipWs(); c = skipWs();
if (skipKey(key)) { if (skipKey(key)) {
skipValue(c); skipValue(c);
@@ -173,7 +173,7 @@ final class Json {
depth--; depth--;
return o; return o;
} }
throw new IOException("Unexpected reply from server"); throw new IOException("Resposta inesperada do servidor");
} }
} }
@@ -189,7 +189,7 @@ final class Json {
continue; continue;
} }
if (c == ']') return a; if (c == ']') return a;
throw new IOException("Unexpected reply from server"); throw new IOException("Resposta inesperada do servidor");
} }
} }
@@ -202,7 +202,7 @@ final class Json {
int d = 1; int d = 1;
while (d > 0) { while (d > 0) {
int ch = read(); int ch = read();
if (ch == -1) throw new IOException("Reply was cut off"); if (ch == -1) throw new IOException("A resposta chegou incompleta");
if (ch == '"') string(0); if (ch == '"') string(0);
else if (ch == '{' || ch == '[') d++; else if (ch == '{' || ch == '[') d++;
else if (ch == '}' || ch == ']') d--; else if (ch == '}' || ch == ']') d--;
@@ -217,7 +217,7 @@ final class Json {
boolean cut = false; boolean cut = false;
while (true) { while (true) {
int c = read(); int c = read();
if (c == -1) throw new IOException("Reply was cut off"); if (c == -1) throw new IOException("A resposta chegou incompleta");
if (c == '"') break; if (c == '"') break;
if (c == '\\') { if (c == '\\') {
c = read(); c = read();
@@ -231,14 +231,14 @@ final class Json {
int v = 0; int v = 0;
for (int i = 0; i < 4; i++) { for (int i = 0; i < 4; i++) {
int d = Character.digit(read(), 16); int d = Character.digit(read(), 16);
if (d < 0) throw new IOException("Unexpected reply from server"); if (d < 0) throw new IOException("Resposta inesperada do servidor");
v = v * 16 + d; v = v * 16 + d;
} }
c = v; c = v;
break; break;
} }
case -1: case -1:
throw new IOException("Reply was cut off"); throw new IOException("A resposta chegou incompleta");
default: default:
break; // \" \\ \/ break; // \" \\ \/
} }
+4 -4
View File
@@ -42,7 +42,7 @@ public class LoginActivity extends Activity {
int vis = on ? View.GONE : View.VISIBLE; int vis = on ? View.GONE : View.VISIBLE;
user.setVisibility(vis); user.setVisibility(vis);
userLabel.setVisibility(vis); userLabel.setVisibility(vis);
passLabel.setText(on ? "API key" : "Password"); passLabel.setText(on ? "Chave da API" : "Contrasinal");
pass.setInputType(on ? InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS pass.setInputType(on ? InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS
: InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD); : InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD);
pass.setText(""); pass.setText("");
@@ -69,17 +69,17 @@ public class LoginActivity extends Activity {
final String pw = pass.getText().toString(); final String pw = pass.getText().toString();
final boolean key = useKey.isChecked(); final boolean key = useKey.isChecked();
if (srv.length() == 0) { if (srv.length() == 0) {
status.setText("Enter your server address."); status.setText("Escribe o enderezo do servidor.");
return; return;
} }
if (key ? pw.trim().length() == 0 : u.length() == 0) { if (key ? pw.trim().length() == 0 : u.length() == 0) {
status.setText(key ? "Paste your API key." : "Enter your username."); status.setText(key ? "Pega a túa chave da API." : "Escribe o teu usuario.");
return; return;
} }
server.setText(srv); server.setText(srv);
((InputMethodManager) getSystemService(INPUT_METHOD_SERVICE)).hideSoftInputFromWindow(pass.getWindowToken(), 0); ((InputMethodManager) getSystemService(INPUT_METHOD_SERVICE)).hideSoftInputFromWindow(pass.getWindowToken(), 0);
connect.setEnabled(false); connect.setEnabled(false);
status.setText("Connecting…"); status.setText("Conectando…");
final Prefs p = Prefs.get(this); final Prefs p = Prefs.get(this);
p.logout(); p.logout();
+18 -18
View File
@@ -167,9 +167,9 @@ public class PlayerActivity extends BaseActivity {
Episode e = current(); Episode e = current();
if (e == null) { if (e == null) {
show.setText(""); show.setText("");
title.setText("Nothing playing"); title.setText("Non se está a reproducir nada");
date.setText(""); date.setText("");
status.setText("Pick an episode from the list."); status.setText("Escolle un episodio da lista.");
desc.setText(""); desc.setText("");
cover.setVisibility(View.GONE); cover.setVisibility(View.GONE);
setControls(false); setControls(false);
@@ -181,7 +181,7 @@ public class PlayerActivity extends BaseActivity {
title.setText(e.title); title.setText(e.title);
date.setText(Util.join(Util.date(e.published), e.duration > 0 ? Util.duration(e.duration) : "")); date.setText(Util.join(Util.date(e.published), e.duration > 0 ? Util.duration(e.duration) : ""));
desc.setText(Util.plainText(e.desc)); desc.setText(Util.plainText(e.desc));
Covers.load(this, cover, e.itemId, (int) (84 * getResources().getDisplayMetrics().density)); Covers.load(this, cover, e.itemId, getResources().getDimensionPixelSize(R.dimen.cover_player));
} }
PlayerService ps = PlayerService.get(); PlayerService ps = PlayerService.get();
@@ -195,8 +195,8 @@ public class PlayerActivity extends BaseActivity {
playing = ps.isPlaying() || (ps.state() == PlayerService.LOADING); playing = ps.isPlaying() || (ps.state() == PlayerService.LOADING);
st = ps.status(); st = ps.status();
long sleep = ps.sleepLeftMs(); long sleep = ps.sleepLeftMs();
if (sleep == -1) st = Util.join(st, "Sleep at end"); if (sleep == -1) st = Util.join(st, "Pausa ao final do episodio");
else if (sleep > 0) st = Util.join(st, "Sleep in " + Util.clock(sleep)); else if (sleep > 0) st = Util.join(st, "Pausa en " + Util.clock(sleep));
seek.setSecondaryProgress(ps.bufferedPercent() * 10); seek.setSecondaryProgress(ps.bufferedPercent() * 10);
setControls(true); setControls(true);
} else { } else {
@@ -204,7 +204,7 @@ public class PlayerActivity extends BaseActivity {
double cur = pr != null ? pr[0] : Math.max(0, App.localPosition(this, e.id)); double cur = pr != null ? pr[0] : Math.max(0, App.localPosition(this, e.id));
p = (int) (cur * 1000); p = (int) (cur * 1000);
dur = (int) (e.duration * 1000); dur = (int) (e.duration * 1000);
st = pending != null ? "Connecting…" : (Downloads.has(this, e.id) ? "Saved on SD card" : ""); st = pending != null ? "Conectando…" : (Downloads.has(this, e.id) ? "Gardado na tarxeta SD" : "");
playing = pending != null; playing = pending != null;
seek.setSecondaryProgress(0); seek.setSecondaryProgress(0);
setControls(true); setControls(true);
@@ -234,17 +234,17 @@ public class PlayerActivity extends BaseActivity {
if (e == null) return false; if (e == null) return false;
PlayerService ps = PlayerService.get(); PlayerService ps = PlayerService.get();
boolean active = ps != null && ps.episode() != null && ps.episode().id.equals(e.id); boolean active = ps != null && ps.episode() != null && ps.episode().id.equals(e.id);
if (active) menu.add(0, 1, 0, "Sleep timer").setIcon(android.R.drawable.ic_lock_idle_alarm); if (active) menu.add(0, 1, 0, "Temporizador de pausa").setIcon(android.R.drawable.ic_lock_idle_alarm);
menu.add(0, 2, 0, "Mark as played").setIcon(android.R.drawable.ic_menu_agenda); menu.add(0, 2, 0, "Marcar como escoitado").setIcon(android.R.drawable.ic_menu_agenda);
if (DownloadService.busyWith(e.id)) { if (DownloadService.busyWith(e.id)) {
menu.add(0, 3, 0, "Cancel download").setIcon(android.R.drawable.ic_menu_close_clear_cancel); menu.add(0, 3, 0, "Cancelar descarga").setIcon(android.R.drawable.ic_menu_close_clear_cancel);
} else if (Downloads.has(this, e.id)) { } else if (Downloads.has(this, e.id)) {
menu.add(0, 3, 0, "Delete download").setIcon(android.R.drawable.ic_menu_delete); menu.add(0, 3, 0, "Eliminar descarga").setIcon(android.R.drawable.ic_menu_delete);
} else { } else {
menu.add(0, 3, 0, "Download").setIcon(android.R.drawable.ic_menu_save); menu.add(0, 3, 0, "Descargar").setIcon(android.R.drawable.ic_menu_save);
} }
menu.add(0, 4, 0, "Go to show").setIcon(android.R.drawable.ic_menu_view); menu.add(0, 4, 0, "Ir ao programa").setIcon(android.R.drawable.ic_menu_view);
if (active) menu.add(0, 5, 0, "Stop").setIcon(android.R.drawable.ic_menu_close_clear_cancel); if (active) menu.add(0, 5, 0, "Deter").setIcon(android.R.drawable.ic_menu_close_clear_cancel);
return true; return true;
} }
@@ -261,7 +261,7 @@ public class PlayerActivity extends BaseActivity {
case 2: case 2:
if (active) { if (active) {
ps.finishAndStop(); ps.finishAndStop();
toast("Marked as played"); toast("Marcado como escoitado");
} else { } else {
episodeAction(M_PLAYED, e, null); episodeAction(M_PLAYED, e, null);
} }
@@ -285,15 +285,15 @@ public class PlayerActivity extends BaseActivity {
private void sleepDialog() { private void sleepDialog() {
final int[] minutes = {0, 5, 10, 15, 30, 45, 60, -1}; final int[] minutes = {0, 5, 10, 15, 30, 45, 60, -1};
String[] labels = {"Off", "5 minutes", "10 minutes", "15 minutes", "30 minutes", "45 minutes", "1 hour", String[] labels = {"Desactivado", "5 minutos", "10 minutos", "15 minutos", "30 minutos", "45 minutos", "1 hora",
"End of episode"}; "Ao final do episodio"};
new AlertDialog.Builder(this).setTitle("Sleep timer") new AlertDialog.Builder(this).setTitle("Temporizador de pausa")
.setItems(labels, new DialogInterface.OnClickListener() { .setItems(labels, new DialogInterface.OnClickListener() {
public void onClick(DialogInterface d, int which) { public void onClick(DialogInterface d, int which) {
PlayerService ps = PlayerService.get(); PlayerService ps = PlayerService.get();
if (ps == null) return; if (ps == null) return;
ps.setSleep(minutes[which]); ps.setSleep(minutes[which]);
toast(minutes[which] == 0 ? "Sleep timer off" : "Sleep timer set"); toast(minutes[which] == 0 ? "Temporizador desactivado" : "Temporizador activado");
refresh(); refresh();
} }
}).show(); }).show();
+22 -22
View File
@@ -256,7 +256,7 @@ public class PlayerService extends Service implements MediaPlayer.OnPreparedList
} }
lastPosMs = positionMs(); lastPosMs = positionMs();
state = PAUSED; state = PAUSED;
status = "Paused"; status = "En pausa";
pausedAt = SystemClock.elapsedRealtime(); pausedAt = SystemClock.elapsedRealtime();
syncNow(false); syncNow(false);
wifi.release(); wifi.release();
@@ -345,7 +345,7 @@ public class PlayerService extends Service implements MediaPlayer.OnPreparedList
markFinished(); markFinished();
releasePlayer(); releasePlayer();
state = ENDED; state = ENDED;
status = "Marked as played"; status = "Marcado como escoitado";
lastPosMs = dur; lastPosMs = dur;
pausedAt = SystemClock.elapsedRealtime(); pausedAt = SystemClock.elapsedRealtime();
wifi.release(); wifi.release();
@@ -364,7 +364,7 @@ public class PlayerService extends Service implements MediaPlayer.OnPreparedList
private void open(final Episode e, final double startSec, boolean playNow) { private void open(final Episode e, final double startSec, boolean playNow) {
releasePlayer(); releasePlayer();
state = LOADING; state = LOADING;
status = "Connecting…"; status = "Conectando…";
playOnPrepared = playNow; playOnPrepared = playNow;
showNotification(true); showNotification(true);
final int gen = ++generation; final int gen = ++generation;
@@ -407,9 +407,9 @@ public class PlayerService extends Service implements MediaPlayer.OnPreparedList
JSONObject t = tracks == null ? null : tracks.optJSONObject(0); JSONObject t = tracks == null ? null : tracks.optJSONObject(0);
String cu = t == null ? "" : Util.str(t, "contentUrl"); String cu = t == null ? "" : Util.str(t, "contentUrl");
if (cu.length() == 0) { if (cu.length() == 0) {
s.error = "The server sent no audio for this episode."; s.error = "O servidor non enviou audio para este episodio.";
} else if (cu.contains(".m3u8")) { } else if (cu.contains(".m3u8")) {
s.error = "This episode needs transcoding, which Android 2.x can't play. Try downloading it."; s.error = "Este episodio precisa transcodificación, que Android 2.x non pode reproducir. Proba a descargalo.";
} else { } else {
s.contentUrl = cu; s.contentUrl = cu;
s.url = streamUrl(c, cu); s.url = streamUrl(c, cu);
@@ -442,7 +442,7 @@ public class PlayerService extends Service implements MediaPlayer.OnPreparedList
local = s.local; local = s.local;
resumeMs = (int) (s.start * 1000); resumeMs = (int) (s.start * 1000);
lastPosMs = resumeMs; lastPosMs = resumeMs;
prepare(s.url, s.offline ? "Offline · progress will sync later" : null); prepare(s.url, s.offline ? "Sen conexión · o progreso sincronizarase despois" : null);
} }
private void prepare(String url, String note) { private void prepare(String url, String note) {
@@ -467,10 +467,10 @@ public class PlayerService extends Service implements MediaPlayer.OnPreparedList
wifi.acquire(); wifi.acquire();
} }
state = LOADING; state = LOADING;
status = note != null ? note : local ? "Opening saved file…" : "Buffering…"; status = note != null ? note : local ? "Abrindo o ficheiro gardado…" : "Cargando…";
mp.prepareAsync(); mp.prepareAsync();
} catch (Exception ex) { } catch (Exception ex) {
fail("Can't open the audio: " + Util.describe(ex)); fail("Non se pode abrir o audio: " + Util.describe(ex));
} }
} }
@@ -490,7 +490,7 @@ public class PlayerService extends Service implements MediaPlayer.OnPreparedList
start(); start();
} else { } else {
state = PAUSED; state = PAUSED;
status = "Paused"; status = "En pausa";
showNotification(false); showNotification(false);
} }
} }
@@ -506,12 +506,12 @@ public class PlayerService extends Service implements MediaPlayer.OnPreparedList
mp.setVolume(1f, 1f); mp.setVolume(1f, 1f);
mp.start(); mp.start();
} catch (Exception ex) { } catch (Exception ex) {
fail("Playback failed: " + Util.describe(ex)); fail("Fallou a reprodución: " + Util.describe(ex));
return; return;
} }
if (!local) wifi.acquire(); if (!local) wifi.acquire();
state = PLAYING; state = PLAYING;
status = local ? (sessionId == null ? "Saved file · offline" : "Saved file") : "Streaming"; status = local ? (sessionId == null ? "Ficheiro gardado · sen conexión" : "Ficheiro gardado") : "Reproducindo en liña";
playingSince = SystemClock.elapsedRealtime(); playingSince = SystemClock.elapsedRealtime();
lastTick = playingSince; lastTick = playingSince;
if (lastSync == 0) lastSync = playingSince; if (lastSync == 0) lastSync = playingSince;
@@ -555,11 +555,11 @@ public class PlayerService extends Service implements MediaPlayer.OnPreparedList
// Old Android's streamer "completes" when the connection drops. Treat that as an error. // Old Android's streamer "completes" when the connection drops. Treat that as an error.
if (!local && dur > 0 && pos < dur - 30000) { if (!local && dur > 0 && pos < dur - 30000) {
lastPosMs = pos; lastPosMs = pos;
retryStream("Connection lost, reconnecting…"); retryStream("Perdeuse a conexión, reconectando…");
return; return;
} }
state = ENDED; state = ENDED;
status = "Finished"; status = "Rematado";
lastPosMs = dur; lastPosMs = dur;
pausedAt = SystemClock.elapsedRealtime(); pausedAt = SystemClock.elapsedRealtime();
wifi.release(); wifi.release();
@@ -573,9 +573,9 @@ public class PlayerService extends Service implements MediaPlayer.OnPreparedList
if (p != mp) return true; if (p != mp) return true;
if (prepared) lastPosMs = positionMs(); if (prepared) lastPosMs = positionMs();
if (!local && contentUrl != null) { if (!local && contentUrl != null) {
retryStream("Connection lost, reconnecting…"); retryStream("Perdeuse a conexión, reconectando…");
} else { } else {
fail("Playback error (" + what + "/" + extra + ")."); fail("Erro de reprodución (" + what + "/" + extra + ").");
} }
return true; return true;
} }
@@ -583,7 +583,7 @@ public class PlayerService extends Service implements MediaPlayer.OnPreparedList
/** Re-opens the stream at the last position with a fresh token. Gives up after three tries. */ /** Re-opens the stream at the last position with a fresh token. Gives up after three tries. */
private void retryStream(final String msg) { private void retryStream(final String msg) {
if (retries >= 3 || contentUrl == null) { if (retries >= 3 || contentUrl == null) {
fail("Lost connection to the server. Press play to try again."); fail("Perdeuse a conexión co servidor. Preme reproducir para tentalo de novo.");
retries = 0; retries = 0;
return; return;
} }
@@ -675,7 +675,7 @@ public class PlayerService extends Service implements MediaPlayer.OnPreparedList
if (sleepAt > 0 && now >= sleepAt) { if (sleepAt > 0 && now >= sleepAt) {
sleepAt = 0; sleepAt = 0;
pause(); pause();
status = "Sleep timer: paused"; status = "Temporizador: en pausa";
} }
} else if (state == PAUSED || state == ENDED || state == ERROR) { } else if (state == PAUSED || state == ENDED || state == ERROR) {
// Free the phone's memory if left paused for a long time. // Free the phone's memory if left paused for a long time.
@@ -756,11 +756,11 @@ public class PlayerService extends Service implements MediaPlayer.OnPreparedList
PendingIntent pi = PendingIntent.getActivity(this, 0, i, PendingIntent.FLAG_UPDATE_CURRENT); PendingIntent pi = PendingIntent.getActivity(this, 0, i, PendingIntent.FLAG_UPDATE_CURRENT);
String what; String what;
switch (state) { switch (state) {
case PLAYING: what = "Playing"; break; case PLAYING: what = "Reproducindo"; break;
case LOADING: what = "Loading…"; break; case LOADING: what = "Cargando…"; break;
case ENDED: what = "Finished"; break; case ENDED: what = "Rematado"; break;
case ERROR: what = "Stopped: tap to retry"; break; case ERROR: what = "Detido: toca para tentalo de novo"; break;
default: what = "Paused"; break; default: what = "En pausa"; break;
} }
n.setLatestEventInfo(this, ep.title, Util.join(what, ep.show), pi); n.setLatestEventInfo(this, ep.title, Util.join(what, ep.show), pi);
if (foreground) { if (foreground) {
+8 -8
View File
@@ -45,7 +45,7 @@ public class PodcastActivity extends BaseActivity {
title.setText(getIntent().getStringExtra("title")); title.setText(getIntent().getStringExtra("title"));
subtitle.setText(getIntent().getStringExtra("author")); subtitle.setText(getIntent().getStringExtra("author"));
Covers.load(this, (ImageView) findViewById(R.id.cover), itemId, Covers.load(this, (ImageView) findViewById(R.id.cover), itemId,
(int) (44 * getResources().getDisplayMetrics().density)); getResources().getDimensionPixelSize(R.dimen.cover_podcast));
Prefs p = Prefs.get(this); Prefs p = Prefs.get(this);
oldestFirst = p.sp.getBoolean("oldest_first", false); oldestFirst = p.sp.getBoolean("oldest_first", false);
@@ -104,7 +104,7 @@ public class PodcastActivity extends BaseActivity {
loading.setVisibility(View.GONE); loading.setVisibility(View.GONE);
episodes = list; episodes = list;
if (showTitle.length() > 0) title.setText(showTitle); if (showTitle.length() > 0) title.setText(showTitle);
String n = list.size() == 1 ? "1 episode" : list.size() + " episodes"; String n = list.size() == 1 ? "1 episodio" : list.size() + " episodios";
subtitle.setText(Util.join(author, n)); subtitle.setText(Util.join(author, n));
render(); render();
} }
@@ -145,10 +145,10 @@ public class PodcastActivity extends BaseActivity {
} }
rows.add(Rows.Row.episode(e)); rows.add(Rows.Row.episode(e));
} }
if (hidden > 0) rows.add(0, Rows.Row.header(hidden + " PLAYED EPISODES HIDDEN")); if (hidden > 0) rows.add(0, Rows.Row.header(hidden + " EPISODIOS ESCOITADOS OCULTOS"));
adapter.set(rows); adapter.set(rows);
boolean none = rows.isEmpty(); boolean none = rows.isEmpty();
empty.setText(none ? "No episodes." : ""); empty.setText(none ? "Non hai episodios." : "");
empty.setVisibility(none ? View.VISIBLE : View.GONE); empty.setVisibility(none ? View.VISIBLE : View.GONE);
} }
@@ -172,10 +172,10 @@ public class PodcastActivity extends BaseActivity {
@Override @Override
public boolean onPrepareOptionsMenu(Menu menu) { public boolean onPrepareOptionsMenu(Menu menu) {
menu.clear(); menu.clear();
menu.add(0, 1, 0, oldestFirst ? "Newest first" : "Oldest first").setIcon(android.R.drawable.ic_menu_sort_by_size); menu.add(0, 1, 0, oldestFirst ? "Máis recentes primeiro" : "Máis antigos primeiro").setIcon(android.R.drawable.ic_menu_sort_by_size);
menu.add(0, 2, 0, hidePlayed ? "Show played" : "Hide played").setIcon(android.R.drawable.ic_menu_view); menu.add(0, 2, 0, hidePlayed ? "Amosar escoitados" : "Ocultar escoitados").setIcon(android.R.drawable.ic_menu_view);
menu.add(0, 3, 0, "Refresh").setIcon(android.R.drawable.ic_menu_rotate); menu.add(0, 3, 0, "Actualizar").setIcon(android.R.drawable.ic_menu_rotate);
menu.add(0, 4, 0, "Now playing").setIcon(android.R.drawable.ic_media_play); menu.add(0, 4, 0, "En reprodución").setIcon(android.R.drawable.ic_media_play);
return true; return true;
} }
+7 -7
View File
@@ -78,7 +78,7 @@ final class Rows extends BaseAdapter {
Rows(Activity a) { Rows(Activity a) {
act = a; act = a;
inf = LayoutInflater.from(a); inf = LayoutInflater.from(a);
coverPx = (int) (40 * a.getResources().getDisplayMetrics().density); coverPx = a.getResources().getDimensionPixelSize(R.dimen.cover_row);
} }
void set(List<Row> list) { void set(List<Row> list) {
@@ -147,7 +147,7 @@ final class Rows extends BaseAdapter {
private void bindShow(View v, Show s) { private void bindShow(View v, Show s) {
((TextView) v.findViewById(R.id.title)).setText(s.title); ((TextView) v.findViewById(R.id.title)).setText(s.title);
String n = s.episodes == 1 ? "1 episode" : s.episodes + " episodes"; String n = s.episodes == 1 ? "1 episodio" : s.episodes + " episodios";
((TextView) v.findViewById(R.id.meta)).setText(Util.join(s.author, n)); ((TextView) v.findViewById(R.id.meta)).setText(Util.join(s.author, n));
Covers.load(act, (ImageView) v.findViewById(R.id.cover), s.id, coverPx); Covers.load(act, (ImageView) v.findViewById(R.id.cover), s.id, coverPx);
} }
@@ -175,21 +175,21 @@ final class Rows extends BaseAdapter {
String m = Util.date(e.published); String m = Util.date(e.published);
if (finished) { if (finished) {
m = Util.join(m, "Played"); m = Util.join(m, "Escoitado");
} else if (started && dur > 0) { } else if (started && dur > 0) {
m = Util.join(m, Util.duration(Math.max(0, dur - cur)) + " left"); m = Util.join(m, "quedan " + Util.duration(Math.max(0, dur - cur)));
} else if (dur > 0) { } else if (dur > 0) {
m = Util.join(m, Util.duration(dur)); m = Util.join(m, Util.duration(dur));
} }
if (DownloadService.busyWith(e.id)) { if (DownloadService.busyWith(e.id)) {
Episode now = DownloadService.current; Episode now = DownloadService.current;
m = Util.join(m, now != null && now.id.equals(e.id) ? "Saving " + DownloadService.percent + "%" : "Queued"); m = Util.join(m, now != null && now.id.equals(e.id) ? "Gardando " + DownloadService.percent + "%" : "Na cola");
} else if (Downloads.has(act, e.id)) { } else if (Downloads.has(act, e.id)) {
m = Util.join(m, "Saved"); m = Util.join(m, "Gardado");
} }
PlayerService ps = PlayerService.get(); PlayerService ps = PlayerService.get();
if (ps != null && ps.episode() != null && ps.episode().id.equals(e.id) && ps.isPlaying()) { if (ps != null && ps.episode() != null && ps.episode().id.equals(e.id) && ps.isPlaying()) {
m = Util.join("Now playing", m); m = Util.join("En reprodución", m);
} }
meta.setText(m); meta.setText(m);
+16 -16
View File
@@ -17,14 +17,14 @@ public class SettingsActivity extends PreferenceActivity {
Preference.OnPreferenceChangeListener skipSummary = new Preference.OnPreferenceChangeListener() { Preference.OnPreferenceChangeListener skipSummary = new Preference.OnPreferenceChangeListener() {
public boolean onPreferenceChange(Preference pref, Object value) { public boolean onPreferenceChange(Preference pref, Object value) {
pref.setSummary(value + " seconds"); pref.setSummary(value + " segundos");
return true; return true;
} }
}; };
ListPreference back = (ListPreference) findPreference("skip_back"); ListPreference back = (ListPreference) findPreference("skip_back");
ListPreference fwd = (ListPreference) findPreference("skip_fwd"); ListPreference fwd = (ListPreference) findPreference("skip_fwd");
back.setSummary(p.skipBack() + " seconds"); back.setSummary(p.skipBack() + " segundos");
fwd.setSummary(p.skipFwd() + " seconds"); fwd.setSummary(p.skipFwd() + " segundos");
back.setOnPreferenceChangeListener(skipSummary); back.setOnPreferenceChangeListener(skipSummary);
fwd.setOnPreferenceChangeListener(skipSummary); fwd.setOnPreferenceChangeListener(skipSummary);
@@ -35,42 +35,42 @@ public class SettingsActivity extends PreferenceActivity {
} }
}); });
String who = p.username().length() > 0 ? p.username() : "API key"; String who = p.username().length() > 0 ? p.username() : "Chave da API";
findPreference("account").setSummary(who + " @ " + p.server()); findPreference("account").setSummary(who + " @ " + p.server());
findPreference("delete_all").setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() { findPreference("delete_all").setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
public boolean onPreferenceClick(Preference pref) { public boolean onPreferenceClick(Preference pref) {
new AlertDialog.Builder(SettingsActivity.this) new AlertDialog.Builder(SettingsActivity.this)
.setMessage("Delete every downloaded episode from the SD card?") .setMessage("Eliminar todos os episodios descargados da tarxeta SD?")
.setPositiveButton("Delete", new DialogInterface.OnClickListener() { .setPositiveButton("Eliminar", new DialogInterface.OnClickListener() {
public void onClick(DialogInterface d, int w) { public void onClick(DialogInterface d, int w) {
PlayerService ps = PlayerService.get(); PlayerService ps = PlayerService.get();
if (ps != null && ps.isLocal()) ps.stop(); if (ps != null && ps.isLocal()) ps.stop();
int n = Downloads.deleteAll(SettingsActivity.this); int n = Downloads.deleteAll(SettingsActivity.this);
Toast.makeText(SettingsActivity.this, n + " deleted", Toast.LENGTH_SHORT).show(); Toast.makeText(SettingsActivity.this, n + " eliminados", Toast.LENGTH_SHORT).show();
} }
}) })
.setNegativeButton("Cancel", null).show(); .setNegativeButton("Cancelar", null).show();
return true; return true;
} }
}); });
final Preference pins = findPreference("pins"); final Preference pins = findPreference("pins");
int n = Trust.pinned(this).size(); int n = Trust.pinned(this).size();
pins.setSummary(n == 0 ? "None" : n + " (tap to forget)"); pins.setSummary(n == 0 ? "Ningún" : n + " (toca para esquecelos)");
pins.setEnabled(n > 0); pins.setEnabled(n > 0);
pins.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() { pins.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
public boolean onPreferenceClick(Preference pref) { public boolean onPreferenceClick(Preference pref) {
new AlertDialog.Builder(SettingsActivity.this) new AlertDialog.Builder(SettingsActivity.this)
.setMessage("Forget the certificates you chose to trust? You'll be asked again next time.") .setMessage("Esquecer os certificados nos que decidiches confiar? Preguntarase de novo a próxima vez.")
.setPositiveButton("Forget", new DialogInterface.OnClickListener() { .setPositiveButton("Esquecer", new DialogInterface.OnClickListener() {
public void onClick(DialogInterface d, int w) { public void onClick(DialogInterface d, int w) {
Trust.forgetPins(SettingsActivity.this); Trust.forgetPins(SettingsActivity.this);
pins.setSummary("None"); pins.setSummary("Ningún");
pins.setEnabled(false); pins.setEnabled(false);
} }
}) })
.setNegativeButton("Cancel", null).show(); .setNegativeButton("Cancelar", null).show();
return true; return true;
} }
}); });
@@ -78,8 +78,8 @@ public class SettingsActivity extends PreferenceActivity {
findPreference("logout").setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() { findPreference("logout").setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
public boolean onPreferenceClick(Preference pref) { public boolean onPreferenceClick(Preference pref) {
new AlertDialog.Builder(SettingsActivity.this) new AlertDialog.Builder(SettingsActivity.this)
.setMessage("Log out of " + p.server() + "? Downloads stay on the SD card.") .setMessage("Pechar sesión en " + p.server() + "? As descargas quedan na tarxeta SD.")
.setPositiveButton("Log out", new DialogInterface.OnClickListener() { .setPositiveButton("Pechar sesión", new DialogInterface.OnClickListener() {
public void onClick(DialogInterface d, int w) { public void onClick(DialogInterface d, int w) {
PlayerService ps = PlayerService.get(); PlayerService ps = PlayerService.get();
if (ps != null) ps.stop(); if (ps != null) ps.stop();
@@ -89,7 +89,7 @@ public class SettingsActivity extends PreferenceActivity {
finish(); // Home sees the logout on resume and opens the login screen finish(); // Home sees the logout on resume and opens the login screen
} }
}) })
.setNegativeButton("Cancel", null).show(); .setNegativeButton("Cancelar", null).show();
return true; return true;
} }
}); });
+9 -10
View File
@@ -7,7 +7,6 @@ import java.security.MessageDigest;
import java.security.cert.CertificateException; import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory; import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate; import java.security.cert.X509Certificate;
import java.text.DateFormat;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collection; import java.util.Collection;
@@ -67,7 +66,7 @@ final class Trust {
ctx.init(null, new TrustManager[]{new Manager(c.getApplicationContext())}, null); ctx.init(null, new TrustManager[]{new Manager(c.getApplicationContext())}, null);
factory = ctx.getSocketFactory(); factory = ctx.getSocketFactory();
} catch (Exception e) { } catch (Exception e) {
throw new IOException("Secure connections aren't available: " + e.getMessage()); throw new IOException("As conexións seguras non están dispoñibles: " + e.getMessage());
} }
} }
return factory; return factory;
@@ -326,15 +325,15 @@ final class Trust {
X509Certificate leaf = r.chain[0]; X509Certificate leaf = r.chain[0];
X509Certificate top = top(r.chain); X509Certificate top = top(r.chain);
StringBuilder sb = new StringBuilder(); StringBuilder sb = new StringBuilder();
sb.append("This phone doesn't recognise the certificate of ").append(host).append(".\n\n"); sb.append("Este teléfono non recoñece o certificado de ").append(host).append(".\n\n");
sb.append("Issued to: ").append(commonName(leaf.getSubjectX500Principal())).append('\n'); sb.append("Emitido para: ").append(commonName(leaf.getSubjectX500Principal())).append('\n');
sb.append("Issued by: ").append(commonName(leaf.getIssuerX500Principal())).append('\n'); sb.append("Emitido por: ").append(commonName(leaf.getIssuerX500Principal())).append('\n');
sb.append("Valid until: ").append(DateFormat.getDateInstance().format(leaf.getNotAfter())).append("\n\n"); sb.append("Válido ata: ").append(Util.date(leaf.getNotAfter().getTime(), true)).append("\n\n");
sb.append("SHA-256 fingerprint:\n").append(fingerprint(leaf)).append("\n\n"); sb.append("Pegada dixital SHA-256:\n").append(fingerprint(leaf)).append("\n\n");
sb.append("Only continue if this matches the fingerprint your computer's browser shows for the site."); sb.append("Continúa só se coincide coa pegada que amosa o navegador do teu ordenador para este sitio.");
if (top != leaf) { if (top != leaf) {
sb.append("\n\nMiniABS will then trust certificates issued by \"") sb.append("\n\nMiniABS confiará desde agora nos certificados emitidos por «")
.append(commonName(top.getSubjectX500Principal())).append("\"."); .append(commonName(top.getSubjectX500Principal())).append("».");
} }
return sb.toString(); return sb.toString();
} }
+4 -4
View File
@@ -24,19 +24,19 @@ final class TrustPrompt {
try { try {
host = new URL(Prefs.get(a).server()).getHost(); host = new URL(Prefs.get(a).server()).getHost();
} catch (Exception e) { } catch (Exception e) {
host = "the server"; host = "o servidor";
} }
showing = true; showing = true;
new AlertDialog.Builder(a) new AlertDialog.Builder(a)
.setTitle("Check this certificate") .setTitle("Comproba este certificado")
.setMessage(Trust.describe(r, host)) .setMessage(Trust.describe(r, host))
.setPositiveButton("Trust", new DialogInterface.OnClickListener() { .setPositiveButton("Confiar", new DialogInterface.OnClickListener() {
public void onClick(DialogInterface d, int w) { public void onClick(DialogInterface d, int w) {
Trust.pin(a, r.chain); Trust.pin(a, r.chain);
if (retry != null) retry.run(); if (retry != null) retry.run();
} }
}) })
.setNegativeButton("Cancel", null) .setNegativeButton("Cancelar", null)
.setOnCancelListener(null) .setOnCancelListener(null)
.show() .show()
.setOnDismissListener(new DialogInterface.OnDismissListener() { .setOnDismissListener(new DialogInterface.OnDismissListener() {
+28 -19
View File
@@ -5,10 +5,7 @@ import java.net.ConnectException;
import java.net.SocketException; import java.net.SocketException;
import java.net.SocketTimeoutException; import java.net.SocketTimeoutException;
import java.net.UnknownHostException; import java.net.UnknownHostException;
import java.text.SimpleDateFormat;
import java.util.Calendar; import java.util.Calendar;
import java.util.Date;
import java.util.Locale;
import javax.net.ssl.SSLException; import javax.net.ssl.SSLException;
@@ -47,12 +44,24 @@ final class Util {
return m + ":" + ss; return m + ":" + ss;
} }
/** Galician month abbreviations (Android 2.3 has no Galician date data of its own). */
private static final String[] MONTHS = {"xan", "feb", "mar", "abr", "mai", "xuñ", "xul", "ago", "set", "out",
"nov", "dec"};
/** "14 out", or "14 out 2025" for other years. */
static String date(long ms) { static String date(long ms) {
if (ms <= 0) return ""; if (ms <= 0) return "";
Calendar now = Calendar.getInstance(), c = Calendar.getInstance(); Calendar now = Calendar.getInstance(), c = Calendar.getInstance();
c.setTimeInMillis(ms); c.setTimeInMillis(ms);
String pattern = c.get(Calendar.YEAR) == now.get(Calendar.YEAR) ? "d MMM" : "d MMM yyyy"; return date(ms, c.get(Calendar.YEAR) != now.get(Calendar.YEAR));
return new SimpleDateFormat(pattern, Locale.getDefault()).format(new Date(ms)); }
static String date(long ms, boolean withYear) {
if (ms <= 0) return "";
Calendar c = Calendar.getInstance();
c.setTimeInMillis(ms);
String d = c.get(Calendar.DAY_OF_MONTH) + " " + MONTHS[c.get(Calendar.MONTH)];
return withYear ? d + " " + c.get(Calendar.YEAR) : d;
} }
static String plainText(String html) { static String plainText(String html) {
@@ -87,28 +96,28 @@ final class Util {
if (e instanceof Api.AuthException) return e.getMessage(); if (e instanceof Api.AuthException) return e.getMessage();
if (e instanceof Api.ApiException) { if (e instanceof Api.ApiException) {
Api.ApiException a = (Api.ApiException) e; Api.ApiException a = (Api.ApiException) e;
if (a.code == 401) return "Not signed in. Please log in again."; if (a.code == 401) return "Non iniciaches sesión. Inicia sesión de novo.";
if (a.code == 403) return "Your account isn't allowed to do that."; if (a.code == 403) return "A túa conta non ten permiso para facer iso.";
if (a.code == 404) return "Not found on the server."; if (a.code == 404) return "Non se atopou no servidor.";
String m = a.getMessage(); String m = a.getMessage();
return "Server error " + a.code + (m != null && m.length() > 0 ? ": " + m : ""); return "Erro do servidor " + a.code + (m != null && m.length() > 0 ? ": " + m : "");
} }
if (e instanceof UnknownHostException) return "Server not found. Check the address and your connection."; if (e instanceof UnknownHostException) return "Non se atopou o servidor. Comproba o enderezo e a conexión.";
if (e instanceof SocketTimeoutException) return "The server took too long to answer."; if (e instanceof SocketTimeoutException) return "O servidor tardou demasiado en responder.";
if (e instanceof ConnectException) return "Can't connect to the server. Is it running and reachable?"; if (e instanceof ConnectException) return "Non se pode conectar co servidor. Está en marcha e accesible?";
if (e instanceof SSLException) { if (e instanceof SSLException) {
Trust.Rejected r = Trust.recentRejection(); Trust.Rejected r = Trust.recentRejection();
if (r != null) { if (r != null) {
return r.expired() ? "The server's certificate has expired, or the phone's date is wrong." return r.expired() ? "O certificado do servidor caducou ou a data do teléfono é incorrecta."
: "This phone doesn't recognise the server's certificate."; : "Este teléfono non recoñece o certificado do servidor.";
} }
return "Couldn't set up a secure connection. Android 2.3 only speaks TLS 1.0, so the server needs " return "Non se puido establecer unha conexión segura. Android 2.3 só admite TLS 1.0, así que o servidor "
+ "a legacy HTTPS port (see README). On your home network, the http:// address works too."; + "precisa un porto HTTPS antigo (ve o README). Na rede da casa tamén funciona o enderezo http://.";
} }
if (e instanceof SocketException) return "Connection dropped. Check your network."; if (e instanceof SocketException) return "Cortouse a conexión. Comproba a rede.";
if (e instanceof FileNotFoundException) return "Not found on the server."; if (e instanceof FileNotFoundException) return "Non se atopou no servidor.";
String m = e.getMessage(); String m = e.getMessage();
if (m != null && m.contains("not verified")) return "The server's certificate is for a different address."; if (m != null && m.contains("not verified")) return "O certificado do servidor é doutro enderezo.";
return m == null || m.length() == 0 ? e.getClass().getSimpleName() : m; return m == null || m.length() == 0 ? e.getClass().getSimpleName() : m;
} }
} }
+1 -1
View File
@@ -55,7 +55,7 @@ public class ApiTest {
Api.login(c, p.server(), "demo", "wrong"); Api.login(c, p.server(), "demo", "wrong");
check(false, "wrong password rejected"); check(false, "wrong password rejected");
} catch (Api.ApiException e) { } catch (Api.ApiException e) {
check(e.code == 401 && e.getMessage().contains("Wrong"), "wrong password rejected with a clear message"); check(e.code == 401 && e.getMessage().contains("contrasinal incorrectos"), "wrong password rejected with a clear message");
} }
String lib = Api.login(c, p.server(), "demo", "pw"); String lib = Api.login(c, p.server(), "demo", "pw");
check(lib.equals("lib-pod"), "login returns default library"); check(lib.equals("lib-pod"), "login returns default library");
+4 -4
View File
@@ -88,13 +88,13 @@ public class TrustTest {
Exception e = tryStatus(good); Exception e = tryStatus(good);
Trust.Rejected r = Trust.recentRejection(); Trust.Rejected r = Trust.recentRejection();
check(e instanceof javax.net.ssl.SSLException && r != null, "unrecognised certificate rejected and remembered"); check(e instanceof javax.net.ssl.SSLException && r != null, "unrecognised certificate rejected and remembered");
check(Util.describe(e).equals("This phone doesn't recognise the server's certificate."), check(Util.describe(e).equals("Este teléfono non recoñece o certificado do servidor."),
"error message: \"" + Util.describe(e) + "\""); "error message: \"" + Util.describe(e) + "\"");
String text = Trust.describe(r, "localhost"); String text = Trust.describe(r, "localhost");
byte[] d = MessageDigest.getInstance("SHA-256").digest(leaf.getEncoded()); byte[] d = MessageDigest.getInstance("SHA-256").digest(leaf.getEncoded());
String fp = String.format("%02X:%02X:%02X", d[0], d[1], d[2]); String fp = String.format("%02X:%02X:%02X", d[0], d[1], d[2]);
check(text.contains(fp) && text.contains("Issued by: Test Intermediate R1") check(text.contains(fp) && text.contains("Emitido por: Test Intermediate R1")
&& text.contains("trust certificates issued by \"Test Intermediate R1\""), && text.contains("certificados emitidos por «Test Intermediate R1»"),
"prompt shows the server's fingerprint and what will be trusted"); "prompt shows the server's fingerprint and what will be trusted");
X509Certificate pinned = Trust.pin(c, r.chain); X509Certificate pinned = Trust.pin(c, r.chain);
check(pinned.equals(inter), "trusting pins the issuing CA, so renewals keep working"); check(pinned.equals(inter), "trusting pins the issuing CA, so renewals keep working");
@@ -145,7 +145,7 @@ public class TrustTest {
check(tryStatus(forged) != null, "...and still rejected even if the user taps Trust"); check(tryStatus(forged) != null, "...and still rejected even if the user taps Trust");
Exception ee = tryStatus(expired); Exception ee = tryStatus(expired);
check(ee != null && Trust.recentRejection() != null && Trust.recentRejection().expired() check(ee != null && Trust.recentRejection() != null && Trust.recentRejection().expired()
&& Util.describe(ee).contains("expired"), && Util.describe(ee).contains("caducou"),
"expired certificate rejected: \"" + Util.describe(ee) + "\""); "expired certificate rejected: \"" + Util.describe(ee) + "\"");
System.out.println("Self-signed server"); System.out.println("Self-signed server");
+5 -5
View File
@@ -88,9 +88,9 @@ def refresh(px, folder):
for folder, k in DENS.items(): for folder, k in DENS.items():
launcher(round(48 * k), folder) launcher(round(48 * k), folder)
status(round(25 * k), folder) status(round(25 * k), folder)
play(round(40 * k), folder, "ic_play", DARK) play(round(48 * k), folder, "ic_play", DARK)
pause(round(40 * k), folder, "ic_pause", DARK) pause(round(48 * k), folder, "ic_pause", DARK)
play(round(28 * k), folder, "ic_play_small", AMBER) play(round(38 * k), folder, "ic_play_small", AMBER)
pause(round(28 * k), folder, "ic_pause_small", AMBER) pause(round(38 * k), folder, "ic_pause_small", AMBER)
refresh(round(24 * k), folder) refresh(round(30 * k), folder)
print("icons ok") print("icons ok")
+260
View File
@@ -0,0 +1,260 @@
"""One-off: translates MiniABS's interface strings to Galician in place.
Each (file, old, new) must match at least once, so nothing is silently missed."""
import os
import re
import sys
ROOT = os.path.join(os.path.dirname(__file__), "..")
S = "src/com/miniabs/"
R = [
# ---------------- layouts / xml
("res/layout/activity_login.xml", 'android:text="Audiobookshelf podcasts"', 'android:text="Podcasts de Audiobookshelf"'),
("res/layout/activity_login.xml", 'android:text="Server address"', 'android:text="Enderezo do servidor"'),
("res/layout/activity_login.xml", 'android:text="Username"', 'android:text="Usuario"'),
("res/layout/activity_login.xml", 'android:text="Password"', 'android:text="Contrasinal"'),
("res/layout/activity_login.xml", 'android:text="Use an API key instead"', 'android:text="Usar unha chave da API"'),
("res/layout/activity_login.xml", 'android:text="Connect"', 'android:text="Conectar"'),
("res/layout/activity_login.xml",
"android:text=\"At home, use your server's local http:// address. For https://, Android 2.3 needs the server to offer TLS 1.0 on a separate port (see README). If the certificate isn't recognised, you'll be shown its fingerprint to check.\"",
'android:text="Na casa, usa o enderezo local http:// do servidor. Para https://, Android 2.3 precisa que o servidor ofreza TLS 1.0 nun porto á parte (ve o README). Se o certificado non se recoñece, amosaráseche a súa pegada dixital para que a comprobes."'),
("res/layout/activity_home.xml", 'android:text="Latest"', 'android:text="Últimos"'),
("res/layout/activity_home.xml", 'android:text="Shows"', 'android:text="Programas"'),
("res/layout/activity_home.xml", 'android:text="Saved"', 'android:text="Gardados"'),
("res/xml/settings.xml", 'android:title="Playback"', 'android:title="Reprodución"'),
("res/xml/settings.xml", 'android:title="Skip back"', 'android:title="Retroceder"'),
("res/xml/settings.xml", 'android:title="Skip forward"', 'android:title="Avanzar"'),
("res/xml/settings.xml", 'android:title="Downloads"', 'android:title="Descargas"'),
("res/xml/settings.xml", 'android:title="Download on Wi-Fi only"', 'android:title="Descargar só con wifi"'),
("res/xml/settings.xml", 'android:summary="Saves your mobile data"', 'android:summary="Aforra datos móbiles"'),
("res/xml/settings.xml", 'android:title="Delete all downloads"', 'android:title="Eliminar todas as descargas"'),
("res/xml/settings.xml", 'android:title="Display"', 'android:title="Aparencia"'),
("res/xml/settings.xml", 'android:title="Show cover art"', 'android:title="Amosar portadas"'),
("res/xml/settings.xml", 'android:summary="Turn off to save data and memory"', 'android:summary="Desactívao para aforrar datos e memoria"'),
("res/xml/settings.xml", 'android:title="Account"', 'android:title="Conta"'),
("res/xml/settings.xml", 'android:title="Signed in"', 'android:title="Sesión iniciada"'),
("res/xml/settings.xml", 'android:title="Certificates you trusted"', 'android:title="Certificados nos que confías"'),
("res/xml/settings.xml", 'android:title="Log out"', 'android:title="Pechar sesión"'),
("res/xml/settings.xml", 'android:title="About"', 'android:title="Acerca de"'),
("res/xml/settings.xml", 'android:summary="A tiny Audiobookshelf podcast client for Android 2.2+"',
'android:summary="Un pequeno cliente de podcasts de Audiobookshelf para Android 2.2+"'),
("res/values/arrays.xml", " seconds</item>", " segundos</item>"),
("AndroidManifest.xml", 'android:label="Settings"', 'android:label="Axustes"'),
# ---------------- Api / Json / Trust (errors)
(S + "Api.java", '"Unexpected reply from server"', '"Resposta inesperada do servidor"'),
(S + "Json.java", '"Unexpected reply from server"', '"Resposta inesperada do servidor"'),
(S + "Json.java", '"Reply was cut off"', '"A resposta chegou incompleta"'),
(S + "Api.java", '"Your sign-in has expired. Please log in again."', '"A túa sesión caducou. Inicia sesión de novo."'),
(S + "Api.java", '"The server redirected; check the address in Settings."', '"O servidor redirixiu a petición; comproba o enderezo do servidor."'),
(S + "Api.java", '"Please log in."', '"Inicia sesión."'),
(S + "Api.java", '"No Audiobookshelf server at this address."', '"Non hai ningún servidor de Audiobookshelf neste enderezo."'),
(S + "Api.java", "\"This server hasn't been set up yet. Finish setup in a browser first.\"",
'"Este servidor aínda non está configurado. Remata a configuración nun navegador."'),
(S + "Api.java", '"Wrong username or password."', '"Usuario ou contrasinal incorrectos."'),
(S + "Api.java", '"Too many attempts. Wait a minute and try again."', '"Demasiados intentos. Agarda un minuto e téntao de novo."'),
(S + "Api.java", "\"The server didn't return a sign-in token.\"", '"O servidor non devolveu ningún token de sesión."'),
(S + "Api.java", "\"That API key wasn't accepted.\"", '"Non se aceptou esa chave da API."'),
(S + "Api.java", "\"Certificate doesn't match \"", '"O certificado non corresponde a "'),
(S + "Trust.java", "\"Secure connections aren't available: \"", '"As conexións seguras non están dispoñibles: "'),
(S + "Trust.java", '"This phone doesn\'t recognise the certificate of "', '"Este teléfono non recoñece o certificado de "'),
(S + "Trust.java", '"Issued to: "', '"Emitido para: "'),
(S + "Trust.java", '"Issued by: "', '"Emitido por: "'),
(S + "Trust.java", '"Valid until: "', '"Válido ata: "'),
(S + "Trust.java", '"SHA-256 fingerprint:\\n"', '"Pegada dixital SHA-256:\\n"'),
(S + "Trust.java", "\"Only continue if this matches the fingerprint your computer's browser shows for the site.\"",
'"Continúa só se coincide coa pegada que amosa o navegador do teu ordenador para este sitio."'),
(S + "Trust.java", '"\\n\\nMiniABS will then trust certificates issued by \\""', '"\\n\\nMiniABS confiará desde agora nos certificados emitidos por «"'),
(S + "Trust.java", '.append("\\".");', '.append("».");'),
(S + "Trust.java", "DateFormat.getDateInstance().format(leaf.getNotAfter())", "Util.date(leaf.getNotAfter().getTime(), true)"),
(S + "Trust.java", "import java.text.DateFormat;\n", ""),
(S + "TrustPrompt.java", '"the server"', '"o servidor"'),
(S + "TrustPrompt.java", '"Check this certificate"', '"Comproba este certificado"'),
(S + "TrustPrompt.java", '"Trust"', '"Confiar"'),
(S + "TrustPrompt.java", '"Cancel"', '"Cancelar"'),
# ---------------- Util
(S + "Util.java", '"Not signed in. Please log in again."', '"Non iniciaches sesión. Inicia sesión de novo."'),
(S + "Util.java", "\"Your account isn't allowed to do that.\"", '"A túa conta non ten permiso para facer iso."'),
(S + "Util.java", '"Not found on the server."', '"Non se atopou no servidor."'),
(S + "Util.java", '"Server error "', '"Erro do servidor "'),
(S + "Util.java", '"Server not found. Check the address and your connection."', '"Non se atopou o servidor. Comproba o enderezo e a conexión."'),
(S + "Util.java", '"The server took too long to answer."', '"O servidor tardou demasiado en responder."'),
(S + "Util.java", "\"Can't connect to the server. Is it running and reachable?\"",
'"Non se pode conectar co servidor. Está en marcha e accesible?"'),
(S + "Util.java", "\"The server's certificate has expired, or the phone's date is wrong.\"",
'"O certificado do servidor caducou ou a data do teléfono é incorrecta."'),
(S + "Util.java", "\"This phone doesn't recognise the server's certificate.\"", '"Este teléfono non recoñece o certificado do servidor."'),
(S + "Util.java", "\"Couldn't set up a secure connection. Android 2.3 only speaks TLS 1.0, so the server needs \"\n + \"a legacy HTTPS port (see README). On your home network, the http:// address works too.\"",
'"Non se puido establecer unha conexión segura. Android 2.3 só admite TLS 1.0, así que o servidor "\n + "precisa un porto HTTPS antigo (ve o README). Na rede da casa tamén funciona o enderezo http://."'),
(S + "Util.java", '"Connection dropped. Check your network."', '"Cortouse a conexión. Comproba a rede."'),
(S + "Util.java", "\"The server's certificate is for a different address.\"", '"O certificado do servidor é doutro enderezo."'),
# ---------------- Downloads / DownloadService
(S + "Downloads.java", '"No SD card available. Insert or unmount it from USB to download."',
'"Non hai tarxeta SD dispoñible. Insírea ou desconéctaa do USB para descargar."'),
(S + "Downloads.java", "\"Can't use the SD card.\"", '"Non se pode usar a tarxeta SD."'),
(S + "Downloads.java", "\"Can't create a folder on the SD card.\"", '"Non se pode crear un cartafol na tarxeta SD."'),
(S + "DownloadService.java", '"Download failed: "', '"Fallou a descarga: "'),
(S + "DownloadService.java", '"not on Wi-Fi (Settings: Download on Wi-Fi only)"', '"non hai wifi (Axustes: Descargar só con wifi)"'),
(S + "DownloadService.java", '"this episode has no audio file"', '"este episodio non ten ficheiro de audio"'),
(S + "DownloadService.java", '"sign-in expired; log in again"', '"a sesión caducou; inicia sesión de novo"'),
(S + "DownloadService.java", "\"your account isn't allowed to download\"", '"a túa conta non ten permiso para descargar"'),
(S + "DownloadService.java", "\"couldn't save the file\"", '"non se puido gardar o ficheiro"'),
(S + "DownloadService.java", '"Downloading " + pct + "%"', '"Descargando " + pct + " %"'),
(S + "DownloadService.java", '" more queued"', '" máis na cola"'),
# ---------------- BaseActivity
(S + "BaseActivity.java", '"Certificate trusted. Try again."', '"Certificado aceptado. Téntao de novo."'),
(S + "BaseActivity.java", 'Util.clock(dur - pos) + " left"', '"quedan " + Util.clock(dur - pos)'),
(S + "BaseActivity.java", '"Paused"', '"En pausa"'),
(S + "BaseActivity.java", '"Play"', '"Reproducir"'),
(S + "BaseActivity.java", '"Cancel download"', '"Cancelar descarga"'),
(S + "BaseActivity.java", '"Delete download"', '"Eliminar descarga"'),
(S + "BaseActivity.java", '"Download"', '"Descargar"'),
(S + "BaseActivity.java", '"Mark as unplayed"', '"Marcar como non escoitado"'),
(S + "BaseActivity.java", '"Mark as played"', '"Marcar como escoitado"'),
(S + "BaseActivity.java", '"Go to show"', '"Ir ao programa"'),
(S + "BaseActivity.java", "\"You're not on Wi-Fi. Download over mobile data this time?\"",
'"Non estás nunha wifi. Descargar con datos móbiles esta vez?"'),
(S + "BaseActivity.java", '"Downloading. Wi-Fi-only is now off (change it in Settings)."',
'"Descargando. Desactivouse «só con wifi» (podes cambialo en Axustes)."'),
(S + "BaseActivity.java", '"Cancel"', '"Cancelar"'),
(S + "BaseActivity.java", '"Downloading…"', '"Descargando…"'),
(S + "BaseActivity.java", '"Download cancelled"', '"Descarga cancelada"'),
(S + "BaseActivity.java", '"Download deleted"', '"Descarga eliminada"'),
(S + "BaseActivity.java", '"Marked as played"', '"Marcado como escoitado"'),
(S + "BaseActivity.java", '"Marked as unplayed"', '"Marcado como non escoitado"'),
# ---------------- HomeActivity
(S + "HomeActivity.java", '"Synced offline progress"', '"Sincronizouse o progreso gardado sen conexión"'),
(S + "HomeActivity.java", '"This server has no podcast libraries."', '"Este servidor non ten bibliotecas de podcasts."'),
(S + "HomeActivity.java", '"CONTINUE LISTENING"', '"CONTINUAR ESCOITANDO"'),
(S + "HomeActivity.java", '"LATEST EPISODES"', '"ÚLTIMOS EPISODIOS"'),
(S + "HomeActivity.java", '"Load more"', '"Cargar máis"'),
(S + "HomeActivity.java", '"No episodes yet."', '"Aínda non hai episodios."'),
(S + "HomeActivity.java", '"\\n\\nTap refresh to try again."', '"\\n\\nToca actualizar para tentalo de novo."'),
(S + "HomeActivity.java", '"No podcasts in this library."', '"Non hai podcasts nesta biblioteca."'),
(S + "HomeActivity.java", "\"Can't reach the server. Showing saved episodes.\"",
'"Non se pode acceder ao servidor. Amósanse os episodios gardados."'),
(S + "HomeActivity.java", '"DOWNLOADING"', '"DESCARGANDO"'),
(S + "HomeActivity.java", '"SAVED ON SD CARD ("', '"GARDADOS NA TARXETA SD ("'),
(S + "HomeActivity.java", '"Nothing saved yet.\\n\\nLong-press an episode and choose Download to listen offline."',
'"Aínda non gardaches nada.\\n\\nMantén premido un episodio e escolle Descargar para escoitalo sen conexión."'),
(S + "HomeActivity.java", '"Loading…"', '"Cargando…"'),
(S + "HomeActivity.java", '"Still loading libraries…"', '"Aínda se están cargando as bibliotecas…"'),
(S + "HomeActivity.java", '"This is your only podcast library."', '"Esta é a túa única biblioteca de podcasts."'),
(S + "HomeActivity.java", '"Podcast library"', '"Biblioteca de podcasts"'),
(S + "HomeActivity.java", '"Refresh"', '"Actualizar"'),
(S + "HomeActivity.java", '"Library"', '"Biblioteca"'),
(S + "HomeActivity.java", '"Now playing"', '"En reprodución"'),
(S + "HomeActivity.java", '"Settings"', '"Axustes"'),
# ---------------- LoginActivity
(S + "LoginActivity.java", '"API key"', '"Chave da API"'),
(S + "LoginActivity.java", '"Password"', '"Contrasinal"'),
(S + "LoginActivity.java", '"Enter your server address."', '"Escribe o enderezo do servidor."'),
(S + "LoginActivity.java", '"Paste your API key."', '"Pega a túa chave da API."'),
(S + "LoginActivity.java", '"Enter your username."', '"Escribe o teu usuario."'),
(S + "LoginActivity.java", '"Connecting…"', '"Conectando…"'),
# ---------------- PlayerActivity
(S + "PlayerActivity.java", '"Nothing playing"', '"Non se está a reproducir nada"'),
(S + "PlayerActivity.java", '"Pick an episode from the list."', '"Escolle un episodio da lista."'),
(S + "PlayerActivity.java", '"Sleep at end"', '"Pausa ao final do episodio"'),
(S + "PlayerActivity.java", '"Sleep in "', '"Pausa en "'),
(S + "PlayerActivity.java", '"Connecting…"', '"Conectando…"'),
(S + "PlayerActivity.java", '"Saved on SD card"', '"Gardado na tarxeta SD"'),
(S + "PlayerActivity.java", '"Sleep timer"', '"Temporizador de pausa"'),
(S + "PlayerActivity.java", '"Mark as played"', '"Marcar como escoitado"'),
(S + "PlayerActivity.java", '"Cancel download"', '"Cancelar descarga"'),
(S + "PlayerActivity.java", '"Delete download"', '"Eliminar descarga"'),
(S + "PlayerActivity.java", '"Download"', '"Descargar"'),
(S + "PlayerActivity.java", '"Go to show"', '"Ir ao programa"'),
(S + "PlayerActivity.java", '"Stop"', '"Deter"'),
(S + "PlayerActivity.java", '"Marked as played"', '"Marcado como escoitado"'),
(S + "PlayerActivity.java", '"Off"', '"Desactivado"'),
(S + "PlayerActivity.java", ' minutes"', ' minutos"'),
(S + "PlayerActivity.java", '"1 hour"', '"1 hora"'),
(S + "PlayerActivity.java", '"End of episode"', '"Ao final do episodio"'),
(S + "PlayerActivity.java", '"Sleep timer off"', '"Temporizador desactivado"'),
(S + "PlayerActivity.java", '"Sleep timer set"', '"Temporizador activado"'),
# ---------------- PlayerService
(S + "PlayerService.java", '"Paused"', '"En pausa"'),
(S + "PlayerService.java", '"Marked as played"', '"Marcado como escoitado"'),
(S + "PlayerService.java", '"Connecting…"', '"Conectando…"'),
(S + "PlayerService.java", '"The server sent no audio for this episode."', '"O servidor non enviou audio para este episodio."'),
(S + "PlayerService.java", "\"This episode needs transcoding, which Android 2.x can't play. Try downloading it.\"",
'"Este episodio precisa transcodificación, que Android 2.x non pode reproducir. Proba a descargalo."'),
(S + "PlayerService.java", '"Offline · progress will sync later"', '"Sen conexión · o progreso sincronizarase despois"'),
(S + "PlayerService.java", '"Opening saved file…"', '"Abrindo o ficheiro gardado…"'),
(S + "PlayerService.java", '"Buffering…"', '"Cargando…"'),
(S + "PlayerService.java", "\"Can't open the audio: \"", '"Non se pode abrir o audio: "'),
(S + "PlayerService.java", '"Playback failed: "', '"Fallou a reprodución: "'),
(S + "PlayerService.java", '"Saved file · offline"', '"Ficheiro gardado · sen conexión"'),
(S + "PlayerService.java", '"Saved file"', '"Ficheiro gardado"'),
(S + "PlayerService.java", '"Streaming"', '"Reproducindo en liña"'),
(S + "PlayerService.java", '"Connection lost, reconnecting…"', '"Perdeuse a conexión, reconectando…"'),
(S + "PlayerService.java", '"Finished"', '"Rematado"'),
(S + "PlayerService.java", '"Playback error ("', '"Erro de reprodución ("'),
(S + "PlayerService.java", '"Lost connection to the server. Press play to try again."',
'"Perdeuse a conexión co servidor. Preme reproducir para tentalo de novo."'),
(S + "PlayerService.java", '"Sleep timer: paused"', '"Temporizador: en pausa"'),
(S + "PlayerService.java", '"Playing"', '"Reproducindo"'),
(S + "PlayerService.java", '"Loading…"', '"Cargando…"'),
(S + "PlayerService.java", '"Stopped: tap to retry"', '"Detido: toca para tentalo de novo"'),
# ---------------- PodcastActivity
(S + "PodcastActivity.java", '"1 episode"', '"1 episodio"'),
(S + "PodcastActivity.java", '" episodes"', '" episodios"'),
(S + "PodcastActivity.java", '" PLAYED EPISODES HIDDEN"', '" EPISODIOS ESCOITADOS OCULTOS"'),
(S + "PodcastActivity.java", '"No episodes."', '"Non hai episodios."'),
(S + "PodcastActivity.java", '"Newest first"', '"Máis recentes primeiro"'),
(S + "PodcastActivity.java", '"Oldest first"', '"Máis antigos primeiro"'),
(S + "PodcastActivity.java", '"Show played"', '"Amosar escoitados"'),
(S + "PodcastActivity.java", '"Hide played"', '"Ocultar escoitados"'),
(S + "PodcastActivity.java", '"Refresh"', '"Actualizar"'),
(S + "PodcastActivity.java", '"Now playing"', '"En reprodución"'),
# ---------------- Rows
(S + "Rows.java", '"1 episode"', '"1 episodio"'),
(S + "Rows.java", '" episodes"', '" episodios"'),
(S + "Rows.java", '"Played"', '"Escoitado"'),
(S + "Rows.java", 'Util.duration(Math.max(0, dur - cur)) + " left"', '"quedan " + Util.duration(Math.max(0, dur - cur))'),
(S + "Rows.java", '"Saving "', '"Gardando "'),
(S + "Rows.java", '"Queued"', '"Na cola"'),
(S + "Rows.java", '"Saved"', '"Gardado"'),
(S + "Rows.java", '"Now playing"', '"En reprodución"'),
# ---------------- SettingsActivity
(S + "SettingsActivity.java", '" seconds"', '" segundos"'),
(S + "SettingsActivity.java", '"API key"', '"Chave da API"'),
(S + "SettingsActivity.java", '"Delete every downloaded episode from the SD card?"',
'"Eliminar todos os episodios descargados da tarxeta SD?"'),
(S + "SettingsActivity.java", '"Delete"', '"Eliminar"'),
(S + "SettingsActivity.java", '" deleted"', '" eliminados"'),
(S + "SettingsActivity.java", '"Cancel"', '"Cancelar"'),
(S + "SettingsActivity.java", '"None"', '"Ningún"'),
(S + "SettingsActivity.java", '" (tap to forget)"', '" (toca para esquecelos)"'),
(S + "SettingsActivity.java", "\"Forget the certificates you chose to trust? You'll be asked again next time.\"",
'"Esquecer os certificados nos que decidiches confiar? Volverase preguntar a próxima vez."'),
(S + "SettingsActivity.java", '"Forget"', '"Esquecer"'),
(S + "SettingsActivity.java", '"Log out of "', '"Pechar sesión en "'),
(S + "SettingsActivity.java", '"? Downloads stay on the SD card."', '"? As descargas quedan na tarxeta SD."'),
(S + "SettingsActivity.java", '"Log out"', '"Pechar sesión"'),
]
missing = []
for f, old, new in R:
p = os.path.join(ROOT, f)
s = open(p, encoding="utf-8").read()
if old not in s:
missing.append((f, old[:70]))
continue
open(p, "w", encoding="utf-8").write(s.replace(old, new))
if missing:
for m in missing:
print("NOT FOUND", m)
sys.exit(1)
print(len(R), "replacements applied")