尚未偵測到更新。GitHub 發布版本後會顯示在這裡。
個人 GitHub 帳號下公開儲存庫的擁有者可以登入並驗證所有權,自行管理介紹,無需管理員核准。
登入並產生驗證檔案,在 30 分鐘內將其提交至此儲存庫的預設分支。其他分支或分叉儲存庫中的檔案無效。然後在下方檢查檔案,即可無需管理員核准直接管理專案。請保留檔案以維持管理權限。
如需登入,將前往登入頁面。登入後請回到此處,再次點擊所選的驗證按鈕。
在專案資料夾中執行 /nomorevibe verify,即可驗證所有權並自行管理介紹。
一分鐘完成驗證Tally for Android — the same ledger as hanifedma.com/tally, in Kotlin and Compose.
翻譯準備中
取自儲存庫 README
正在載入完整 README。 以下為已儲存的摘錄。
The phone half of Tally — a money manager for
people whose money is in more than one currency.
Six screens, six different things — the last one carries both the light theme
and the Korean translation rather than spending a picture on each.
Taken by tools/screenshots.sh . The ledger in
them is the one the
web screenshots use, so
the two can be compared row by row — the same ₩260,768 of expenses, the same
₩13,139,252 of net worth, arrived at by two separate implementations.檢視完整 README ↗The phone half of Tally — a money manager for people whose money is in more than one currency.
Web app: hanifedma/tally
The same ledger as the website, not a copy of it. Both apps read and write the same Postgres and both listen to the same realtime stream, so an expense entered here appears in the browser about a second later, and the other way round.
The same feature as the
web app's. On the
phone, Export as CSV → Claude, ChatGPT or Gemini hands the file and the
instructions straight to that app when it is installed — named in the
manifest's <queries>, which Android 11 and later need before an app can see
another — and otherwise saves the file and opens the website, with the
instructions on the clipboard. The 📋 button above the + is where the reply is
pasted.
Use without an account on the sign-in screen gives you the whole app with
nothing to set up and nothing sent anywhere. It is not a demo: it is the same
LedgerRepository with local = true, which closes every path to the network
and leaves the rules — client-made ids, soft deletes, whole-row writes —
exactly as they are. The device cache stops being an optimisation and becomes
the ledger.
Nothing is queued in that mode, on purpose. An outbox that could never drain would be the only thing holding a row, and the file it lives in is not the one the app treats as the ledger.
Signing in later offers, once, to copy that ledger into the account. The rows keep their ids; only the derived ones — the starter categories and accounts, whose ids come from the user id so two devices seeding one account write one set of rows — are translated, along with every reference to them. Nothing is erased at the far end, so a failure there loses nothing.
This is also the only way in on a phone with no Google Play Services, where Credential Manager cannot offer an account at all.
See SETUP.md. It is short, because the real work is in the web repo's SETUP.md — one Supabase project and one Google OAuth client serve both apps.
./gradlew assembleRelease # app/build/outputs/apk/release/app-release.apk
./gradlew installDebug # onto a connected device or emulator
./gradlew test # 82 unit tests, no device needed
./gradlew connectedAndroidTest # the screen tests; needs a device
./tools/screenshots.sh # docs/screenshots/; needs a device
tools/screenshots.sh drives device-only mode, so it needs no Supabase
project and no Google account. It loads the same demo ledger the web
screenshots use — generated by the web repo's tools/demo-cache.mjs — so the
phone and the browser are photographed holding exactly the same money.
The screen tests install the debug build, and Android will not put that over a
copy signed with the release key: the run stops at
INSTALL_FAILED_UPDATE_INCOMPATIBLE before a single test starts. Uninstalling
Tally to make room deletes a ledger kept only on that device, so run them
somewhere the released app is not — an emulator started with -read-only
forgets the whole visit when it closes, which makes it a good place.
Signing is described by a keystore.properties at the repo root, which is not
committed — see keystore.properties.example. Without it the release build
falls back to the debug key: still installable, still testable, just not
something to publish, and Google sign-in will refuse it because the certificate
is not the one registered.
core/ Money, Dates, Calc, Compute, Models, Ids
— the same arithmetic as the web app's money.js
data/ Supabase, LedgerRepository, LocalStore, Prefs
auth/ AuthManager — Credential Manager → Supabase
i18n/ Strings.kt — GENERATED, do not edit
ui/ theme, TallyViewModel, TallyApp, screens/, components/
Strings.kt is generatedIt is produced from the web app's i18n.js:
cd ../tally && node tools/gen-android-strings.mjs ../tally-android
Two hand-maintained copies of nearly three hundred strings drift within a
week — a key renamed on one side and the phone quietly starts showing
tx.saveAnother where a button should be. So the web table is the source and
this is a build artefact that happens to be committed.
The handful of strings only the phone says — one names a file that exists only
in this repo, one names a failure only Credential Manager can have — live at
the top of the generator, not pencilled into Strings.kt afterwards, where
the next run would silently delete them.
Nothing checks this file against i18n.js automatically: that table is
JavaScript and these tests run on the JVM. ParityTest checks what it can from
this side — that every key exists in both languages, and that no
{placeholder} was lost in translation. Falling behind i18n.js is caught by
running the generator, not by a test.
app/src/test/.../ParityTest.kt is not a test that Kotlin does what Kotlin
does. Every expected value in it was produced by running the web app's
money.js in Node and pasting the answer in. It asserts that the phone and the
browser put the same number on the screen for the same row — the same formatted
string, the same converted amount, the same derived UUID. If one side drifts,
it fails.
Ktor and kotlinx.serialization — the two libraries the whole sync layer rests on — resolve types reflectively at the edges, and a wrong keep rule fails at runtime on a user's phone rather than at build time here. A few extra megabytes is the right trade for a sync layer that cannot silently work in debug and break in release.
Google sign-in needs Google Play Services. On a phone that has none — some Huawei models, some custom ROMs — there is no way in, and Tally will say so rather than showing a button that cannot work. The web app works on any of them.