6 Commits
v1.3 ... main

Author SHA1 Message Date
2e1bf3907d Collapse interval bell behind underlined dropdown; 5 selectable gong sounds with preview
All checks were successful
Build & Release APK / build (push) Successful in 6m11s
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-07 23:38:33 -07:00
f0cdddf2d4 Interval bell setting: Off/5/10/15/20/30 min, distinct soft bell, intervalMin synced
All checks were successful
Build & Release APK / build (push) Successful in 2m54s
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-07 23:08:05 -07:00
688147b8ef Pull server session history into local log (merge, legacy Jhana sessions included)
All checks were successful
Build & Release APK / build (push) Successful in 6m49s
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-07 21:58:35 -07:00
b36a9c636b Prettier adaptive launcher icon: gradient gong with boss, aura, themed-icon monochrome
All checks were successful
Build & Release APK / build (push) Successful in 6m39s
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-07 01:38:10 -07:00
d9b886cdc6 Fix unclickable tabs: pad for status/nav bars under enforced edge-to-edge
All checks were successful
Build & Release APK / build (push) Successful in 6m49s
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-07 01:29:33 -07:00
cc800e756b Add Reading tab with Jhanas Guide and Retreat Instructions PDFs
Some checks failed
Build & Release APK / build (push) Has been cancelled
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-07 01:23:16 -07:00
18 changed files with 412 additions and 5 deletions

View File

