Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6cbe3596e4 | ||
|
|
a6d2c8b2d2 | ||
|
|
2c23ec9131 |
No files matched your search
@@ -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="5"
|
android:versionCode="7"
|
||||||
android:versionName="1.4"
|
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" />
|
||||||
|
|||||||
@@ -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, ~130 KB. The interface is in Galician, with large text and touch targets sized for the Mini's 240×320 screen.
|
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
|
||||||
|
|
||||||
|
|||||||
|
After Width: | Height: | Size: 224 KiB |
|
Before Width: | Height: | Size: 5.2 KiB After Width: | Height: | Size: 7.8 KiB |
|
Before Width: | Height: | Size: 312 B After Width: | Height: | Size: 272 B |
|
Before Width: | Height: | Size: 510 B After Width: | Height: | Size: 533 B |
|
Before Width: | Height: | Size: 760 B After Width: | Height: | Size: 588 B |
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.0 KiB |
|
Before Width: | Height: | Size: 690 B After Width: | Height: | Size: 977 B |
|
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 354 B After Width: | Height: | Size: 238 B |
|
Before Width: | Height: | Size: 413 B After Width: | Height: | Size: 434 B |
|
Before Width: | Height: | Size: 467 B After Width: | Height: | Size: 393 B |
|
Before Width: | Height: | Size: 656 B After Width: | Height: | Size: 647 B |
|
Before Width: | Height: | Size: 326 B After Width: | Height: | Size: 434 B |
|
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 4.1 KiB |
|
Before Width: | Height: | Size: 355 B After Width: | Height: | Size: 301 B |
|
Before Width: | Height: | Size: 486 B After Width: | Height: | Size: 494 B |
|
Before Width: | Height: | Size: 561 B After Width: | Height: | Size: 376 B |
|
Before Width: | Height: | Size: 834 B After Width: | Height: | Size: 806 B |
|
Before Width: | Height: | Size: 457 B After Width: | Height: | Size: 567 B |
|
After Width: | Height: | Size: 12 KiB |
@@ -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>
|
||||||
@@ -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">
|
||||||
|
|||||||
@@ -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><solid android:color="#FFC766" /><corners android:radius="23dp" /></shape>
|
<shape><gradient android:startColor="#AA5CFF" android:endColor="#7022E6" android:angle="270" /><corners android:radius="23dp" /></shape>
|
||||||
</item>
|
</item>
|
||||||
<item>
|
<item>
|
||||||
<shape><solid android:color="@color/accent" /><corners android:radius="23dp" /></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>
|
||||||
@@ -1,6 +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" />
|
||||||
<stroke android:width="2dp" android:color="@color/bg" />
|
<stroke android:width="2dp" android:color="@color/bg" />
|
||||||
<size android:width="@dimen/seek_thumb" android:height="@dimen/seek_thumb" />
|
<size android:width="@dimen/seek_thumb" android:height="@dimen/seek_thumb" />
|
||||||
</shape>
|
</shape>
|
||||||
@@ -12,7 +12,7 @@
|
|||||||
android:text="MiniABS"
|
android:text="MiniABS"
|
||||||
android:textSize="@dimen/header_text"
|
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"
|
||||||
|
|||||||
@@ -34,7 +34,7 @@
|
|||||||
android:text="MiniABS"
|
android:text="MiniABS"
|
||||||
android:textSize="@dimen/login_title"
|
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"
|
||||||
@@ -82,7 +82,7 @@
|
|||||||
android:text="Conectar"
|
android:text="Conectar"
|
||||||
android:textStyle="bold"
|
android:textStyle="bold"
|
||||||
android:textSize="@dimen/login_btn_text"
|
android:textSize="@dimen/login_btn_text"
|
||||||
android:textColor="#121519" />
|
android:textColor="#FFFFFF" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/status"
|
android:id="@+id/status"
|
||||||
@@ -90,7 +90,7 @@
|
|||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginTop="10dp"
|
android:layout_marginTop="10dp"
|
||||||
android:textSize="@dimen/login_status"
|
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"
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
android:paddingRight="10dp"
|
android:paddingRight="10dp"
|
||||||
android:textSize="@dimen/header_sub"
|
android:textSize="@dimen/header_sub"
|
||||||
android:textStyle="bold"
|
android:textStyle="bold"
|
||||||
android:textColor="@color/accent"
|
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,7 +27,7 @@
|
|||||||
android:layout_width="fill_parent"
|
android:layout_width="fill_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:textSize="@dimen/text_show"
|
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" />
|
||||||
|
|
||||||
|
|||||||
@@ -6,4 +6,4 @@
|
|||||||
android:gravity="center"
|
android:gravity="center"
|
||||||
android:textSize="@dimen/text_more"
|
android:textSize="@dimen/text_more"
|
||||||
android:textStyle="bold"
|
android:textStyle="bold"
|
||||||
android:textColor="@color/accent" />
|
android:textColor="@color/accent_light" />
|
||||||
@@ -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">#A9B2BC</color>
|
<color name="text">#EEEBF3</color>
|
||||||
<color name="dim">#6E7882</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>
|
||||||
|
|||||||
@@ -50,7 +50,7 @@
|
|||||||
<PreferenceCategory android:title="Acerca de">
|
<PreferenceCategory android:title="Acerca de">
|
||||||
<Preference
|
<Preference
|
||||||
android:key="about"
|
android:key="about"
|
||||||
android:title="MiniABS 1.4"
|
android:title="MiniABS 1.5"
|
||||||
android:summary="Un pequeno cliente de podcasts de Audiobookshelf para Android 2.2+" />
|
android:summary="Un pequeno cliente de podcasts de Audiobookshelf para Android 2.2+" />
|
||||||
</PreferenceCategory>
|
</PreferenceCategory>
|
||||||
</PreferenceScreen>
|
</PreferenceScreen>
|
||||||
@@ -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.4";
|
static final String VERSION = "1.6";
|
||||||
|
|
||||||
static class ApiException extends IOException {
|
static class ApiException extends IOException {
|
||||||
final int code;
|
final int code;
|
||||||
|
|||||||
@@ -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));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -495,9 +496,17 @@ public class HomeActivity extends BaseActivity {
|
|||||||
menu.add(0, 2, 0, "Biblioteca").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, "En reprodución").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, "Axustes").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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -245,11 +245,17 @@ public class PlayerActivity extends BaseActivity {
|
|||||||
}
|
}
|
||||||
menu.add(0, 4, 0, "Ir ao programa").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, "Deter").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();
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -552,10 +555,16 @@ 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("Perdeuse a conexión, reconectando…");
|
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;
|
||||||
@@ -672,6 +681,16 @@ 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();
|
||||||
|
|||||||
@@ -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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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(32 * k), folder, "ic_play", DARK)
|
pause(round(32 * k), folder, "ic_pause", WHITE)
|
||||||
pause(round(32 * k), folder, "ic_pause", DARK)
|
play(round(38 * k), folder, "ic_play_small", LILAC) # mini player bar, on dark
|
||||||
play(round(38 * k), folder, "ic_play_small", AMBER)
|
pause(round(38 * k), folder, "ic_pause_small", LILAC)
|
||||||
pause(round(38 * k), folder, "ic_pause_small", AMBER)
|
|
||||||
refresh(round(30 * k), folder)
|
refresh(round(30 * k), folder)
|
||||||
print("icons ok")
|
print("icons ok")
|
||||||