3 Commits
Author SHA1 Message Date
xockoandClaude Opus 5.5 2c23ec9131 MiniABS 1.5: Audiobookshelf icon and purple theme
- Launcher icon from the Audiobookshelf icon pack (art/icon_master_512.png),
  including an xhdpi size
- Notification icon cut out of the same artwork; player icons redrawn in the
  icon's lilac (tools/make_icons.py)
- Purple colour scheme taken from the icon: gradient accent buttons, lilac
  text accents (accent_light), purple-tinted backgrounds
- README: where the icon and colours come from

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 10:07:49 +02:00
xockoandClaude Opus 5.5 0fea07f1b2 MiniABS 1.4: smaller player controls
- Play/pause becomes a rounded rectangle (58x46dp) instead of a 78dp circle,
  with 32dp icons and wider spacing (play_w/play_h/play_gap in dimens.xml)
- Skip buttons shrink from 82x56dp to 62x42dp, with 16sp text
- Version 1.4 (versionCode 5); there was no public 1.3

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 08:02:53 +02:00
xockoandClaude Opus 5.5 275bee3c11 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>
2026-10-14 21:29:17 +02:00
62 changed files with 765 additions and 426 deletions

No files matched your search

+3 -3
View File
@@ -1,8 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.miniabs"
android:versionCode="2"
android:versionName="1.1"
android:versionCode="6"
android:versionName="1.5"
android:installLocation="auto">
<uses-sdk android:minSdkVersion="8" android:targetSdkVersion="10" />
@@ -54,7 +54,7 @@
<activity
android:name=".SettingsActivity"
android:label="Settings"
android:label="Axustes"
android:theme="@android:style/Theme"
android:screenOrientation="portrait"
android:configChanges="keyboardHidden|orientation" />
+3 -1
View File
@@ -1,6 +1,6 @@
# 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. Its purple colour scheme and launcher icon come from the Audiobookshelf icon pack (`art/icon_master_512.png`); `tools/make_icons.py` derives the notification and player icons from it.
## 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.
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.
`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.

After

Width:  |  Height:  |  Size: 224 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 499 B

After

Width:  |  Height:  |  Size: 272 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 346 B

After

Width:  |  Height:  |  Size: 533 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 650 B

After

Width:  |  Height:  |  Size: 588 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 880 B

After

Width:  |  Height:  |  Size: 1.0 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: 690 B

After

Width:  |  Height:  |  Size: 977 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 381 B

After

Width:  |  Height:  |  Size: 238 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 349 B

After

Width:  |  Height:  |  Size: 434 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 470 B

After

Width:  |  Height:  |  Size: 393 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 527 B

After

Width:  |  Height:  |  Size: 647 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: 326 B

After

Width:  |  Height:  |  Size: 434 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 408 B

After

Width:  |  Height:  |  Size: 301 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 413 B

After

Width:  |  Height:  |  Size: 494 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 513 B

After

Width:  |  Height:  |  Size: 376 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 656 B

After

Width:  |  Height:  |  Size: 806 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 463 B

After

Width:  |  Height:  |  Size: 699 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 457 B

After

Width:  |  Height:  |  Size: 567 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

+2 -2
View File
@@ -1,12 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_pressed="true">
<shape><solid android:color="#FFC766" /><corners android:radius="6dp" /></shape>
<shape><gradient android:startColor="#AA5CFF" android:endColor="#7022E6" android:angle="270" /><corners android:radius="6dp" /></shape>
</item>
<item android:state_enabled="false">
<shape><solid android:color="@color/dim" /><corners android:radius="6dp" /></shape>
</item>
<item>
<shape><solid android:color="@color/accent" /><corners android:radius="6dp" /></shape>
<shape><gradient android:startColor="@color/accent_top" android:endColor="@color/accent_bottom" android:angle="270" /><corners android:radius="6dp" /></shape>
</item>
</selector>
+1 -1
View File
@@ -4,7 +4,7 @@
<shape>
<solid android:color="@color/pressed" />
<corners android:radius="6dp" />
<stroke android:width="1dp" android:color="@color/accent" />
<stroke android:width="1dp" android:color="@color/accent_light" />
</shape>
</item>
<item android:state_enabled="false">
+2 -2
View File
@@ -1,9 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_pressed="true">
<shape android:shape="oval"><solid android:color="#FFC766" /></shape>
<shape><gradient android:startColor="#AA5CFF" android:endColor="#7022E6" android:angle="270" /><corners android:radius="23dp" /></shape>
</item>
<item>
<shape android:shape="oval"><solid android:color="@color/accent" /></shape>
<shape><gradient android:startColor="@color/accent_top" android:endColor="@color/accent_bottom" android:angle="270" /><corners android:radius="23dp" /></shape>
</item>
</selector>
+3 -2
View File
@@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="oval">
<solid android:color="@color/accent" />
<size android:width="14dp" android:height="14dp" />
<solid android:color="@color/accent_light" />
<stroke android:width="2dp" android:color="@color/bg" />
<size android:width="@dimen/seek_thumb" android:height="@dimen/seek_thumb" />
</shape>
+9 -9
View File
@@ -10,9 +10,9 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="MiniABS"
android:textSize="15sp"
android:textSize="@dimen/header_text"
android:textStyle="bold"
android:textColor="@color/accent" />
android:textColor="@color/accent_light" />
<TextView
android:id="@+id/library"
@@ -20,8 +20,8 @@
android:layout_height="fill_parent"
android:layout_weight="1"
android:gravity="center_vertical"
android:paddingLeft="8dp"
android:textSize="11sp"
android:paddingLeft="10dp"
android:textSize="@dimen/header_sub"
android:textColor="@color/sub"
android:singleLine="true"
android:ellipsize="end"
@@ -29,7 +29,7 @@
<ImageButton
android:id="@+id/refresh"
android:layout_width="36dp"
android:layout_width="@dimen/header_h"
android:layout_height="fill_parent"
android:background="@drawable/row_sel"
android:src="@drawable/ic_refresh" />
@@ -37,12 +37,12 @@
<LinearLayout
android:layout_width="fill_parent"
android:layout_height="32dp"
android:layout_height="@dimen/tab_h"
android:orientation="horizontal">
<TextView android:id="@+id/tab_latest" style="@style/Tab" android:text="Latest" />
<TextView android:id="@+id/tab_shows" style="@style/Tab" android:text="Shows" />
<TextView android:id="@+id/tab_saved" style="@style/Tab" android:text="Saved" />
<TextView android:id="@+id/tab_latest" style="@style/Tab" android:text="Últimos" />
<TextView android:id="@+id/tab_shows" style="@style/Tab" android:text="Programas" />
<TextView android:id="@+id/tab_saved" style="@style/Tab" android:text="Gardados" />
</LinearLayout>
<include layout="@layout/list_area" />
+27 -24
View File
@@ -15,50 +15,50 @@
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:layout_marginBottom="4dp">
android:layout_marginBottom="6dp">
<ImageView
android:layout_width="36dp"
android:layout_height="36dp"
android:layout_width="48dp"
android:layout_height="48dp"
android:src="@drawable/ic_launcher" />
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingLeft="8dp">
android:paddingLeft="10dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="MiniABS"
android:textSize="18sp"
android:textSize="@dimen/login_title"
android:textStyle="bold"
android:textColor="@color/accent" />
android:textColor="@color/accent_light" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Audiobookshelf podcasts"
android:textSize="11sp"
android:text="Podcasts de Audiobookshelf"
android:textSize="@dimen/login_tip"
android:textColor="@color/sub" />
</LinearLayout>
</LinearLayout>
<TextView style="@style/Label" android:text="Server address" />
<TextView style="@style/Label" android:text="Enderezo do servidor" />
<EditText
android:id="@+id/server"
style="@style/Field"
android:hint="http://192.168.1.10:13378"
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
android:id="@+id/user"
style="@style/Field"
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
android:id="@+id/pass"
style="@style/Field"
@@ -66,36 +66,39 @@
<CheckBox
android:id="@+id/use_key"
android:layout_width="wrap_content"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:textSize="12sp"
android:text="Use an API key instead" />
android:layout_marginTop="4dp"
android:minHeight="48dp"
android:textSize="@dimen/login_check"
android:text="Usar unha chave da API" />
<Button
android:id="@+id/connect"
android:layout_width="fill_parent"
android:layout_height="42dp"
android:layout_height="@dimen/login_btn_h"
android:layout_marginTop="6dp"
android:background="@drawable/btn_accent"
android:text="Connect"
android:text="Conectar"
android:textStyle="bold"
android:textSize="14sp"
android:textColor="#121519" />
android:textSize="@dimen/login_btn_text"
android:textColor="#FFFFFF" />
<TextView
android:id="@+id/status"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:textSize="12sp"
android:textColor="@color/accent" />
android:layout_marginTop="10dp"
android:textSize="@dimen/login_status"
android:textColor="@color/accent_light" />
<TextView
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:textSize="10sp"
android:layout_marginTop="10dp"
android:textSize="@dimen/login_tip"
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>
</ScrollView>
+38 -34
View File
@@ -11,8 +11,10 @@
android:layout_height="fill_parent"
android:layout_weight="1"
android:gravity="center_vertical"
android:textSize="12sp"
android:textColor="@color/accent"
android:paddingRight="10dp"
android:textSize="@dimen/header_sub"
android:textStyle="bold"
android:textColor="@color/accent_light"
android:singleLine="true"
android:ellipsize="end"
android:background="@drawable/row_sel" />
@@ -27,9 +29,9 @@
<ImageView
android:id="@+id/cover"
android:layout_width="84dp"
android:layout_height="84dp"
android:layout_marginRight="8dp"
android:layout_width="@dimen/cover_player"
android:layout_height="@dimen/cover_player"
android:layout_marginRight="10dp"
android:scaleType="centerCrop"
android:background="@color/surface" />
@@ -43,7 +45,7 @@
android:id="@+id/title"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:textSize="14sp"
android:textSize="@dimen/player_title"
android:textStyle="bold"
android:textColor="@color/text"
android:maxLines="3"
@@ -53,8 +55,8 @@
android:id="@+id/date"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_marginTop="2dp"
android:textSize="10sp"
android:layout_marginTop="3dp"
android:textSize="@dimen/player_meta"
android:textColor="@color/sub"
android:singleLine="true" />
@@ -63,9 +65,10 @@
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_marginTop="2dp"
android:textSize="10sp"
android:textSize="@dimen/player_meta"
android:textColor="@color/fresh"
android:maxLines="2" />
android:maxLines="2"
android:ellipsize="end" />
</LinearLayout>
</LinearLayout>
@@ -73,28 +76,29 @@
android:id="@+id/seek"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:paddingLeft="14dp"
android:paddingRight="14dp"
android:minHeight="4dp"
android:maxHeight="4dp"
android:paddingLeft="18dp"
android:paddingRight="18dp"
android:paddingTop="6dp"
android:paddingBottom="6dp"
android:minHeight="@dimen/seek_track"
android:maxHeight="@dimen/seek_track"
android:progressDrawable="@drawable/seek_track"
android:thumb="@drawable/seek_thumb"
android:thumbOffset="7dp"
android:thumbOffset="13dp"
android:max="1000" />
<RelativeLayout
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:paddingLeft="10dp"
android:paddingRight="10dp"
android:paddingTop="2dp">
android:paddingLeft="12dp"
android:paddingRight="12dp">
<TextView
android:id="@+id/pos"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:textSize="11sp"
android:textSize="@dimen/player_time"
android:textColor="@color/sub" />
<TextView
@@ -102,7 +106,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:textSize="11sp"
android:textSize="@dimen/player_time"
android:textColor="@color/sub" />
</RelativeLayout>
@@ -112,33 +116,33 @@
android:orientation="horizontal"
android:gravity="center"
android:paddingTop="4dp"
android:paddingBottom="6dp">
android:paddingBottom="8dp">
<Button
android:id="@+id/back"
android:layout_width="58dp"
android:layout_height="38dp"
android:layout_width="@dimen/skip_w"
android:layout_height="@dimen/skip_h"
android:background="@drawable/btn_flat"
android:textColor="@color/text"
android:textSize="13sp"
android:textSize="@dimen/skip_text"
android:textStyle="bold" />
<ImageButton
android:id="@+id/play"
android:layout_width="56dp"
android:layout_height="56dp"
android:layout_marginLeft="16dp"
android:layout_marginRight="16dp"
android:layout_width="@dimen/play_w"
android:layout_height="@dimen/play_h"
android:layout_marginLeft="@dimen/play_gap"
android:layout_marginRight="@dimen/play_gap"
android:background="@drawable/btn_round"
android:src="@drawable/ic_play" />
<Button
android:id="@+id/fwd"
android:layout_width="58dp"
android:layout_height="38dp"
android:layout_width="@dimen/skip_w"
android:layout_height="@dimen/skip_h"
android:background="@drawable/btn_flat"
android:textColor="@color/text"
android:textSize="13sp"
android:textSize="@dimen/skip_text"
android:textStyle="bold" />
</LinearLayout>
@@ -154,9 +158,9 @@
android:id="@+id/desc"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:padding="8dp"
android:textSize="12sp"
android:padding="10dp"
android:textSize="@dimen/player_desc"
android:textColor="@color/sub"
android:lineSpacingExtra="2dp" />
android:lineSpacingExtra="3dp" />
</ScrollView>
</LinearLayout>
+7 -11
View File
@@ -9,14 +9,14 @@
android:layout_height="wrap_content"
android:orientation="horizontal"
android:background="@color/header"
android:padding="6dp"
android:padding="8dp"
android:gravity="center_vertical">
<ImageView
android:id="@+id/cover"
android:layout_width="44dp"
android:layout_height="44dp"
android:layout_marginRight="8dp"
android:layout_width="@dimen/cover_podcast"
android:layout_height="@dimen/cover_podcast"
android:layout_marginRight="10dp"
android:scaleType="centerCrop"
android:background="@color/surface" />
@@ -30,7 +30,7 @@
android:id="@+id/title"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:textSize="14sp"
android:textSize="@dimen/podcast_title"
android:textStyle="bold"
android:textColor="@color/text"
android:maxLines="2"
@@ -38,12 +38,8 @@
<TextView
android:id="@+id/subtitle"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:textSize="10sp"
android:textColor="@color/sub"
android:singleLine="true"
android:ellipsize="end" />
style="@style/RowMeta"
android:layout_marginTop="2dp" />
</LinearLayout>
</LinearLayout>
+3 -2
View File
@@ -13,13 +13,14 @@
android:layout_gravity="center"
android:gravity="center"
android:padding="20dp"
android:textSize="13sp"
android:textSize="@dimen/text_empty"
android:textColor="@color/sub"
android:lineSpacingExtra="2dp"
android:visibility="gone" />
<ProgressBar
android:id="@+id/loading"
style="?android:attr/progressBarStyleSmall"
style="?android:attr/progressBarStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
+8 -7
View File
@@ -11,13 +11,13 @@
android:id="@+id/mini_progress"
style="?android:attr/progressBarStyleHorizontal"
android:layout_width="fill_parent"
android:layout_height="2dp"
android:layout_height="@dimen/mini_progress"
android:progressDrawable="@drawable/thin_progress"
android:max="1000" />
<LinearLayout
android:layout_width="fill_parent"
android:layout_height="44dp"
android:layout_height="@dimen/mini_h"
android:orientation="horizontal"
android:gravity="center_vertical">
@@ -28,14 +28,14 @@
android:layout_weight="1"
android:orientation="vertical"
android:gravity="center_vertical"
android:paddingLeft="8dp"
android:paddingLeft="10dp"
android:background="@drawable/row_sel">
<TextView
android:id="@+id/mini_title"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:textSize="13sp"
android:textSize="@dimen/mini_title"
android:textColor="@color/text"
android:singleLine="true"
android:ellipsize="end" />
@@ -44,7 +44,8 @@
android:id="@+id/mini_sub"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:textSize="10sp"
android:layout_marginTop="1dp"
android:textSize="@dimen/mini_sub"
android:textColor="@color/sub"
android:singleLine="true"
android:ellipsize="end" />
@@ -52,8 +53,8 @@
<ImageButton
android:id="@+id/mini_play"
android:layout_width="44dp"
android:layout_height="44dp"
android:layout_width="@dimen/mini_btn"
android:layout_height="@dimen/mini_btn"
android:background="@drawable/row_sel"
android:src="@drawable/ic_play_small" />
</LinearLayout>
+16 -30
View File
@@ -3,18 +3,18 @@
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:minHeight="48dp"
android:paddingLeft="6dp"
android:paddingRight="6dp"
android:paddingTop="5dp"
android:paddingBottom="5dp">
android:minHeight="@dimen/row_min"
android:paddingLeft="@dimen/row_pad"
android:paddingRight="@dimen/row_pad"
android:paddingTop="7dp"
android:paddingBottom="7dp">
<View
android:id="@+id/dot"
android:layout_width="7dp"
android:layout_height="7dp"
android:layout_marginTop="5dp"
android:layout_marginRight="6dp" />
android:layout_width="@dimen/dot"
android:layout_height="@dimen/dot"
android:layout_marginTop="6dp"
android:layout_marginRight="8dp" />
<LinearLayout
android:layout_width="0dp"
@@ -26,37 +26,23 @@
android:id="@+id/show"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:textSize="10sp"
android:textColor="@color/accent"
android:textSize="@dimen/text_show"
android:textColor="@color/accent_light"
android:singleLine="true"
android:ellipsize="end" />
<TextView
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 android:id="@+id/title" style="@style/RowTitle" />
<ProgressBar
android:id="@+id/progress"
style="?android:attr/progressBarStyleHorizontal"
android:layout_width="fill_parent"
android:layout_height="2dp"
android:layout_marginTop="3dp"
android:layout_marginBottom="1dp"
android:layout_height="@dimen/row_progress"
android:layout_marginTop="4dp"
android:layout_marginBottom="2dp"
android:progressDrawable="@drawable/thin_progress"
android:max="1000" />
<TextView
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" />
<TextView android:id="@+id/meta" style="@style/RowMeta" android:layout_marginTop="1dp" />
</LinearLayout>
</LinearLayout>
+5 -4
View File
@@ -4,9 +4,10 @@
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:background="@color/surface"
android:paddingLeft="8dp"
android:paddingTop="4dp"
android:paddingBottom="4dp"
android:textSize="10sp"
android:paddingLeft="10dp"
android:paddingRight="10dp"
android:paddingTop="6dp"
android:paddingBottom="6dp"
android:textSize="@dimen/text_section"
android:textStyle="bold"
android:textColor="@color/sub" />
+4 -3
View File
@@ -2,7 +2,8 @@
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/title"
android:layout_width="fill_parent"
android:layout_height="44dp"
android:layout_height="@dimen/row_min"
android:gravity="center"
android:textSize="13sp"
android:textColor="@color/accent" />
android:textSize="@dimen/text_more"
android:textStyle="bold"
android:textColor="@color/accent_light" />
+7 -21
View File
@@ -3,15 +3,15 @@
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:minHeight="48dp"
android:padding="5dp"
android:minHeight="@dimen/row_min"
android:padding="7dp"
android:gravity="center_vertical">
<ImageView
android:id="@+id/cover"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_marginRight="8dp"
android:layout_width="@dimen/cover_row"
android:layout_height="@dimen/cover_row"
android:layout_marginRight="10dp"
android:scaleType="centerCrop"
android:background="@color/surface" />
@@ -21,22 +21,8 @@
android:layout_weight="1"
android:orientation="vertical">
<TextView
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 android:id="@+id/title" style="@style/RowTitle" />
<TextView
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" />
<TextView android:id="@+id/meta" style="@style/RowMeta" android:layout_marginTop="2dp" />
</LinearLayout>
</LinearLayout>
+6 -6
View File
@@ -1,12 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string-array name="skip_labels">
<item>5 seconds</item>
<item>10 seconds</item>
<item>15 seconds</item>
<item>30 seconds</item>
<item>45 seconds</item>
<item>60 seconds</item>
<item>5 segundos</item>
<item>10 segundos</item>
<item>15 segundos</item>
<item>30 segundos</item>
<item>45 segundos</item>
<item>60 segundos</item>
</string-array>
<string-array name="skip_values">
<item>5</item>
+63
View File
@@ -0,0 +1,63 @@
<?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">62dp</dimen>
<dimen name="skip_h">42dp</dimen>
<dimen name="skip_text">16sp</dimen>
<dimen name="play_w">58dp</dimen>
<dimen name="play_h">46dp</dimen>
<dimen name="play_gap">28dp</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>
+41 -16
View File
@@ -2,16 +2,20 @@
<resources>
<string name="app_name">MiniABS</string>
<color name="bg">#121519</color>
<color name="surface">#1C2127</color>
<color name="header">#0B0D10</color>
<color name="line">#2A3038</color>
<color name="text">#ECEFF2</color>
<color name="sub">#9AA4AE</color>
<color name="dim">#5C6670</color>
<color name="accent">#F5A623</color>
<!-- Colours taken from the Audiobookshelf icon: gradient #9437FC to #570DCF, waves #BC8AFC -->
<color name="bg">#13111A</color>
<color name="surface">#1D1928</color>
<color name="header">#0C0A12</color>
<color name="line">#2C2638</color>
<color name="text">#EEEBF3</color>
<color name="sub">#ADA6BC</color>
<color name="dim">#736C82</color>
<color name="accent">#7A25EC</color> <!-- fills: tabs, progress, seek bar -->
<color name="accent_light">#BC8AFC</color> <!-- text and small marks on dark backgrounds -->
<color name="accent_top">#9437FC</color> <!-- button gradient, as in the icon -->
<color name="accent_bottom">#5A11D3</color>
<color name="fresh">#6EC1E4</color>
<color name="pressed">#2C333C</color>
<color name="pressed">#2E2642</color>
<style name="MiniTheme" parent="@android:style/Theme.NoTitleBar">
<item name="android:windowBackground">@color/bg</item>
@@ -22,11 +26,11 @@
<style name="Header">
<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:gravity">center_vertical</item>
<item name="android:paddingLeft">8dp</item>
<item name="android:paddingRight">2dp</item>
<item name="android:paddingLeft">10dp</item>
<item name="android:paddingRight">0dp</item>
</style>
<style name="Tab">
@@ -34,9 +38,10 @@
<item name="android:layout_height">fill_parent</item>
<item name="android:layout_weight">1</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:textColor">@color/sub</item>
<item name="android:singleLine">true</item>
<item name="android:background">@drawable/tab_bg</item>
</style>
@@ -53,7 +58,8 @@
<style name="Field">
<item name="android:layout_width">fill_parent</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:textColor">#000000</item>
</style>
@@ -61,8 +67,27 @@
<style name="Label">
<item name="android:layout_width">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: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>
</resources>
+17 -17
View File
@@ -1,56 +1,56 @@
<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
<PreferenceCategory android:title="Playback">
<PreferenceCategory android:title="Reprodución">
<ListPreference
android:key="skip_back"
android:title="Skip back"
android:title="Retroceder"
android:entries="@array/skip_labels"
android:entryValues="@array/skip_values"
android:defaultValue="15" />
<ListPreference
android:key="skip_fwd"
android:title="Skip forward"
android:title="Avanzar"
android:entries="@array/skip_labels"
android:entryValues="@array/skip_values"
android:defaultValue="30" />
</PreferenceCategory>
<PreferenceCategory android:title="Downloads">
<PreferenceCategory android:title="Descargas">
<CheckBoxPreference
android:key="wifi_only"
android:title="Download on Wi-Fi only"
android:summary="Saves your mobile data"
android:title="Descargar só con wifi"
android:summary="Aforra datos móbiles"
android:defaultValue="true" />
<Preference
android:key="delete_all"
android:title="Delete all downloads" />
android:title="Eliminar todas as descargas" />
</PreferenceCategory>
<PreferenceCategory android:title="Display">
<PreferenceCategory android:title="Aparencia">
<CheckBoxPreference
android:key="covers"
android:title="Show cover art"
android:summary="Turn off to save data and memory"
android:title="Amosar portadas"
android:summary="Desactívao para aforrar datos e memoria"
android:defaultValue="true" />
</PreferenceCategory>
<PreferenceCategory android:title="Account">
<PreferenceCategory android:title="Conta">
<Preference
android:key="account"
android:title="Signed in" />
android:title="Sesión iniciada" />
<Preference
android:key="pins"
android:title="Certificates you trusted" />
android:title="Certificados nos que confías" />
<Preference
android:key="logout"
android:title="Log out" />
android:title="Pechar sesión" />
</PreferenceCategory>
<PreferenceCategory android:title="About">
<PreferenceCategory android:title="Acerca de">
<Preference
android:key="about"
android:title="MiniABS 1.1"
android:summary="A tiny Audiobookshelf podcast client for Android 2.2+" />
android:title="MiniABS 1.5"
android:summary="Un pequeno cliente de podcasts de Audiobookshelf para Android 2.2+" />
</PreferenceCategory>
</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). */
final class Api {
static final String VERSION = "1.1";
static final String VERSION = "1.5";
static class ApiException extends IOException {
final int code;
@@ -100,7 +100,7 @@ final class Api {
if (peer.length == 0 || !(peer[0] instanceof java.security.cert.X509Certificate)
|| !Trust.hostMatches((java.security.cert.X509Certificate) peer[0], u.getHost())) {
ss.close();
throw new SSLException("Certificate doesn't match " + u.getHost());
throw new SSLException("O certificado non corresponde a " + u.getHost());
}
io = ss;
}
@@ -123,11 +123,11 @@ final class Api {
int ch;
while ((ch = in.read()) != -1 && ch != '\n' && line.length() < 200) line.append((char) ch);
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 {
code = Integer.parseInt(parts[1]);
} catch (NumberFormatException e) {
throw new IOException("Unexpected reply from server");
throw new IOException("Resposta inesperada do servidor");
}
byte[] rest = new byte[1024];
int total = 0, n;
@@ -144,10 +144,10 @@ final class Api {
refreshIf(c, used, 0);
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 >= 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";
}
@@ -216,7 +216,7 @@ final class Api {
static Object call(Context c, String method, String path, JSONObject body, int maxString, String[] deepSkip,
boolean mayRefresh) throws IOException {
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 (mayRefresh) ensureFresh(c, 120);
String used = p.token();
@@ -232,7 +232,7 @@ final class Api {
refreshIf(c, used, 0);
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));
return readBody(conn, maxString, deepSkip);
@@ -243,7 +243,7 @@ final class Api {
static JSONObject obj(Object o) throws IOException {
if (o instanceof JSONObject) return (JSONObject) o;
throw new IOException("Unexpected reply from server");
throw new IOException("Resposta inesperada do servidor");
}
// ------------------------------------------------------------------ sign-in
@@ -253,13 +253,13 @@ final class Api {
HttpURLConnection conn = open(c, server + "/status", "GET");
try {
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));
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;
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");
} finally {
@@ -280,19 +280,19 @@ final class Api {
}
writeBody(conn, body);
int code = responseCode(conn);
if (code == 401) throw new ApiException(401, "Wrong username or password.");
if (code == 429) throw new ApiException(429, "Too many attempts. Wait a minute and try again.");
if (code == 401) throw new ApiException(401, "Usuario ou contrasinal incorrectos.");
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));
JSONObject o = obj(readBody(conn, 500, null));
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 refresh = Util.str(u, "refreshToken");
String legacy = Util.str(u, "token");
Prefs p = Prefs.get(c);
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 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");
} finally {
conn.disconnect();
@@ -310,7 +310,7 @@ final class Api {
return name;
} catch (IOException e) {
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;
}
}
@@ -335,7 +335,7 @@ final class Api {
private static void refreshIf(Context c, String stale, long minSeconds) throws IOException {
synchronized (REFRESH_LOCK) {
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.equals(p.token())) return;
} else if (p.tokenSecondsLeft() >= minSeconds) {
@@ -348,12 +348,12 @@ final class Api {
writeBody(conn, null);
int code = responseCode(conn);
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));
JSONObject u = obj(readBody(conn, 500, null)).optJSONObject("user");
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"));
} finally {
conn.disconnect();
+18 -18
View File
@@ -73,7 +73,7 @@ abstract class BaseActivity extends Activity {
return TrustPrompt.offer(this, new Runnable() {
public void 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();
int st = ps.state();
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 {
double[] p = App.progress(e.id);
double cur = p != null ? p[0] : Math.max(0, App.localPosition(this, e.id));
pos = (int) (cur * 1000);
dur = (int) (e.duration * 1000);
sub = Util.join(e.show, "Paused");
sub = Util.join(e.show, "En pausa");
}
miniSub.setText(sub);
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) {
menu.setHeaderTitle(e.title);
menu.add(0, M_PLAY, 0, "Play");
if (DownloadService.busyWith(e.id)) menu.add(0, M_CANCEL, 0, "Cancel download");
else if (Downloads.has(this, e.id)) menu.add(0, M_DELETE, 0, "Delete download");
else menu.add(0, M_DOWNLOAD, 0, "Download");
menu.add(0, M_PLAY, 0, "Reproducir");
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, "Eliminar descarga");
else menu.add(0, M_DOWNLOAD, 0, "Descargar");
double[] p = App.progress(e.id);
if (p != null && p[2] > 0) menu.add(0, M_UNPLAYED, 0, "Mark as unplayed");
else menu.add(0, M_PLAYED, 0, "Mark as played");
if (offerShow) menu.add(0, M_SHOW, 0, "Go to show");
if (p != null && p[2] > 0) menu.add(0, M_UNPLAYED, 0, "Marcar como non escoitado");
else menu.add(0, M_PLAYED, 0, "Marcar como escoitado");
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. */
@@ -168,26 +168,26 @@ abstract class BaseActivity extends Activity {
case M_DOWNLOAD:
if (Prefs.get(this).wifiOnly() && !Util.onWifi(this)) {
new AlertDialog.Builder(this)
.setMessage("You're not on Wi-Fi. Download over mobile data this time?")
.setPositiveButton("Download", new DialogInterface.OnClickListener() {
.setMessage("Non estás nunha wifi. Descargar con datos móbiles esta vez?")
.setPositiveButton("Descargar", new DialogInterface.OnClickListener() {
public void onClick(DialogInterface d, int w) {
Prefs.get(BaseActivity.this).sp.edit().putBoolean("wifi_only", false).commit();
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();
}
})
.setNegativeButton("Cancel", null)
.setNegativeButton("Cancelar", null)
.show();
} else {
DownloadService.enqueue(this, e);
toast("Downloading…");
toast("Descargando…");
}
if (onChanged != null) onChanged.run();
return true;
case M_CANCEL:
DownloadService.cancel(e.id);
toast("Download cancelled");
toast("Descarga cancelada");
if (onChanged != null) onChanged.run();
return true;
case M_DELETE:
@@ -196,7 +196,7 @@ abstract class BaseActivity extends Activity {
ps.stop();
}
Downloads.delete(this, e);
toast("Download deleted");
toast("Descarga eliminada");
if (onChanged != null) onChanged.run();
return true;
case M_PLAYED:
@@ -230,7 +230,7 @@ abstract class BaseActivity extends Activity {
}
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) {
+7 -7
View File
@@ -102,7 +102,7 @@ public class DownloadService extends IntentService {
}
} catch (Exception ex) {
if (!e.id.equals(cancelId)) {
lastError = "Download failed: " + Util.describe(ex);
lastError = "Fallou a descarga: " + Util.describe(ex);
notifyDone(e.title, lastError);
}
} finally {
@@ -116,14 +116,14 @@ public class DownloadService extends IntentService {
private void download(Episode e) throws IOException {
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);
if (Downloads.audio(this, e) != null) {
Downloads.saved(this, e);
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");
Notification n = progressNotification(e, 0);
startForeground(NOTIF_ID, n);
@@ -158,8 +158,8 @@ public class DownloadService extends IntentService {
if (code != 404 && code != 403) break;
}
if (code != 200 && code != 206) {
if (code == 401) throw new Api.AuthException("sign-in expired; log in again");
if (code == 403) throw new IOException("your account isn't allowed to download");
if (code == 401) throw new Api.AuthException("a sesión caducou; inicia sesión de novo");
if (code == 403) throw new IOException("a túa conta non ten permiso para descargar");
throw new Api.ApiException(code, "");
}
try {
@@ -194,7 +194,7 @@ public class DownloadService extends IntentService {
}
if (e.id.equals(cancelId)) return;
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);
percent = 100;
}
@@ -216,7 +216,7 @@ public class DownloadService extends IntentService {
synchronized (queue) {
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());
return n;
}
+3 -3
View File
@@ -28,11 +28,11 @@ final class Downloads {
static File dir(Context c) throws IOException {
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);
if (d == null) throw new IOException("Can't use the SD card.");
if (!d.exists() && !d.mkdirs()) throw new IOException("Can't create a folder on the SD card.");
if (d == null) throw new IOException("Non se pode usar a tarxeta SD.");
if (!d.exists() && !d.mkdirs()) throw new IOException("Non se pode crear un cartafol na tarxeta SD.");
File nomedia = new File(d, ".nomedia");
if (!nomedia.exists()) {
try {
+21 -21
View File
@@ -202,7 +202,7 @@ public class HomeActivity extends BaseActivity {
}
void done(Integer n) {
if (n > 0) toast("Synced offline progress");
if (n > 0) toast("Sincronizouse o progreso gardado sen conexión");
}
}.start();
}
@@ -222,7 +222,7 @@ public class HomeActivity extends BaseActivity {
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;
}
String id = p.libraryId();
@@ -273,10 +273,10 @@ public class HomeActivity extends BaseActivity {
// older servers: just skip this section
}
if (!cont.isEmpty()) {
rows.add(Rows.Row.header("CONTINUE LISTENING"));
rows.add(Rows.Row.header("CONTINUAR ESCOITANDO"));
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");
int n = eps == null ? 0 : eps.length();
@@ -301,8 +301,8 @@ public class HomeActivity extends BaseActivity {
}
latestPage = page;
latestMore = hasMore;
if (hasMore) latestCache.add(Rows.Row.more("Load more"));
if (tab == TAB_LATEST) show(latestCache, "No episodes yet.");
if (hasMore) latestCache.add(Rows.Row.more("Cargar máis"));
if (tab == TAB_LATEST) show(latestCache, "Aínda non hai episodios.");
}
void failed(Exception e) {
@@ -315,12 +315,12 @@ public class HomeActivity extends BaseActivity {
if (more) {
toast(Util.describe(e));
if (latestCache != null && !latestCache.isEmpty()) {
latestCache.get(latestCache.size() - 1).text = "Load more";
latestCache.get(latestCache.size() - 1).text = "Cargar máis";
adapter.notifyDataSetChanged();
}
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 (tab == TAB_LATEST) show(null, null);
return;
@@ -368,7 +368,7 @@ public class HomeActivity extends BaseActivity {
if (isFinishing()) return;
updateLibraryLabel();
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) {
@@ -378,7 +378,7 @@ public class HomeActivity extends BaseActivity {
showError(e);
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 (tab == TAB_SHOWS) show(null, null);
return;
@@ -391,7 +391,7 @@ public class HomeActivity extends BaseActivity {
/** When the server can't be reached but episodes are saved, show those instead. */
private void offlineFallback(Exception e) {
if (!(e instanceof Api.ApiException) && !Downloads.list(this).isEmpty()) {
toast("Can't reach the server. Showing saved episodes.");
toast("Non se pode acceder ao servidor. Amósanse os episodios gardados.");
selectTab(TAB_SAVED);
} else {
show(null, null);
@@ -405,17 +405,17 @@ public class HomeActivity extends BaseActivity {
savedSignature = (cur == null ? "" : cur.id) + "/" + queued.size() + "/" + DownloadService.lastError;
if (DownloadService.lastError != null) rows.add(Rows.Row.header(DownloadService.lastError));
if (cur != null || !queued.isEmpty()) {
rows.add(Rows.Row.header("DOWNLOADING"));
rows.add(Rows.Row.header("DESCARGANDO"));
if (cur != null) rows.add(Rows.Row.episode(cur));
for (Episode q : queued) rows.add(Rows.Row.episode(q));
}
List<Episode> saved = Downloads.list(this);
if (!saved.isEmpty()) {
rows.add(Rows.Row.header("SAVED ON SD CARD (" + saved.size() + ")"));
rows.add(Rows.Row.header("GARDADOS NA TARXETA SD (" + saved.size() + ")"));
for (Episode e : saved) rows.add(Rows.Row.episode(e));
}
if (rows.isEmpty()) {
show(rows, "Nothing saved yet.\n\nLong-press an episode and choose Download to listen offline.");
show(rows, "Aínda non gardaches nada.\n\nMantén premido un episodio e escolle Descargar para escoitalo sen conexión.");
} else {
show(rows, null);
}
@@ -436,7 +436,7 @@ public class HomeActivity extends BaseActivity {
break;
case Rows.MORE:
if (!loadingLatest && latestMore) {
r.text = "Loading…";
r.text = "Cargando…";
adapter.notifyDataSetChanged();
loadLatest(true);
}
@@ -469,12 +469,12 @@ public class HomeActivity extends BaseActivity {
private void chooseLibrary() {
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;
}
String[] names = new String[podcastLibs.size()];
for (int i = 0; i < names.length; i++) names[i] = podcastLibs.get(i)[1];
new AlertDialog.Builder(this).setTitle("Podcast library")
new AlertDialog.Builder(this).setTitle("Biblioteca de podcasts")
.setItems(names, new DialogInterface.OnClickListener() {
public void onClick(DialogInterface d, int which) {
String[] l = podcastLibs.get(which);
@@ -491,10 +491,10 @@ public class HomeActivity extends BaseActivity {
@Override
public boolean onCreateOptionsMenu(Menu menu) {
menu.add(0, 1, 0, "Refresh").setIcon(android.R.drawable.ic_menu_rotate);
menu.add(0, 2, 0, "Library").setIcon(android.R.drawable.ic_menu_agenda);
menu.add(0, 3, 0, "Now playing").setIcon(android.R.drawable.ic_media_play);
menu.add(0, 4, 0, "Settings").setIcon(android.R.drawable.ic_menu_preferences);
menu.add(0, 1, 0, "Actualizar").setIcon(android.R.drawable.ic_menu_rotate);
menu.add(0, 2, 0, "Biblioteca").setIcon(android.R.drawable.ic_menu_agenda);
menu.add(0, 3, 0, "En reprodución").setIcon(android.R.drawable.ic_media_play);
menu.add(0, 4, 0, "Axustes").setIcon(android.R.drawable.ic_menu_preferences);
return true;
}
+10 -10
View File
@@ -88,13 +88,13 @@ final class Json {
return JSONObject.NULL;
default:
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 {
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;
}
while (true) {
if (c != '"') throw new IOException("Unexpected reply from server");
if (c != '"') throw new IOException("Resposta inesperada do servidor");
String key = string(512);
c = skipWs();
if (c != ':') throw new IOException("Unexpected reply from server");
if (c != ':') throw new IOException("Resposta inesperada do servidor");
c = skipWs();
if (skipKey(key)) {
skipValue(c);
@@ -173,7 +173,7 @@ final class Json {
depth--;
return o;
}
throw new IOException("Unexpected reply from server");
throw new IOException("Resposta inesperada do servidor");
}
}
@@ -189,7 +189,7 @@ final class Json {
continue;
}
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;
while (d > 0) {
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);
else if (ch == '{' || ch == '[') d++;
else if (ch == '}' || ch == ']') d--;
@@ -217,7 +217,7 @@ final class Json {
boolean cut = false;
while (true) {
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 == '\\') {
c = read();
@@ -231,14 +231,14 @@ final class Json {
int v = 0;
for (int i = 0; i < 4; i++) {
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;
}
c = v;
break;
}
case -1:
throw new IOException("Reply was cut off");
throw new IOException("A resposta chegou incompleta");
default:
break; // \" \\ \/
}
+4 -4
View File
@@ -42,7 +42,7 @@ public class LoginActivity extends Activity {
int vis = on ? View.GONE : View.VISIBLE;
user.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
: InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD);
pass.setText("");
@@ -69,17 +69,17 @@ public class LoginActivity extends Activity {
final String pw = pass.getText().toString();
final boolean key = useKey.isChecked();
if (srv.length() == 0) {
status.setText("Enter your server address.");
status.setText("Escribe o enderezo do servidor.");
return;
}
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;
}
server.setText(srv);
((InputMethodManager) getSystemService(INPUT_METHOD_SERVICE)).hideSoftInputFromWindow(pass.getWindowToken(), 0);
connect.setEnabled(false);
status.setText("Connecting…");
status.setText("Conectando…");
final Prefs p = Prefs.get(this);
p.logout();
+18 -18
View File
@@ -167,9 +167,9 @@ public class PlayerActivity extends BaseActivity {
Episode e = current();
if (e == null) {
show.setText("");
title.setText("Nothing playing");
title.setText("Non se está a reproducir nada");
date.setText("");
status.setText("Pick an episode from the list.");
status.setText("Escolle un episodio da lista.");
desc.setText("");
cover.setVisibility(View.GONE);
setControls(false);
@@ -181,7 +181,7 @@ public class PlayerActivity extends BaseActivity {
title.setText(e.title);
date.setText(Util.join(Util.date(e.published), e.duration > 0 ? Util.duration(e.duration) : ""));
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();
@@ -195,8 +195,8 @@ public class PlayerActivity extends BaseActivity {
playing = ps.isPlaying() || (ps.state() == PlayerService.LOADING);
st = ps.status();
long sleep = ps.sleepLeftMs();
if (sleep == -1) st = Util.join(st, "Sleep at end");
else if (sleep > 0) st = Util.join(st, "Sleep in " + Util.clock(sleep));
if (sleep == -1) st = Util.join(st, "Pausa ao final do episodio");
else if (sleep > 0) st = Util.join(st, "Pausa en " + Util.clock(sleep));
seek.setSecondaryProgress(ps.bufferedPercent() * 10);
setControls(true);
} else {
@@ -204,7 +204,7 @@ public class PlayerActivity extends BaseActivity {
double cur = pr != null ? pr[0] : Math.max(0, App.localPosition(this, e.id));
p = (int) (cur * 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;
seek.setSecondaryProgress(0);
setControls(true);
@@ -234,17 +234,17 @@ public class PlayerActivity extends BaseActivity {
if (e == null) return false;
PlayerService ps = PlayerService.get();
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);
menu.add(0, 2, 0, "Mark as played").setIcon(android.R.drawable.ic_menu_agenda);
if (active) menu.add(0, 1, 0, "Temporizador de pausa").setIcon(android.R.drawable.ic_lock_idle_alarm);
menu.add(0, 2, 0, "Marcar como escoitado").setIcon(android.R.drawable.ic_menu_agenda);
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)) {
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 {
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);
if (active) menu.add(0, 5, 0, "Stop").setIcon(android.R.drawable.ic_menu_close_clear_cancel);
menu.add(0, 4, 0, "Ir ao programa").setIcon(android.R.drawable.ic_menu_view);
if (active) menu.add(0, 5, 0, "Deter").setIcon(android.R.drawable.ic_menu_close_clear_cancel);
return true;
}
@@ -261,7 +261,7 @@ public class PlayerActivity extends BaseActivity {
case 2:
if (active) {
ps.finishAndStop();
toast("Marked as played");
toast("Marcado como escoitado");
} else {
episodeAction(M_PLAYED, e, null);
}
@@ -285,15 +285,15 @@ public class PlayerActivity extends BaseActivity {
private void sleepDialog() {
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",
"End of episode"};
new AlertDialog.Builder(this).setTitle("Sleep timer")
String[] labels = {"Desactivado", "5 minutos", "10 minutos", "15 minutos", "30 minutos", "45 minutos", "1 hora",
"Ao final do episodio"};
new AlertDialog.Builder(this).setTitle("Temporizador de pausa")
.setItems(labels, new DialogInterface.OnClickListener() {
public void onClick(DialogInterface d, int which) {
PlayerService ps = PlayerService.get();
if (ps == null) return;
ps.setSleep(minutes[which]);
toast(minutes[which] == 0 ? "Sleep timer off" : "Sleep timer set");
toast(minutes[which] == 0 ? "Temporizador desactivado" : "Temporizador activado");
refresh();
}
}).show();
+22 -22
View File
@@ -256,7 +256,7 @@ public class PlayerService extends Service implements MediaPlayer.OnPreparedList
}
lastPosMs = positionMs();
state = PAUSED;
status = "Paused";
status = "En pausa";
pausedAt = SystemClock.elapsedRealtime();
syncNow(false);
wifi.release();
@@ -345,7 +345,7 @@ public class PlayerService extends Service implements MediaPlayer.OnPreparedList
markFinished();
releasePlayer();
state = ENDED;
status = "Marked as played";
status = "Marcado como escoitado";
lastPosMs = dur;
pausedAt = SystemClock.elapsedRealtime();
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) {
releasePlayer();
state = LOADING;
status = "Connecting…";
status = "Conectando…";
playOnPrepared = playNow;
showNotification(true);
final int gen = ++generation;
@@ -407,9 +407,9 @@ public class PlayerService extends Service implements MediaPlayer.OnPreparedList
JSONObject t = tracks == null ? null : tracks.optJSONObject(0);
String cu = t == null ? "" : Util.str(t, "contentUrl");
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")) {
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 {
s.contentUrl = cu;
s.url = streamUrl(c, cu);
@@ -442,7 +442,7 @@ public class PlayerService extends Service implements MediaPlayer.OnPreparedList
local = s.local;
resumeMs = (int) (s.start * 1000);
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) {
@@ -467,10 +467,10 @@ public class PlayerService extends Service implements MediaPlayer.OnPreparedList
wifi.acquire();
}
state = LOADING;
status = note != null ? note : local ? "Opening saved file…" : "Buffering…";
status = note != null ? note : local ? "Abrindo o ficheiro gardado…" : "Cargando…";
mp.prepareAsync();
} 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();
} else {
state = PAUSED;
status = "Paused";
status = "En pausa";
showNotification(false);
}
}
@@ -506,12 +506,12 @@ public class PlayerService extends Service implements MediaPlayer.OnPreparedList
mp.setVolume(1f, 1f);
mp.start();
} catch (Exception ex) {
fail("Playback failed: " + Util.describe(ex));
fail("Fallou a reprodución: " + Util.describe(ex));
return;
}
if (!local) wifi.acquire();
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();
lastTick = 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.
if (!local && dur > 0 && pos < dur - 30000) {
lastPosMs = pos;
retryStream("Connection lost, reconnecting…");
retryStream("Perdeuse a conexión, reconectando…");
return;
}
state = ENDED;
status = "Finished";
status = "Rematado";
lastPosMs = dur;
pausedAt = SystemClock.elapsedRealtime();
wifi.release();
@@ -573,9 +573,9 @@ public class PlayerService extends Service implements MediaPlayer.OnPreparedList
if (p != mp) return true;
if (prepared) lastPosMs = positionMs();
if (!local && contentUrl != null) {
retryStream("Connection lost, reconnecting…");
retryStream("Perdeuse a conexión, reconectando…");
} else {
fail("Playback error (" + what + "/" + extra + ").");
fail("Erro de reprodución (" + what + "/" + extra + ").");
}
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. */
private void retryStream(final String msg) {
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;
return;
}
@@ -675,7 +675,7 @@ public class PlayerService extends Service implements MediaPlayer.OnPreparedList
if (sleepAt > 0 && now >= sleepAt) {
sleepAt = 0;
pause();
status = "Sleep timer: paused";
status = "Temporizador: en pausa";
}
} else if (state == PAUSED || state == ENDED || state == ERROR) {
// 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);
String what;
switch (state) {
case PLAYING: what = "Playing"; break;
case LOADING: what = "Loading…"; break;
case ENDED: what = "Finished"; break;
case ERROR: what = "Stopped: tap to retry"; break;
default: what = "Paused"; break;
case PLAYING: what = "Reproducindo"; break;
case LOADING: what = "Cargando…"; break;
case ENDED: what = "Rematado"; break;
case ERROR: what = "Detido: toca para tentalo de novo"; break;
default: what = "En pausa"; break;
}
n.setLatestEventInfo(this, ep.title, Util.join(what, ep.show), pi);
if (foreground) {
+8 -8
View File
@@ -45,7 +45,7 @@ public class PodcastActivity extends BaseActivity {
title.setText(getIntent().getStringExtra("title"));
subtitle.setText(getIntent().getStringExtra("author"));
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);
oldestFirst = p.sp.getBoolean("oldest_first", false);
@@ -104,7 +104,7 @@ public class PodcastActivity extends BaseActivity {
loading.setVisibility(View.GONE);
episodes = list;
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));
render();
}
@@ -145,10 +145,10 @@ public class PodcastActivity extends BaseActivity {
}
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);
boolean none = rows.isEmpty();
empty.setText(none ? "No episodes." : "");
empty.setText(none ? "Non hai episodios." : "");
empty.setVisibility(none ? View.VISIBLE : View.GONE);
}
@@ -172,10 +172,10 @@ public class PodcastActivity extends BaseActivity {
@Override
public boolean onPrepareOptionsMenu(Menu menu) {
menu.clear();
menu.add(0, 1, 0, oldestFirst ? "Newest first" : "Oldest first").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, 3, 0, "Refresh").setIcon(android.R.drawable.ic_menu_rotate);
menu.add(0, 4, 0, "Now playing").setIcon(android.R.drawable.ic_media_play);
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 ? "Amosar escoitados" : "Ocultar escoitados").setIcon(android.R.drawable.ic_menu_view);
menu.add(0, 3, 0, "Actualizar").setIcon(android.R.drawable.ic_menu_rotate);
menu.add(0, 4, 0, "En reprodución").setIcon(android.R.drawable.ic_media_play);
return true;
}
+8 -8
View File
@@ -78,7 +78,7 @@ final class Rows extends BaseAdapter {
Rows(Activity a) {
act = 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) {
@@ -147,7 +147,7 @@ final class Rows extends BaseAdapter {
private void bindShow(View v, Show s) {
((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));
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);
if (finished) {
m = Util.join(m, "Played");
m = Util.join(m, "Escoitado");
} 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) {
m = Util.join(m, Util.duration(dur));
}
if (DownloadService.busyWith(e.id)) {
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)) {
m = Util.join(m, "Saved");
m = Util.join(m, "Gardado");
}
PlayerService ps = PlayerService.get();
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);
@@ -201,7 +201,7 @@ final class Rows extends BaseAdapter {
} else {
bar.setVisibility(View.GONE);
}
int color = act.getResources().getColor(finished ? R.color.line : started ? R.color.accent : R.color.fresh);
int color = act.getResources().getColor(finished ? R.color.line : started ? R.color.accent_light : R.color.fresh);
dotView.setBackgroundDrawable(dot(color));
}
}
+16 -16
View File
@@ -17,14 +17,14 @@ public class SettingsActivity extends PreferenceActivity {
Preference.OnPreferenceChangeListener skipSummary = new Preference.OnPreferenceChangeListener() {
public boolean onPreferenceChange(Preference pref, Object value) {
pref.setSummary(value + " seconds");
pref.setSummary(value + " segundos");
return true;
}
};
ListPreference back = (ListPreference) findPreference("skip_back");
ListPreference fwd = (ListPreference) findPreference("skip_fwd");
back.setSummary(p.skipBack() + " seconds");
fwd.setSummary(p.skipFwd() + " seconds");
back.setSummary(p.skipBack() + " segundos");
fwd.setSummary(p.skipFwd() + " segundos");
back.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("delete_all").setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
public boolean onPreferenceClick(Preference pref) {
new AlertDialog.Builder(SettingsActivity.this)
.setMessage("Delete every downloaded episode from the SD card?")
.setPositiveButton("Delete", new DialogInterface.OnClickListener() {
.setMessage("Eliminar todos os episodios descargados da tarxeta SD?")
.setPositiveButton("Eliminar", new DialogInterface.OnClickListener() {
public void onClick(DialogInterface d, int w) {
PlayerService ps = PlayerService.get();
if (ps != null && ps.isLocal()) ps.stop();
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;
}
});
final Preference pins = findPreference("pins");
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.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
public boolean onPreferenceClick(Preference pref) {
new AlertDialog.Builder(SettingsActivity.this)
.setMessage("Forget the certificates you chose to trust? You'll be asked again next time.")
.setPositiveButton("Forget", new DialogInterface.OnClickListener() {
.setMessage("Esquecer os certificados nos que decidiches confiar? Preguntarase de novo a próxima vez.")
.setPositiveButton("Esquecer", new DialogInterface.OnClickListener() {
public void onClick(DialogInterface d, int w) {
Trust.forgetPins(SettingsActivity.this);
pins.setSummary("None");
pins.setSummary("Ningún");
pins.setEnabled(false);
}
})
.setNegativeButton("Cancel", null).show();
.setNegativeButton("Cancelar", null).show();
return true;
}
});
@@ -78,8 +78,8 @@ public class SettingsActivity extends PreferenceActivity {
findPreference("logout").setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
public boolean onPreferenceClick(Preference pref) {
new AlertDialog.Builder(SettingsActivity.this)
.setMessage("Log out of " + p.server() + "? Downloads stay on the SD card.")
.setPositiveButton("Log out", new DialogInterface.OnClickListener() {
.setMessage("Pechar sesión en " + p.server() + "? As descargas quedan na tarxeta SD.")
.setPositiveButton("Pechar sesión", new DialogInterface.OnClickListener() {
public void onClick(DialogInterface d, int w) {
PlayerService ps = PlayerService.get();
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
}
})
.setNegativeButton("Cancel", null).show();
.setNegativeButton("Cancelar", null).show();
return true;
}
});
+9 -10
View File
@@ -7,7 +7,6 @@ import java.security.MessageDigest;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.text.DateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
@@ -67,7 +66,7 @@ final class Trust {
ctx.init(null, new TrustManager[]{new Manager(c.getApplicationContext())}, null);
factory = ctx.getSocketFactory();
} 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;
@@ -326,15 +325,15 @@ final class Trust {
X509Certificate leaf = r.chain[0];
X509Certificate top = top(r.chain);
StringBuilder sb = new StringBuilder();
sb.append("This phone doesn't recognise the certificate of ").append(host).append(".\n\n");
sb.append("Issued to: ").append(commonName(leaf.getSubjectX500Principal())).append('\n');
sb.append("Issued by: ").append(commonName(leaf.getIssuerX500Principal())).append('\n');
sb.append("Valid until: ").append(DateFormat.getDateInstance().format(leaf.getNotAfter())).append("\n\n");
sb.append("SHA-256 fingerprint:\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("Este teléfono non recoñece o certificado de ").append(host).append(".\n\n");
sb.append("Emitido para: ").append(commonName(leaf.getSubjectX500Principal())).append('\n');
sb.append("Emitido por: ").append(commonName(leaf.getIssuerX500Principal())).append('\n');
sb.append("Válido ata: ").append(Util.date(leaf.getNotAfter().getTime(), true)).append("\n\n");
sb.append("Pegada dixital SHA-256:\n").append(fingerprint(leaf)).append("\n\n");
sb.append("Continúa só se coincide coa pegada que amosa o navegador do teu ordenador para este sitio.");
if (top != leaf) {
sb.append("\n\nMiniABS will then trust certificates issued by \"")
.append(commonName(top.getSubjectX500Principal())).append("\".");
sb.append("\n\nMiniABS confiará desde agora nos certificados emitidos por «")
.append(commonName(top.getSubjectX500Principal())).append("».");
}
return sb.toString();
}
+4 -4
View File
@@ -24,19 +24,19 @@ final class TrustPrompt {
try {
host = new URL(Prefs.get(a).server()).getHost();
} catch (Exception e) {
host = "the server";
host = "o servidor";
}
showing = true;
new AlertDialog.Builder(a)
.setTitle("Check this certificate")
.setTitle("Comproba este certificado")
.setMessage(Trust.describe(r, host))
.setPositiveButton("Trust", new DialogInterface.OnClickListener() {
.setPositiveButton("Confiar", new DialogInterface.OnClickListener() {
public void onClick(DialogInterface d, int w) {
Trust.pin(a, r.chain);
if (retry != null) retry.run();
}
})
.setNegativeButton("Cancel", null)
.setNegativeButton("Cancelar", null)
.setOnCancelListener(null)
.show()
.setOnDismissListener(new DialogInterface.OnDismissListener() {
+28 -19
View File
@@ -5,10 +5,7 @@ import java.net.ConnectException;
import java.net.SocketException;
import java.net.SocketTimeoutException;
import java.net.UnknownHostException;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.Locale;
import javax.net.ssl.SSLException;
@@ -47,12 +44,24 @@ final class Util {
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) {
if (ms <= 0) return "";
Calendar now = Calendar.getInstance(), c = Calendar.getInstance();
c.setTimeInMillis(ms);
String pattern = c.get(Calendar.YEAR) == now.get(Calendar.YEAR) ? "d MMM" : "d MMM yyyy";
return new SimpleDateFormat(pattern, Locale.getDefault()).format(new Date(ms));
return date(ms, c.get(Calendar.YEAR) != now.get(Calendar.YEAR));
}
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) {
@@ -87,28 +96,28 @@ final class Util {
if (e instanceof Api.AuthException) return e.getMessage();
if (e instanceof Api.ApiException) {
Api.ApiException a = (Api.ApiException) e;
if (a.code == 401) return "Not signed in. Please log in again.";
if (a.code == 403) return "Your account isn't allowed to do that.";
if (a.code == 404) return "Not found on the server.";
if (a.code == 401) return "Non iniciaches sesión. Inicia sesión de novo.";
if (a.code == 403) return "A túa conta non ten permiso para facer iso.";
if (a.code == 404) return "Non se atopou no servidor.";
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 SocketTimeoutException) return "The server took too long to answer.";
if (e instanceof ConnectException) return "Can't connect to the server. Is it running and reachable?";
if (e instanceof UnknownHostException) return "Non se atopou o servidor. Comproba o enderezo e a conexión.";
if (e instanceof SocketTimeoutException) return "O servidor tardou demasiado en responder.";
if (e instanceof ConnectException) return "Non se pode conectar co servidor. Está en marcha e accesible?";
if (e instanceof SSLException) {
Trust.Rejected r = Trust.recentRejection();
if (r != null) {
return r.expired() ? "The server's certificate has expired, or the phone's date is wrong."
: "This phone doesn't recognise the server's certificate.";
return r.expired() ? "O certificado do servidor caducou ou a data do teléfono é incorrecta."
: "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 "
+ "a legacy HTTPS port (see README). On your home network, the http:// address works too.";
return "Non se puido establecer unha conexión segura. Android 2.3 só admite TLS 1.0, así que o servidor "
+ "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 FileNotFoundException) return "Not found on the server.";
if (e instanceof SocketException) return "Cortouse a conexión. Comproba a rede.";
if (e instanceof FileNotFoundException) return "Non se atopou no servidor.";
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;
}
}
+1 -1
View File
@@ -55,7 +55,7 @@ public class ApiTest {
Api.login(c, p.server(), "demo", "wrong");
check(false, "wrong password rejected");
} 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");
check(lib.equals("lib-pod"), "login returns default library");
+4 -4
View File
@@ -88,13 +88,13 @@ public class TrustTest {
Exception e = tryStatus(good);
Trust.Rejected r = Trust.recentRejection();
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) + "\"");
String text = Trust.describe(r, "localhost");
byte[] d = MessageDigest.getInstance("SHA-256").digest(leaf.getEncoded());
String fp = String.format("%02X:%02X:%02X", d[0], d[1], d[2]);
check(text.contains(fp) && text.contains("Issued by: Test Intermediate R1")
&& text.contains("trust certificates issued by \"Test Intermediate R1\""),
check(text.contains(fp) && text.contains("Emitido por: Test Intermediate R1")
&& text.contains("certificados emitidos por «Test Intermediate R1»"),
"prompt shows the server's fingerprint and what will be trusted");
X509Certificate pinned = Trust.pin(c, r.chain);
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");
Exception ee = tryStatus(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) + "\"");
System.out.println("Self-signed server");
+39 -38
View File
@@ -1,6 +1,11 @@
"""Draws MiniABS's PNG icons for ldpi (Galaxy Mini), mdpi and hdpi.
Android 2.x has no vector drawables, so every icon is rendered here at 4x and
The launcher icon (res/drawable-*/ic_launcher.png) comes from the supplied
Audiobookshelf icon pack and is not generated here. The notification icon is
cut out of that same artwork (art/icon_master_512.png): the white headphones
and equalizer become a white silhouette, as Android 2.3's status bar expects.
Android 2.x has no vector drawables, so the player icons are rendered at 4x and
downsampled for smooth edges.
"""
import os
@@ -8,9 +13,9 @@ import sys
from PIL import Image, ImageDraw
RES = sys.argv[1]
ROOT = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..")
DENS = {"ldpi": 0.75, "mdpi": 1.0, "hdpi": 1.5}
AMBER = (245, 166, 35, 255)
DARK = (18, 21, 25, 255)
LILAC = (188, 138, 252, 255) # the icon's sound-wave colour (#BC8AFC)
WHITE = (255, 255, 255, 255)
SS = 4 # supersampling
@@ -27,36 +32,32 @@ def save(img, px, folder, name):
out.save(os.path.join(d, name + ".png"))
def broadcast(d, s, cx, cy, scale, color, bg):
"""Podcast mark: a dot with two arcs on each side."""
r = s * 0.075 * scale
d.ellipse([cx - r, cy - r, cx + r, cy + r], fill=color)
w = max(1, int(s * 0.07 * scale))
for k in (0.22, 0.37):
rr = s * k * scale
box = [cx - rr, cy - rr, cx + rr, cy + rr]
d.arc(box, 140, 220, fill=color, width=w)
d.arc(box, -40, 40, fill=color, width=w)
def status_silhouette():
"""White mask of the icon's light parts (headphones, bars, waves) on transparency."""
src = Image.open(os.path.join(ROOT, "art", "icon_master_512.png")).convert("RGBA")
out = Image.new("RGBA", src.size, (0, 0, 0, 0))
sp, op = src.load(), out.load()
for y in range(src.size[1]):
for x in range(src.size[0]):
r, g, b, a = sp[x, y]
# the purple background has a low green channel; the white/lilac artwork a high one
level = min(255, max(0, (g - 80) * 255 // 50))
op[x, y] = (255, 255, 255, level * a // 255)
box = out.getbbox()
out = out.crop(box)
side = max(out.size)
sq = Image.new("RGBA", (side, side), (0, 0, 0, 0))
sq.paste(out, ((side - out.size[0]) // 2, (side - out.size[1]) // 2))
return sq
def launcher(px, folder):
img, s = canvas(px)
d = ImageDraw.Draw(img)
m = s * 0.06
d.rounded_rectangle([m, m, s - m, s - m], radius=s * 0.2, fill=DARK, outline=AMBER, width=int(s * 0.04))
broadcast(d, s, s / 2, s * 0.44, 1.0, AMBER, DARK)
# little stand under the mark
w = s * 0.05
d.rectangle([s / 2 - w / 2, s * 0.5, s / 2 + w / 2, s * 0.74], fill=AMBER)
d.rounded_rectangle([s * 0.36, s * 0.72, s * 0.64, s * 0.78], radius=w / 2, fill=AMBER)
save(img, px, folder, "ic_launcher")
def status(px, folder):
img, s = canvas(px)
d = ImageDraw.Draw(img)
broadcast(d, s, s / 2, s / 2, 1.25, WHITE, None)
save(img, px, folder, "ic_stat")
def status(sil, px, folder):
pad = max(1, round(px * 0.06))
inner = sil.resize((px - 2 * pad, px - 2 * pad), Image.LANCZOS)
img = Image.new("RGBA", (px, px), (0, 0, 0, 0))
img.paste(inner, (pad, pad))
d = os.path.join(RES, "drawable-" + folder)
img.save(os.path.join(d, "ic_stat.png"))
def play(px, folder, name, color):
@@ -85,12 +86,12 @@ def refresh(px, folder):
save(img, px, folder, "ic_refresh")
sil = status_silhouette()
for folder, k in DENS.items():
launcher(round(48 * k), folder)
status(round(25 * k), folder)
play(round(40 * k), folder, "ic_play", DARK)
pause(round(40 * k), folder, "ic_pause", DARK)
play(round(28 * k), folder, "ic_play_small", AMBER)
pause(round(28 * k), folder, "ic_pause_small", AMBER)
refresh(round(24 * k), folder)
status(sil, round(25 * k), folder)
play(round(32 * k), folder, "ic_play", WHITE) # on the purple play button
pause(round(32 * k), folder, "ic_pause", WHITE)
play(round(38 * k), folder, "ic_play_small", LILAC) # mini player bar, on dark
pause(round(38 * k), folder, "ic_pause_small", LILAC)
refresh(round(30 * k), folder)
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")