@@ -51,6 +51,7 @@ android {
dependencies { dependencies {
val composeBom = platform("androidx.compose:compose-bom:2024.10.01") val composeBom = platform("androidx.compose:compose-bom:2024.10.01")
implementation(composeBom) implementation(composeBom)
implementation("androidx.core:core-ktx:1.13.1")
implementation("androidx.compose.ui:ui") implementation("androidx.compose.ui:ui")
implementation("androidx.compose.material3:material3") implementation("androidx.compose.material3:material3")
implementation("androidx.activity:activity-compose:1.9.3") implementation("androidx.activity:activity-compose:1.9.3")

View File

@@ -9,7 +9,7 @@
<application <application
android:label="Meditation" android:label="Meditation"
android:icon="@drawable/ic_gong" android:icon="@mipmap/ic_launcher"
android:theme="@android:style/Theme.Material.NoActionBar" android:theme="@android:style/Theme.Material.NoActionBar"
android:allowBackup="true"> android:allowBackup="true">
@@ -27,6 +27,16 @@
android:name=".TimerService" android:name=".TimerService"
android:exported="false" android:exported="false"
android:foregroundServiceType="mediaPlayback" /> android:foregroundServiceType="mediaPlayback" />
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="com.marcus.meditationtimer.files"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
</application> </application>
</manifest> </manifest>

Binary file not shown.

Binary file not shown.

View File

@@ -1,10 +1,14 @@
package com.marcus.meditationtimer package com.marcus.meditationtimer
import android.Manifest import android.Manifest
import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.widget.Toast
import androidx.core.content.FileProvider
import java.io.File
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
@@ -22,13 +26,17 @@ import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.ui.draw.clip
import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.material3.AlertDialog import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.ButtonDefaults
@@ -128,6 +136,10 @@ private fun App() {
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.background(DeepBlue) .background(DeepBlue)
// app targets SDK 35 → edge-to-edge is forced; without these the tab
// row sits under the status bar and taps never reach it
.statusBarsPadding()
.navigationBarsPadding()
) { ) {
TabRow( TabRow(
selectedTabIndex = tab, selectedTabIndex = tab,
@@ -136,6 +148,7 @@ private fun App() {
) { ) {
Tab(selected = tab == 0, onClick = { tab = 0 }, text = { Text("Timer") }) Tab(selected = tab == 0, onClick = { tab = 0 }, text = { Text("Timer") })
Tab(selected = tab == 1, onClick = { tab = 1 }, text = { Text("History") }) Tab(selected = tab == 1, onClick = { tab = 1 }, text = { Text("History") })
Tab(selected = tab == 2, onClick = { tab = 2 }, text = { Text("Reading") })
} }
when (tab) { when (tab) {
0 -> TimerTab() 0 -> TimerTab()
@@ -143,6 +156,7 @@ private fun App() {
onEditSession = { ratingFor = it }, onEditSession = { ratingFor = it },
onOpenToken = { showTokenDialog = true }, onOpenToken = { showTokenDialog = true },
) )
2 -> ReadingTab()
} }
} }
@@ -172,6 +186,9 @@ private fun TimerTab() {
val context = LocalContext.current val context = LocalContext.current
val timer by TimerService.state.collectAsState() val timer by TimerService.state.collectAsState()
var durationMin by rememberSaveable { mutableIntStateOf(20) } var durationMin by rememberSaveable { mutableIntStateOf(20) }
val settings = remember { context.getSharedPreferences("settings", android.content.Context.MODE_PRIVATE) }
var intervalMin by rememberSaveable { mutableIntStateOf(settings.getInt("interval_min", 0)) }
var gongKey by rememberSaveable { mutableStateOf(settings.getString("gong_sound", "bowl") ?: "bowl") }
val phase = timer.phase val phase = timer.phase
val selectedTotalMs = durationMin * 60_000L val selectedTotalMs = durationMin * 60_000L
@@ -199,9 +216,25 @@ private fun TimerTab() {
if (phase == Phase.Idle || phase == Phase.Done) { if (phase == Phase.Idle || phase == Phase.Done) {
MinutePicker(durationMin = durationMin, onChange = { durationMin = it }) MinutePicker(durationMin = durationMin, onChange = { durationMin = it })
Spacer(Modifier.height(32.dp)) Spacer(Modifier.height(20.dp))
IntervalBellRow(
intervalMin = intervalMin,
onChange = {
intervalMin = it
settings.edit().putInt("interval_min", it).apply()
},
)
Spacer(Modifier.height(6.dp))
GongSoundRow(
soundKey = gongKey,
onChange = {
gongKey = it
settings.edit().putString("gong_sound", it).apply()
},
)
Spacer(Modifier.height(16.dp))
Button( Button(
onClick = { TimerService.start(context, selectedTotalMs) }, onClick = { TimerService.start(context, selectedTotalMs, intervalMin) },
colors = ButtonDefaults.buttonColors(containerColor = Gold, contentColor = DeepBlue), colors = ButtonDefaults.buttonColors(containerColor = Gold, contentColor = DeepBlue),
) { ) {
Text(if (phase == Phase.Done) "Again" else "Begin", fontSize = 18.sp) Text(if (phase == Phase.Done) "Again" else "Begin", fontSize = 18.sp)
@@ -247,6 +280,88 @@ private fun TimerTab() {
} }
} }
private val INTERVAL_OPTIONS = listOf(0, 5, 10, 15, 20, 30)
@Composable
private fun IntervalBellRow(intervalMin: Int, onChange: (Int) -> Unit) {
// Collapsed by default — a small underlined label that expands the options.
var expanded by rememberSaveable { mutableStateOf(false) }
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Text(
text = if (intervalMin == 0) "Interval Timer" else "Interval Timer · ${intervalMin}m",
color = Gold.copy(alpha = if (expanded) 0.9f else 0.55f),
fontSize = 13.sp,
textDecoration = TextDecoration.Underline,
modifier = Modifier
.clickable { expanded = !expanded }
.padding(horizontal = 12.dp, vertical = 6.dp),
)
if (expanded) {
Spacer(Modifier.height(6.dp))
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
INTERVAL_OPTIONS.forEach { min ->
val selected = intervalMin == min
Text(
text = if (min == 0) "Off" else "${min}m",
color = if (selected) DeepBlue else Gold.copy(alpha = 0.7f),
fontSize = 13.sp,
modifier = Modifier
.clip(RoundedCornerShape(14.dp))
.background(if (selected) Gold else Color.White.copy(alpha = 0.06f))
.clickable {
onChange(min)
expanded = false
}
.padding(horizontal = 12.dp, vertical = 6.dp),
)
}
}
}
}
}
@Composable
private fun GongSoundRow(soundKey: String, onChange: (String) -> Unit) {
var expanded by rememberSaveable { mutableStateOf(false) }
val context = LocalContext.current
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Text(
text = "Gong Sound · ${GONG_SOUNDS.firstOrNull { it.first == soundKey }?.second ?: "Bowl"}",
color = Gold.copy(alpha = if (expanded) 0.9f else 0.55f),
fontSize = 13.sp,
textDecoration = TextDecoration.Underline,
modifier = Modifier
.clickable { expanded = !expanded }
.padding(horizontal = 12.dp, vertical = 6.dp),
)
if (expanded) {
Spacer(Modifier.height(6.dp))
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
GONG_SOUNDS.forEach { (key, label) ->
val selected = soundKey == key
Text(
text = label,
color = if (selected) DeepBlue else Gold.copy(alpha = 0.7f),
fontSize = 13.sp,
modifier = Modifier
.clip(RoundedCornerShape(14.dp))
.background(if (selected) Gold else Color.White.copy(alpha = 0.06f))
.clickable {
onChange(key)
// Preview the chosen gong immediately.
android.media.MediaPlayer.create(context, gongRes(key))?.apply {
setOnCompletionListener { it.release() }
start()
}
}
.padding(horizontal = 12.dp, vertical = 6.dp),
)
}
}
}
}
}
private val PRESETS = listOf(20, 45, 60, 90) private val PRESETS = listOf(20, 45, 60, 90)
private const val MAX_MINUTES = 180 private const val MAX_MINUTES = 180
@@ -330,6 +445,9 @@ private fun HistoryTab(onEditSession: (Session) -> Unit, onOpenToken: () -> Unit
val sessions by SessionStore.sessions.collectAsState() val sessions by SessionStore.sessions.collectAsState()
val pending by SessionStore.pendingSync.collectAsState() val pending by SessionStore.pendingSync.collectAsState()
// Pull the server history (incl. legacy Jhana-app sessions) on open.
LaunchedEffect(Unit) { SessionStore.refresh() }
Column( Column(
Modifier Modifier
.fillMaxSize() .fillMaxSize()
@@ -430,6 +548,67 @@ private fun SessionRow(s: Session, onClick: () -> Unit) {
} }
} }
// -------------------------------------------------------------- Reading tab
private data class Doc(val title: String, val subtitle: String, val asset: String)
private val DOCS = listOf(
Doc("Pragmatic Jhanas Guide", "v4.3", "jhanas-guide.pdf"),
Doc("Retreat Instructions", "v6.0.3 · Day 1 & 2", "retreat-instructions.pdf"),
)
@Composable
private fun ReadingTab() {
val context = LocalContext.current
Column(
Modifier
.fillMaxSize()
.padding(20.dp)
) {
DOCS.forEach { doc ->
Column(
Modifier
.fillMaxWidth()
.clickable { openPdf(context, doc) }
.background(Color.White.copy(alpha = 0.05f), RoundedCornerShape(12.dp))
.padding(20.dp)
) {
Text(doc.title, color = Color.White.copy(alpha = 0.9f), fontSize = 16.sp)
Spacer(Modifier.height(4.dp))
Text(doc.subtitle, color = Dim, fontSize = 12.sp)
}
Spacer(Modifier.height(12.dp))
}
Text(
"Opens in the system PDF viewer.",
color = Dim,
fontSize = 11.sp,
textAlign = TextAlign.Center,
modifier = Modifier.fillMaxWidth(),
)
}
}
private fun openPdf(context: Context, doc: Doc) {
try {
val dir = File(context.cacheDir, "reading").apply { mkdirs() }
val outFile = File(dir, doc.asset)
if (!outFile.exists() || outFile.length() == 0L) {
context.assets.open("reading/${doc.asset}").use { input ->
outFile.outputStream().use { input.copyTo(it) }
}
}
val uri = FileProvider.getUriForFile(context, "${context.packageName}.files", outFile)
context.startActivity(
Intent(Intent.ACTION_VIEW)
.setDataAndType(uri, "application/pdf")
.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
)
} catch (e: Exception) {
Toast.makeText(context, "Could not open PDF: ${e.message}", Toast.LENGTH_SHORT).show()
}
}
// ----------------------------------------------------------------- Dialogs // ----------------------------------------------------------------- Dialogs
@Composable @Composable

View File

@@ -22,6 +22,7 @@ data class Session(
val pauseCount: Int, val pauseCount: Int,
val rating: Int? = null, val rating: Int? = null,
val note: String? = null, val note: String? = null,
val intervalMin: Int? = null,
val synced: Boolean = false, val synced: Boolean = false,
) { ) {
val completionPct: Int val completionPct: Int
@@ -55,6 +56,7 @@ object SessionStore {
publish(load()) publish(load())
} }
syncPending() syncPending()
refresh()
} }
var token: String var token: String
@@ -93,6 +95,69 @@ object SessionStore {
} }
} }
/**
* Pull the full server history (incl. sessions from other apps/devices,
* e.g. the legacy Jhana app) and merge it into the local log. Local
* sessions still awaiting push (synced=false) always win over the server
* copy; everything else takes the server's version.
*/
fun refresh() {
val tok = token
if (tok.isEmpty()) return
scope.launch {
val remote = fetchSessions(tok) ?: return@launch
synchronized(lock) {
val local = load().associateBy { it.id }
val merged = LinkedHashMap(local)
for (r in remote) {
val l = local[r.id]
if (l == null || l.synced) {
// Keep any locally-entered rating/note if the server has none.
merged[r.id] = r.copy(
rating = r.rating ?: l?.rating,
note = r.note ?: l?.note,
synced = true,
)
}
}
persist(merged.values.toList())
}
}
}
private fun fetchSessions(tok: String): List<Session>? = try {
val conn = URL("$BASE_URL/v1/sessions?limit=1000").openConnection() as HttpURLConnection
conn.requestMethod = "GET"
conn.setRequestProperty("Authorization", "Bearer $tok")
conn.connectTimeout = 10_000
conn.readTimeout = 10_000
val body = if (conn.responseCode in 200..299) {
conn.inputStream.bufferedReader().use { it.readText() }
} else null
conn.disconnect()
body?.let {
val arr = JSONObject(it).getJSONArray("sessions")
(0 until arr.length()).map { i ->
val o = arr.getJSONObject(i)
Session(
id = o.getString("id"),
startedAt = o.getString("startedAt"),
endedAt = o.optString("endedAt").ifEmpty { null },
plannedMin = o.optInt("plannedMin", 0),
actualMin = o.optInt("actualMin", 0),
completed = o.optBoolean("completed", false),
pauseCount = o.optInt("pauseCount", 0),
rating = if (o.isNull("rating")) null else o.optInt("rating"),
note = o.optString("note").ifEmpty { null },
intervalMin = if (o.isNull("intervalMin")) null else o.optInt("intervalMin"),
synced = true,
)
}
}
} catch (_: Exception) {
null
}
private fun postSession(s: Session, tok: String): Boolean = try { private fun postSession(s: Session, tok: String): Boolean = try {
val conn = URL("$BASE_URL/v1/sessions").openConnection() as HttpURLConnection val conn = URL("$BASE_URL/v1/sessions").openConnection() as HttpURLConnection
conn.requestMethod = "POST" conn.requestMethod = "POST"
@@ -111,6 +176,7 @@ object SessionStore {
put("pauseCount", s.pauseCount) put("pauseCount", s.pauseCount)
s.rating?.let { put("rating", it) } s.rating?.let { put("rating", it) }
s.note?.let { put("note", it) } s.note?.let { put("note", it) }
s.intervalMin?.let { put("intervalMin", it) }
} }
conn.outputStream.use { it.write(body.toString().toByteArray()) } conn.outputStream.use { it.write(body.toString().toByteArray()) }
val ok = conn.responseCode in 200..299 val ok = conn.responseCode in 200..299
@@ -136,6 +202,7 @@ object SessionStore {
pauseCount = o.optInt("pauseCount", 0), pauseCount = o.optInt("pauseCount", 0),
rating = if (o.has("rating")) o.getInt("rating") else null, rating = if (o.has("rating")) o.getInt("rating") else null,
note = o.optString("note").ifEmpty { null }, note = o.optString("note").ifEmpty { null },
intervalMin = if (o.has("intervalMin")) o.getInt("intervalMin") else null,
synced = o.optBoolean("synced", false), synced = o.optBoolean("synced", false),
) )
} }
@@ -157,6 +224,7 @@ object SessionStore {
put("pauseCount", s.pauseCount) put("pauseCount", s.pauseCount)
s.rating?.let { put("rating", it) } s.rating?.let { put("rating", it) }
s.note?.let { put("note", it) } s.note?.let { put("note", it) }
s.intervalMin?.let { put("intervalMin", it) }
put("synced", s.synced) put("synced", s.synced)
}) })
} }

