尚未检测到更新。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.