5 Commits
Author SHA1 Message Date
XoséandClaude Opus 5.5 6cbe3596e4 MiniABS 1.6: never mark early endings as played, stall warning, Close
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 16:13:26 +02:00
XoséandClaude Opus 5.5 a6d2c8b2d2 Backport from the iOS version: never mark early endings as played, stall warning, Close
- An episode only counts as played when playback reaches the last minute.
  Saved files that "complete" early (cut-short download, SD card problems) were
  marked as played on the server, as were streams ending 30-60 s early; now a
  stream is retried and a file is reported.
- "Playing" but the position not moving for 12 s: say so.
- Menu -> "Pechar MiniABS" (Latest/Shows/Saved and the player): save the
  position, stop playback and close the app.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 16:05:13 +02:00
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 807 additions and 430 deletions

No files matched your search

+3 -3
View File
@@ -1,8 +1,8 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.miniabs" package="com.miniabs"
android:versionCode="2" android:versionCode="7"
android:versionName="1.1" android:versionName="1.6"
android:installLocation="auto"> android:installLocation="auto">
<uses-sdk android:minSdkVersion="8" android:targetSdkVersion="10" /> <uses-sdk android:minSdkVersion="8" android:targetSdkVersion="10" />
@@ -54,7 +54,7 @@
<activity <activity
android:name=".SettingsActivity" android:name=".SettingsActivity"
android:label="Settings" android:label="Axustes"
android:theme="@android:style/Theme" android:theme="@android:style/Theme"
android:screenOrientation="portrait" android:screenOrientation="portrait"
android:configChanges="keyboardHidden|orientation" /> android:configChanges="keyboardHidden|orientation" />
+6 -3
View File
@@ -1,6 +1,6 @@
# MiniABS # MiniABS
A tiny Audiobookshelf **podcast** client for the Samsung Galaxy Mini (GT-S5570) and other Android 2.2+ phones. Native Java, no libraries, ~100 KB. A tiny Audiobookshelf **podcast** client for the Samsung Galaxy Mini (GT-S5570) and other Android 2.2+ phones. Native Java, no libraries, ~130 KB. The interface is in Galician, with large text and touch targets sized for the Mini's 240×320 screen. 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 ## Install
@@ -68,14 +68,15 @@ Android 2.x's media player can't stream https:// URLs, so for HTTPS servers Mini
- **Shows**: every podcast; tap one for its episodes. Menu key → sort oldest/newest, hide played. - **Shows**: every podcast; tap one for its episodes. Menu key → sort oldest/newest, hide played.
- **Saved**: episodes downloaded to the SD card, plus download progress. - **Saved**: episodes downloaded to the SD card, plus download progress.
- **Tap** an episode to play. **Long-press** for Download, Delete download, Mark as played/unplayed, Go to show. - **Tap** an episode to play. **Long-press** for Download, Delete download, Mark as played/unplayed, Go to show.
- **Player**: skip back/forward (set the amounts in Settings), seek bar, show notes. Menu key → sleep timer, mark as played, download, stop. - **Player**: skip back/forward (set the amounts in Settings), seek bar, show notes. Menu key → sleep timer, mark as played, download, stop, close MiniABS.
- **Close**: Menu key → *Pechar MiniABS* (on the Latest/Shows/Saved screen or the player) saves the position, stops playback and closes the app.
- **Headset buttons**: play/pause; next/previous skip forward/back. Playback pauses when headphones are unplugged and during calls. - **Headset buttons**: play/pause; next/previous skip forward/back. Playback pauses when headphones are unplugged and during calls.
Colored dots: blue = new, amber = in progress (with a bar), grey = played. Colored dots: blue = new, amber = in progress (with a bar), grey = played.
## Progress sync ## Progress sync
Playback opens an Audiobookshelf session and syncs every 15 seconds and on pause, so progress follows you to other apps. Saved episodes play offline; their positions are kept on the phone and sent to the server next time you're online (or tap refresh on the Saved tab). Playback opens an Audiobookshelf session and syncs every 15 seconds and on pause, so progress follows you to other apps. An episode only counts as played when playback gets within a minute of its end: a stream that drops or a saved file cut short is retried or reported, never marked as played. If playback says it's playing but stops moving for 12 seconds, MiniABS says so. Saved episodes play offline; their positions are kept on the phone and sent to the server next time you're online (or tap refresh on the Saved tab).
## Limits ## Limits
@@ -91,6 +92,8 @@ Needs a JDK, `aapt`, `dalvik-exchange` (dx), `zipalign`, `apksigner` (Ubuntu: `a
Compiling against the API 8 jar guarantees no newer Android APIs are used. The APK is signed with `miniabs.keystore` (password `miniabs`); keep it to install updates over the top. Compiling against the API 8 jar guarantees no newer Android APIs are used. The APK is signed with `miniabs.keystore` (password `miniabs`); keep it to install updates over the top.
All sizes (text, rows, buttons, covers) are in `res/values/dimens.xml`; change them there and rebuild.
`tools/gen_roots.py` regenerates the bundled root certificates from a system CA store. `tools/gen_roots.py` regenerates the bundled root certificates from a system CA store.
`test/run_tests.sh` runs the API, JSON, progress, HTTPS-trust and streaming-proxy code on the desktop JVM against a mock Audiobookshelf server (`test/mock_abs.py`), over plain HTTP and over TLS with good, expired, wrong-host, forged and self-signed certificates (`test/make_pki.sh`). `test/run_tests.sh` runs the API, JSON, progress, HTTPS-trust and streaming-proxy code on the desktop JVM against a mock Audiobookshelf server (`test/mock_abs.py`), over plain HTTP and over TLS with good, expired, wrong-host, forged and self-signed certificates (`test/make_pki.sh`).
Binary file not shown.

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