View File

@@ -0,0 +1,18 @@
package com.marcus.meditationtimer
/** Selectable session-gong sounds (key → label). Key persisted in settings. */
val GONG_SOUNDS = listOf(
"bowl" to "Bowl",
"deep" to "Deep",
"bright" to "Bright",
"bell" to "Bell",
"twin" to "Twin",
)
fun gongRes(key: String): Int = when (key) {
"deep" -> R.raw.gong_deep
"bright" -> R.raw.gong_bright
"bell" -> R.raw.gong_bell
"twin" -> R.raw.gong_twin
else -> R.raw.gong
}

View File

@@ -40,14 +40,16 @@ class TimerService : Service() {
const val ACTION_RESUME = "resume" const val ACTION_RESUME = "resume"
const val ACTION_STOP = "stop" const val ACTION_STOP = "stop"
const val EXTRA_DURATION_MS = "duration_ms" const val EXTRA_DURATION_MS = "duration_ms"
const val EXTRA_INTERVAL_MIN = "interval_min"
val state = MutableStateFlow(TimerState()) val state = MutableStateFlow(TimerState())
fun start(context: Context, durationMs: Long) { fun start(context: Context, durationMs: Long, intervalMin: Int = 0) {
context.startForegroundService( context.startForegroundService(
Intent(context, TimerService::class.java) Intent(context, TimerService::class.java)
.setAction(ACTION_START) .setAction(ACTION_START)
.putExtra(EXTRA_DURATION_MS, durationMs) .putExtra(EXTRA_DURATION_MS, durationMs)
.putExtra(EXTRA_INTERVAL_MIN, intervalMin)
) )
} }
@@ -65,6 +67,9 @@ class TimerService : Service() {
private var remainingMs = 0L private var remainingMs = 0L
private var startedAtIso = "" private var startedAtIso = ""
private var pauseCount = 0 private var pauseCount = 0
private var intervalMin = 0
private var nextBellElapsedMs = 0L
private var gongResId = R.raw.gong
override fun onBind(intent: Intent?) = null override fun onBind(intent: Intent?) = null
@@ -83,6 +88,11 @@ class TimerService : Service() {
endAt = SystemClock.elapsedRealtime() + totalMs endAt = SystemClock.elapsedRealtime() + totalMs
startedAtIso = Instant.now().toString() startedAtIso = Instant.now().toString()
pauseCount = 0 pauseCount = 0
intervalMin = intent.getIntExtra(EXTRA_INTERVAL_MIN, 0)
nextBellElapsedMs = if (intervalMin > 0) intervalMin * 60_000L else Long.MAX_VALUE
gongResId = gongRes(
getSharedPreferences("settings", MODE_PRIVATE).getString("gong_sound", "bowl") ?: "bowl"
)
startForeground(NOTIF_ID, buildNotification(remainingMs, paused = false)) startForeground(NOTIF_ID, buildNotification(remainingMs, paused = false))
acquireWakeLock(totalMs) acquireWakeLock(totalMs)
playGong() playGong()
@@ -130,6 +140,13 @@ class TimerService : Service() {
} }
remainingMs = left remainingMs = left
state.value = TimerState(Phase.Running, totalMs, left) state.value = TimerState(Phase.Running, totalMs, left)
// Interval bell on each boundary — skipped near the end so it
// never collides with the closing gong.
val elapsed = totalMs - left
if (elapsed >= nextBellElapsedMs) {
if (left > 30_000L) playBell()
nextBellElapsedMs += intervalMin * 60_000L
}
delay(200L) delay(200L)
} }
} }
@@ -156,14 +173,23 @@ class TimerService : Service() {
actualMin = (elapsedMs / 60_000.0).roundToInt().coerceAtLeast(1), actualMin = (elapsedMs / 60_000.0).roundToInt().coerceAtLeast(1),
completed = completed, completed = completed,
pauseCount = pauseCount, pauseCount = pauseCount,
intervalMin = if (intervalMin > 0) intervalMin else null,
) )
SessionStore.record(session) SessionStore.record(session)
SessionStore.lastSessionId.value = session.id SessionStore.lastSessionId.value = session.id
} }
private fun playBell() {
// Separate one-shot player so a bell never cuts off the session gong.
MediaPlayer.create(this, R.raw.bell)?.apply {
setOnCompletionListener { it.release() }
start()
}
}
private fun playGong(onComplete: (() -> Unit)? = null) { private fun playGong(onComplete: (() -> Unit)? = null) {
player?.release() player?.release()
player = MediaPlayer.create(this, R.raw.gong)?.apply { player = MediaPlayer.create(this, gongResId)?.apply {
setOnCompletionListener { setOnCompletionListener {
it.release() it.release()
if (player == it) player = null if (player == it) player = null

View File

@@ -0,0 +1,29 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path android:pathData="M0,0h108v108H0z">
<aapt:attr name="android:fillColor">
<gradient
android:type="linear"
android:startX="54" android:startY="0"
android:endX="54" android:endY="108"
android:startColor="#1D3550"
android:endColor="#0A1622" />
</aapt:attr>
</path>
<!-- faint aura rings behind the gong -->
<path
android:pathData="M54,54m-44,0a44,44 0,1 1,88 0a44,44 0,1 1,-88 0"
android:strokeColor="#22E3BC4B"
android:strokeWidth="1.5"
android:fillColor="#00000000" />
<path
android:pathData="M54,54m-38,0a38,38 0,1 1,76 0a38,38 0,1 1,-76 0"
android:strokeColor="#14E3BC4B"
android:strokeWidth="1"
android:fillColor="#00000000" />
</vector>

View File

@@ -0,0 +1,51 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<!-- gong rim -->
<path android:pathData="M54,54m-29,0a29,29 0,1 1,58 0a29,29 0,1 1,-58 0">
<aapt:attr name="android:fillColor">
<gradient
android:type="linear"
android:startX="36" android:startY="30"
android:endX="72" android:endY="80"
android:startColor="#D9AE35"
android:endColor="#A87E1C" />
</aapt:attr>
</path>
<!-- face -->
<path android:pathData="M54,54m-24.5,0a24.5,24.5 0,1 1,49 0a24.5,24.5 0,1 1,-49 0">
<aapt:attr name="android:fillColor">
<gradient
android:type="linear"
android:startX="38" android:startY="32"
android:endX="70" android:endY="78"
android:startColor="#F0CF66"
android:endColor="#CD9F2E" />
</aapt:attr>
</path>
<!-- hammered ring -->
<path
android:pathData="M54,54m-17.5,0a17.5,17.5 0,1 1,35 0a17.5,17.5 0,1 1,-35 0"
android:strokeColor="#B8892288"
android:strokeWidth="1.2"
android:fillColor="#00000000" />
<!-- boss (center dome) -->
<path android:pathData="M54,54m-9.5,0a9.5,9.5 0,1 1,19 0a9.5,9.5 0,1 1,-19 0">
<aapt:attr name="android:fillColor">
<gradient
android:type="linear"
android:startX="47" android:startY="46"
android:endX="60" android:endY="62"
android:startColor="#FCEBA8"
android:endColor="#E3BC4B" />
</aapt:attr>
</path>
<!-- specular highlight, upper-left crescent -->
<path
android:pathData="M38.5,40.5 a21,21 0 0 1 13,-7.5 a24,24 0 0 0 -16.5,10.8 z"
android:fillColor="#66FFF7D6" />
</vector>

View File

@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:pathData="M54,54m-27,0a27,27 0,1 1,54 0a27,27 0,1 1,-54 0"
android:strokeColor="#FFFFFF"
android:strokeWidth="5"
android:fillColor="#00000000" />
<path
android:pathData="M54,54m-9,0a9,9 0,1 1,18 0a9,9 0,1 1,-18 0"
android:fillColor="#FFFFFF" />
</vector>

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_monochrome" />
</adaptive-icon>

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<paths>
<cache-path name="reading" path="reading/" />
</paths>