4 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
40 changed files with 123 additions and 78 deletions

No files matched your search

+2 -2
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="3" android:versionCode="7"
android:versionName="1.2" 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" />
+4 -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, ~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
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: 513 B

After

Width:  |  Height:  |  Size: 272 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 510 B

After

Width:  |  Height:  |  Size: 533 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 994 B

After

Width:  |  Height:  |  Size: 588 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

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

After

Width:  |  Height:  |  Size: 238 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 413 B

After

Width:  |  Height:  |  Size: 434 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 656 B

After

Width:  |  Height:  |  Size: 393 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 656 B

After

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

After

Width:  |  Height:  |  Size: 301 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 486 B

After

Width:  |  Height:  |  Size: 494 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 760 B

After

Width:  |  Height:  |  Size: 376 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 834 B

After

Width:  |  Height:  |  Size: 806 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>
+1 -1
View File
@@ -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>
+1 -1
View File
@@ -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"
+3 -3
View File
@@ -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"
+5 -5
View File
@@ -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" />
@@ -129,10 +129,10 @@
<ImageButton <ImageButton
android:id="@+id/play" android:id="@+id/play"
android:layout_width="@dimen/play_size" android:layout_width="@dimen/play_w"
android:layout_height="@dimen/play_size" android:layout_height="@dimen/play_h"
android:layout_marginLeft="14dp" android:layout_marginLeft="@dimen/play_gap"
android:layout_marginRight="14dp" 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" />
+1 -1
View File
@@ -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" />
+1 -1
View File
@@ -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" />
+6 -4
View File
@@ -41,10 +41,12 @@
<dimen name="player_meta">13sp</dimen> <dimen name="player_meta">13sp</dimen>
<dimen name="player_time">15sp</dimen> <dimen name="player_time">15sp</dimen>
<dimen name="player_desc">15sp</dimen> <dimen name="player_desc">15sp</dimen>
<dimen name="skip_w">82dp</dimen> <dimen name="skip_w">62dp</dimen>
<dimen name="skip_h">56dp</dimen> <dimen name="skip_h">42dp</dimen>
<dimen name="skip_text">20sp</dimen> <dimen name="skip_text">16sp</dimen>
<dimen name="play_size">78dp</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_thumb">26dp</dimen>
<dimen name="seek_track">7dp</dimen> <dimen name="seek_track">7dp</dimen>
+13 -9
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">#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>
+1 -1
View File
@@ -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.2" 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>
+1 -1
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.2"; static final String VERSION = "1.6";
static class ApiException extends IOException { static class ApiException extends IOException {
final int code; final int code;
+12
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));
} }
@@ -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);
} }
+6
View File
@@ -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();
+22 -3
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;
@@ -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();
+1 -1
View File
@@ -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));
} }
} }
+38 -37
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(48 * k), folder, "ic_play", DARK) pause(round(32 * k), folder, "ic_pause", WHITE)
pause(round(48 * 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")