301 Commits

Author SHA1 Message Date
Mo Tarbin
b3b3a0e5a8 release 1.2.49
Some checks failed
Build validation / build (push) Has been cancelled
2026-08-18 08:50:46 -04:00
Mohamad Tarbin
a92eeaf149 Merge pull request #237 from donetick/missing-translation
Missing translation Part 1
2026-08-18 08:44:45 -04:00
Mo Tarbin
86989362fe fix: standardize section separators in README.md 2026-08-18 08:41:48 -04:00
Mo Tarbin
cb81749dbf fix: update ESLint ignores and improve i18n audit script readability 2026-08-18 08:23:19 -04:00
Mo Tarbin
0bac419c3a feat: enhance localization and user experience across various components
- Added new localization keys for project management, settings, and onboarding views.
- Updated labels and placeholders in project modal, password change modal, and backup/restore modal for better clarity.
- Improved user feedback messages for password mismatch and backup creation failures.
- Enhanced the timer details view with localized strings for active work, sessions, and distribution.
- Refined the due date picker modal with localized quick date and time options.
- Updated photo task modal and scan panel to include localized text for better accessibility.
- Ensured consistent use of translation function for all user-facing strings.
2026-08-18 01:31:16 -04:00
Mo Tarbin
1fa4748168 feat: integrate i18n for translation in various components
- Updated NativeCancelSubscriptionModal to use translation keys for titles and messages.
- Enhanced CircleSetupView with translation for notifications and UI text.
- Refactored ThingsView to implement translation for notifications, titles, and descriptions.
- Added translation support in AdvancedOptionsSection for labels and descriptions.
- Implemented i18n in RepeatPickerField for various UI elements and labels.
2026-08-18 01:27:57 -04:00
Mo Tarbin
c1318a2cc4 feat: add i18n support to Settings and AddTaskModal components
- Integrated translation functionality using react-i18next in Settings.jsx
- Replaced hardcoded strings with translation keys for better localization support
- Updated AddTaskModal.jsx to include translations for task-related strings
- Enhanced user experience by providing localized messages for various actions and notifications
2026-08-18 01:11:26 -04:00
Mo Tarbin
cc2e656e14 update localization for notifications, colors, and icons across multiple components 2026-08-18 00:06:34 -04:00
Mo Tarbin
77f87daeba Release 1.2.48
Some checks failed
Build validation / build (push) Has been cancelled
2026-08-17 01:34:15 -04:00
Mohamad Tarbin
64479f0858 Merge pull request #236 from donetick/0815-fixes
0815 fixes
2026-08-17 01:03:36 -04:00
Mohamad Tarbin
4c235c7d00 Merge branch 'develop' into 0815-fixes 2026-08-17 01:03:29 -04:00
Mo Tarbin
c0d2d5a9ec fix: update terminology from "Report a Bug" to "Report an Issue" across multiple components 2026-08-16 18:03:34 -04:00
Mo Tarbin
dae647a4be feat: enhance global search with navigation mode and updated action hints 2026-08-16 17:56:50 -04:00
Mo Tarbin
f27bb79a07 fix: ensure untranslated strings are skipped in Crowdin configuration 2026-08-16 17:08:44 -04:00
Mohamad Tarbin
631d273b02 Merge pull request #174 from scottanderson/eslint3
Pre-commit hooks and CI enforcement for eslint and prettier
2026-08-16 16:32:25 -04:00
Scott Anderson
d4b21f8f4e Enable CI enforcement and pre-commit hooks 2026-08-16 15:51:14 -04:00
Mohamad Tarbin
a407606f1c Merge pull request #235 from donetick/eslint3
ran npx prettier -w .
2026-08-16 15:38:12 -04:00
Mohamad Tarbin
4f21242e39 ran npx prettier -w . 2026-08-16 15:35:44 -04:00
Mohamad Tarbin
2a4f085e18 Merge pull request #234 from donetick/eslint3
ran npm run lint:fix
2026-08-16 15:33:13 -04:00
Mo Tarbin
253514a6e9 remove unused view mode logic and simplify card rendering in ArchivedTasks and CompactChoreCard 2026-08-16 14:07:54 -04:00
Mo Tarbin
e18c829ae7 feat: integrate PostHog for sourcemap uploads and update dependencies 2026-08-16 13:52:42 -04:00
Mo Tarbin
9329eaf3a9 update consent handling and remove unused error capture logic 2026-08-16 13:13:00 -04:00
Mohamad Tarbin
292597e7c6 ran npm run lint:fix 2026-08-16 11:52:40 -04:00
Mohamad Tarbin
e377fb5b06 Merge pull request #233 from donetick/0814-fixes
0814 fixes
2026-08-16 11:32:06 -04:00
Mohamad Tarbin
84f86032c9 Merge pull request #231 from everysingletear/fix/activities-card-scope
fix: ActivitiesCard crashes — t() in a prop default has no scope
2026-08-16 11:27:12 -04:00
Mo Tarbin
834db6b39d Merge remote-tracking branch 'origin/develop' into 0814-fixes
# Conflicts:
#	public/locales/en/labels.json
#	src/views/Chores/MyChores.jsx
#	src/views/Chores/components/ChoreToolbarPrototype.jsx
#	src/views/Chores/components/MultiSelectToolbar.jsx
#	src/views/Chores/hooks/useChoreActions.js
2026-08-16 02:59:24 -04:00
Mo Tarbin
97453803d6 feat: add new view options and update keyboard shortcuts for improved navigation 2026-08-16 02:53:53 -04:00
Mo Tarbin
1c99bd1886 feat: enhance chore management with new modals and filter options 2026-08-16 02:46:04 -04:00
Mo Tarbin
5359ae193b add SortAndFilterMenu component and integrate it into various views for enhanced sorting and filtering capabilities 2026-08-16 02:16:33 -04:00
Mo Tarbin
325d5e6df7 fix: update assignee filter logic and improve display options in MyChores and ChoreToolbarPrototype 2026-08-16 02:07:44 -04:00
Mo Tarbin
79d92ca8c9 add quick actions for creating labels, projects, and filters in GlobalSearchPalette
refactor: move stripHtml function to Helpers utility

fix: update chore filters to include raw description for better search indexing
enhance: implement search parameter handling for modal openings in ProjectView and LabelView
2026-08-16 01:36:20 -04:00
Mo Tarbin
2480049fd1 Improve user feedback and modal behavior in ErrorReportModal and PolicyUpdateModal 2026-08-16 01:26:28 -04:00
Mo Tarbin
5c71e4ce58 fix: handle manual bug reports by modifying error structure in submitErrorReport 2026-08-16 01:07:55 -04:00
Mo Tarbin
77834aa3c0 fix: add z-index to AppModal handle as ReportIssue modal X was not pressable 2026-08-16 00:32:14 -04:00
Mo Tarbin
76fb3500ec fix: update PolicyUpdateModal to open documents in system browser 2026-08-16 00:31:47 -04:00
Mo Tarbin
2d5dd81371 Fix : https://github.com/donetick/donetick/issues/794 2026-08-15 21:36:56 -04:00
Mo Tarbin
c6f7ce48d8 feat: enhance chore actions with bulk operations and new label detail view
- Refactored bulk operations in useChoreActions to streamline completion, archiving, deletion, and other actions.
- Introduced new hooks for managing local chore state during bulk operations.
- Added handleBulkDueDate, handleBulkAssignee, handleBulkPriority, and handleBulkLabels functions for better task management.
- Implemented a new LabelDetailView component to display and manage tasks associated with a specific label.
- Updated LabelView to navigate to LabelDetailView on label click.
- Improved multi-select functionality to support range selection and summary of selected chores.
- Minor UI adjustments and text updates in AdvancedOptionsSection for clarity.
2026-08-15 12:33:39 -04:00
everysingletear
64aca507af fix: ChoresOverview does not parse — hook inserted into an object literal
`CHORE_STATUS` is a module-level object literal, and a `useTranslation`
call ended up inside it:

    const CHORE_STATUS = {
      const { t } = useTranslation('chores')
      NO_DUE_DATE: 'No due date',

That is a syntax error, so the file cannot be parsed at all. It does not
break the build because nothing imports `ChoresOverview` — vite never
compiles it — but it breaks eslint, editors, and anything else that walks
the whole tree.

The hook moves into the component, which is where the file's `t()` calls
actually live.

This one arrived with #210, the same way #216 brought the ActivitiesCard
crash: my tooling wrote the hook in mechanically and nothing downstream
parsed the result. Both are now covered by the `no-undef` pass I described
in the other commit — it reports the parse error too.
2026-08-15 17:02:02 +08:00
everysingletear
d813971809 fix: ActivitiesCard crashes — t() in a prop default has no scope
`ActivitiesCard` takes its title as `({ title = t('activity.title') })`.
A default parameter is evaluated in the function's own scope, where `t`
does not exist: the only `t` in the file is bound inside `ActivityItem`,
a separate component. `Sidepanel` renders the card without a `title`, and
`activities` is `enabled: true` in `DEFAULT_SIDEPANEL_CONFIG`, so the
default is always evaluated on a desktop-width screen.

The hook moves into the component body and the title falls back there
instead. Same rendered output; three render sites now read `displayTitle`.

This is my regression: it arrived with #216, which described itself as
"no behaviour change". It was not caught because a bundler cannot flag it
— an unbound `t` is indistinguishable from a global — and my checks only
verified that keys and imports existed, not that `t` was in scope. It is
visible in the built bundle: `({title:e=t("activity.title")})` keeps the
literal `t` because the minifier cannot rename a free variable, while a
working call nearby minifies to `q=e("common.confirm")`.

I have added an eslint `no-undef` pass to my own pipeline and run it
before sending anything from now on.
2026-08-15 17:01:18 +08:00
Mo Tarbin
7729917611 enhance label search functionality with Fuse.js integration and improved UI 2026-08-15 00:45:51 -04:00
Mohamad Tarbin
96ba672b09 Merge pull request #210 from everysingletear/i18n/chores
i18n: extract the chore list and its modals (Part of #145)
2026-08-15 00:03:09 -04:00
everysingletear
7d36090e46 i18n: extract the chore list and its modals (chores namespace)
Part of #145.

Twenty-three files across the task list zone: the list and card views,
sorting and grouping, multi-select and its toolbar and help sheet,
archived tasks, the assignee card, the chore action menu, the
nudge/NFC/photo modals, the rich text editor, the scan panel, the
notification templates and the keyboard-shortcut toasts.

Extends the existing `chores` namespace, so `src/i18n/config.js` is
untouched.

English only — no translations, no behaviour change. Every t() value is
checked against this branch's base: the string must appear
character-for-character in the code it replaces (247 call sites).

Two values are matched loosely and worth naming: archived.closeMultiSelect
in both the archived view and the toolbar. The base builds that tooltip as
`${size === 0 ? 'Close' : 'Clear'} multi-select (Esc)`, so only one branch
of the ternary exists contiguously in the source. Both keys hold exactly
what each branch renders; the tooltip is kept whole so a translator can
reorder it.

Rebased on current `develop` again after #215 and #216 landed — the
dictionary conflict was theirs, not the code's. No code file in this PR
was touched upstream in the meantime.
2026-08-15 09:03:28 +08:00
Mohamad Tarbin
f43369c960 Merge pull request #229 from donetick/revert-175-enable-dependabot
Revert "Enable dependabot"
2026-08-14 18:56:30 -04:00
Mohamad Tarbin
64fa636413 Revert "Enable dependabot" 2026-08-14 18:56:17 -04:00
Mohamad Tarbin
c1f6431174 Merge pull request #175 from scottanderson/enable-dependabot
Enable dependabot
2026-08-14 18:48:46 -04:00
Mohamad Tarbin
537ab0568b Merge pull request #216 from everysingletear/i18n/regen
i18n: extract the remaining task, history, filter and timer screens (Part of #145)
2026-08-14 18:41:00 -04:00
Mohamad Tarbin
b095f26d61 Merge branch 'develop' into i18n/regen 2026-08-14 18:40:51 -04:00
Mohamad Tarbin
2d776e973a Merge pull request #215 from everysingletear/i18n/leftovers
i18n: extract the strings left behind in already-localized screens (Part of #145)
2026-08-14 18:40:07 -04:00
Mo Tarbin
ddb726ad1f Bump version to 1.2.47
Some checks failed
Build validation / build (push) Has been cancelled
2026-08-14 08:59:25 -04:00
Mohamad Tarbin
67593ec284 Merge pull request #221 from donetick/0813-fixes
0813 fixes
2026-08-14 08:58:34 -04:00
Mo Tarbin
6bc5982294 fix: update key prop in SelectModal options to use item.id for consistency 2026-08-14 08:57:53 -04:00
Mo Tarbin
1dd01e0eca Fix: Fixing Select start render below the modal make it rendioner in modal but keep the modal small so completely overlap with it. fixing that by applying custom global class 2026-08-14 08:55:25 -04:00
Mo Tarbin
7ec8bfe7e6 Release 1.2.46
Some checks failed
Build validation / build (push) Has been cancelled
2026-08-14 01:57:07 -04:00
Mohamad Tarbin
bc62f22e6d Merge pull request #220 from donetick/0812-fixes
fix: update chore ID reference in ChoresOverview and clean up unused …
2026-08-14 01:46:35 -04:00
Mo Tarbin
9e9659c9fe fix: update chore ID reference in ChoresOverview and clean up unused code in HistoryDetailModal and AdvancedOptionsSection 2026-08-14 01:43:01 -04:00
Mohamad Tarbin
ef7810f2d2 Merge pull request #217 from donetick/l10n_develop
New Crowdin updates
2026-08-14 01:36:31 -04:00
Mohamad Tarbin
c8c9f46623 Merge pull request #219 from donetick/0812-fixes
fix: update STATUS_CONFIG to use labelKey for localization
2026-08-14 01:36:12 -04:00
Mo Tarbin
95b31fb14d fix: update STATUS_CONFIG to use labelKey for localization 2026-08-14 01:35:26 -04:00
Mohamad Tarbin
cbba7f6b79 Merge pull request #218 from donetick/analytics
Integrate PostHog and Plausible for enhanced analytics and privacy management
2026-08-14 01:29:02 -04:00
Mo Tarbin
fc12f93316 Merge remote-tracking branch 'origin/develop' into analytics
# Conflicts:
#	public/locales/en/settings.json
#	src/views/Settings/DeveloperSettings.jsx
#	src/views/Settings/SettingsOverview.jsx
2026-08-14 01:26:51 -04:00
Mo Tarbin
27560a078c feat: integrate Plausible analytics script and adjust routing for marketing site 2026-08-14 01:13:46 -04:00
Mohamad Tarbin
cf460b3ba7 New translations timer.json (Portuguese, Brazilian)
[ci skip]
2026-08-14 01:05:45 -04:00
Mohamad Tarbin
bd54ee6ab8 New translations things.json (Portuguese, Brazilian)
[ci skip]
2026-08-14 01:05:44 -04:00
Mohamad Tarbin
4e0c7eb03c New translations projects.json (Portuguese, Brazilian)
[ci skip]
2026-08-14 01:05:43 -04:00
Mohamad Tarbin
9e6f21fc18 New translations points.json (Portuguese, Brazilian)
[ci skip]
2026-08-14 01:05:42 -04:00
Mohamad Tarbin
a71ccbe16b New translations labels.json (Portuguese, Brazilian)
[ci skip]
2026-08-14 01:05:40 -04:00
Mohamad Tarbin
dce6ba46f2 New translations history.json (Portuguese, Brazilian)
[ci skip]
2026-08-14 01:05:39 -04:00
Mohamad Tarbin
3d140091d4 New translations filters.json (Portuguese, Brazilian)
[ci skip]
2026-08-14 01:05:38 -04:00
Mohamad Tarbin
3e9c9b465d New translations auth.json (Portuguese, Brazilian)
[ci skip]
2026-08-14 01:05:37 -04:00
Mohamad Tarbin
578a5dbc84 New translations timer.json (Chinese Simplified)
[ci skip]
2026-08-14 01:05:36 -04:00
Mohamad Tarbin
87a623657f New translations things.json (Chinese Simplified)
[ci skip]
2026-08-14 01:05:35 -04:00
Mohamad Tarbin
a1c614d1ff New translations projects.json (Chinese Simplified)
[ci skip]
2026-08-14 01:05:34 -04:00
Mohamad Tarbin
bb5acd8204 New translations points.json (Chinese Simplified)
[ci skip]
2026-08-14 01:05:33 -04:00
Mohamad Tarbin
b2a3055840 New translations labels.json (Chinese Simplified)
[ci skip]
2026-08-14 01:05:32 -04:00
Mohamad Tarbin
1a06928f8b New translations history.json (Chinese Simplified)
[ci skip]
2026-08-14 01:05:31 -04:00
Mohamad Tarbin
222b24069c New translations filters.json (Chinese Simplified)
[ci skip]
2026-08-14 01:05:30 -04:00
Mohamad Tarbin
92795317fe New translations auth.json (Chinese Simplified)
[ci skip]
2026-08-14 01:05:28 -04:00
Mohamad Tarbin
a77c63b73c New translations timer.json (Dutch)
[ci skip]
2026-08-14 01:05:27 -04:00
Mohamad Tarbin
415b2af31a New translations things.json (Dutch)
[ci skip]
2026-08-14 01:05:26 -04:00
Mohamad Tarbin
605eb7f4e6 New translations projects.json (Dutch)
[ci skip]
2026-08-14 01:05:25 -04:00
Mohamad Tarbin
1163085c61 New translations points.json (Dutch)
[ci skip]
2026-08-14 01:05:24 -04:00
Mohamad Tarbin
0ed140a9aa New translations labels.json (Dutch)
[ci skip]
2026-08-14 01:05:23 -04:00
Mohamad Tarbin
57e75d689a New translations history.json (Dutch)
[ci skip]
2026-08-14 01:05:22 -04:00
Mohamad Tarbin
f3808dc0e1 New translations filters.json (Dutch)
[ci skip]
2026-08-14 01:05:20 -04:00
Mohamad Tarbin
dd04d6536c New translations auth.json (Dutch)
[ci skip]
2026-08-14 01:05:19 -04:00
Mohamad Tarbin
96ccf63367 New translations timer.json (Japanese)
[ci skip]
2026-08-14 01:05:18 -04:00
Mohamad Tarbin
735b288c40 New translations things.json (Japanese)
[ci skip]
2026-08-14 01:05:17 -04:00
Mohamad Tarbin
3eedf2bc6b New translations projects.json (Japanese)
[ci skip]
2026-08-14 01:05:16 -04:00
Mohamad Tarbin
061ca90ef1 New translations points.json (Japanese)
[ci skip]
2026-08-14 01:05:15 -04:00
Mohamad Tarbin
a4f141c7b9 New translations labels.json (Japanese)
[ci skip]
2026-08-14 01:05:14 -04:00
Mohamad Tarbin
f914a4d9bd New translations history.json (Japanese)
[ci skip]
2026-08-14 01:05:13 -04:00
Mohamad Tarbin
1fabd374ab New translations filters.json (Japanese)
[ci skip]
2026-08-14 01:05:12 -04:00
Mohamad Tarbin
3bb1cc7114 New translations auth.json (Japanese)
[ci skip]
2026-08-14 01:05:10 -04:00
Mohamad Tarbin
6b066846c8 New translations timer.json (German)
[ci skip]
2026-08-14 01:05:09 -04:00
Mohamad Tarbin
feae64883b New translations things.json (German)
[ci skip]
2026-08-14 01:05:08 -04:00
Mohamad Tarbin
78ddbaf4ed New translations projects.json (German)
[ci skip]
2026-08-14 01:05:07 -04:00
Mohamad Tarbin
dedc5f4cdc New translations points.json (German)
[ci skip]
2026-08-14 01:05:06 -04:00
Mohamad Tarbin
a1f609c551 New translations labels.json (German)
[ci skip]
2026-08-14 01:05:05 -04:00
Mohamad Tarbin
689e390190 New translations history.json (German)
[ci skip]
2026-08-14 01:05:04 -04:00
Mohamad Tarbin
249434de44 New translations filters.json (German)
[ci skip]
2026-08-14 01:05:03 -04:00
Mohamad Tarbin
d09c32216f New translations auth.json (German)
[ci skip]
2026-08-14 01:05:02 -04:00
Mohamad Tarbin
8b6d82c657 New translations timer.json (Arabic)
[ci skip]
2026-08-14 01:05:01 -04:00
Mohamad Tarbin
dc9ec6f9bd New translations things.json (Arabic)
[ci skip]
2026-08-14 01:04:59 -04:00
Mohamad Tarbin
68d9d3d642 New translations projects.json (Arabic)
[ci skip]
2026-08-14 01:04:58 -04:00
Mohamad Tarbin
74efb48236 New translations points.json (Arabic)
[ci skip]
2026-08-14 01:04:57 -04:00
Mohamad Tarbin
206eb69dec New translations labels.json (Arabic)
[ci skip]
2026-08-14 01:04:56 -04:00
Mohamad Tarbin
43387dc56d New translations history.json (Arabic)
[ci skip]
2026-08-14 01:04:55 -04:00
Mohamad Tarbin
7c434602f5 New translations filters.json (Arabic)
[ci skip]
2026-08-14 01:04:54 -04:00
Mohamad Tarbin
ba876ac8bf New translations chores.json (Portuguese, Brazilian)
[ci skip]
2026-08-14 01:04:53 -04:00
Mohamad Tarbin
afe3964198 New translations chores.json (Chinese Simplified)
[ci skip]
2026-08-14 01:04:52 -04:00
Mohamad Tarbin
7c5b5d534e New translations chores.json (Dutch)
[ci skip]
2026-08-14 01:04:50 -04:00
Mohamad Tarbin
4653074811 New translations chores.json (Japanese)
[ci skip]
2026-08-14 01:04:49 -04:00
Mohamad Tarbin
bcd19c5f67 New translations chores.json (German)
[ci skip]
2026-08-14 01:04:48 -04:00
Mohamad Tarbin
cb5e23086a New translations settings.json (Portuguese, Brazilian)
[ci skip]
2026-08-14 01:04:47 -04:00
Mohamad Tarbin
87f7f98a64 New translations common.json (Portuguese, Brazilian)
[ci skip]
2026-08-14 01:04:46 -04:00
Mohamad Tarbin
436f2913a5 New translations settings.json (Chinese Simplified)
[ci skip]
2026-08-14 01:04:44 -04:00
Mohamad Tarbin
0f1b4a3296 New translations common.json (Chinese Simplified)
[ci skip]
2026-08-14 01:04:43 -04:00
Mohamad Tarbin
2c82f3f4c0 New translations settings.json (Dutch)
[ci skip]
2026-08-14 01:04:42 -04:00
Mohamad Tarbin
98b2692d07 New translations common.json (Dutch)
[ci skip]
2026-08-14 01:04:41 -04:00
Mohamad Tarbin
e1725e92d4 New translations settings.json (Japanese)
[ci skip]
2026-08-14 01:04:39 -04:00
Mohamad Tarbin
2d8cb79053 New translations common.json (Japanese)
[ci skip]
2026-08-14 01:04:38 -04:00
Mohamad Tarbin
b42c5101b4 New translations settings.json (German)
[ci skip]
2026-08-14 01:04:37 -04:00
Mohamad Tarbin
c73423a5d1 New translations common.json (German)
[ci skip]
2026-08-14 01:04:35 -04:00
Mohamad Tarbin
1eb36ed8e9 New translations auth.json (Arabic)
[ci skip]
2026-08-14 01:04:34 -04:00
Mohamad Tarbin
de1034278e New translations timer.json (Spanish)
[ci skip]
2026-08-14 01:04:33 -04:00
Mohamad Tarbin
a88418fa36 New translations things.json (Spanish)
[ci skip]
2026-08-14 01:04:32 -04:00
Mohamad Tarbin
83ecc89bc5 New translations projects.json (Spanish)
[ci skip]
2026-08-14 01:04:31 -04:00
Mohamad Tarbin
1c89549b01 New translations points.json (Spanish)
[ci skip]
2026-08-14 01:04:30 -04:00
Mohamad Tarbin
62e401e215 New translations labels.json (Spanish)
[ci skip]
2026-08-14 01:04:29 -04:00
Mohamad Tarbin
b75527fe6c New translations history.json (Spanish)
[ci skip]
2026-08-14 01:04:28 -04:00
Mohamad Tarbin
8d6874072e New translations filters.json (Spanish)
[ci skip]
2026-08-14 01:04:26 -04:00
Mohamad Tarbin
0b6f9b581b New translations auth.json (Spanish)
[ci skip]
2026-08-14 01:04:25 -04:00
Mohamad Tarbin
9848adf8c8 New translations timer.json (French)
[ci skip]
2026-08-14 01:04:24 -04:00
Mohamad Tarbin
44647c4e38 New translations things.json (French)
[ci skip]
2026-08-14 01:04:23 -04:00
Mohamad Tarbin
36d33bba48 New translations projects.json (French)
[ci skip]
2026-08-14 01:04:22 -04:00
Mohamad Tarbin
9d9b4a37fb New translations points.json (French)
[ci skip]
2026-08-14 01:04:21 -04:00
Mohamad Tarbin
e0949a9c33 New translations labels.json (French)
[ci skip]
2026-08-14 01:04:20 -04:00
Mohamad Tarbin
a2fd65234b New translations history.json (French)
[ci skip]
2026-08-14 01:04:19 -04:00
Mohamad Tarbin
98693961d4 New translations filters.json (French)
[ci skip]
2026-08-14 01:04:18 -04:00
Mohamad Tarbin
b4512a6cbc New translations auth.json (French)
[ci skip]
2026-08-14 01:04:17 -04:00
Mohamad Tarbin
b98c097b34 New translations chores.json (Arabic)
[ci skip]
2026-08-14 01:04:15 -04:00
Mohamad Tarbin
fa4c11b2ee New translations chores.json (Spanish)
[ci skip]
2026-08-14 01:04:14 -04:00
Mohamad Tarbin
4f0f9e25c5 New translations settings.json (French)
[ci skip]
2026-08-14 01:04:13 -04:00
Mohamad Tarbin
328dcac0e4 New translations chores.json (French)
[ci skip]
2026-08-14 01:04:12 -04:00
Mohamad Tarbin
56a5b49fca New translations settings.json (Arabic)
[ci skip]
2026-08-14 01:04:11 -04:00
Mohamad Tarbin
0669ee9499 New translations common.json (Arabic)
[ci skip]
2026-08-14 01:04:09 -04:00
Mohamad Tarbin
64cd0baa5e New translations settings.json (Spanish)
[ci skip]
2026-08-14 01:04:08 -04:00
Mohamad Tarbin
9860e9d25a New translations common.json (Spanish)
[ci skip]
2026-08-14 01:04:06 -04:00
Mohamad Tarbin
f9c2539fbb New translations common.json (French)
[ci skip]
2026-08-14 01:04:05 -04:00
everysingletear
ede1a11099 i18n: extract the remaining task, history, filter and timer screens
Part of #145.

Sixteen files that were left out of my earlier PRs because my branch also
carried unrelated changes in them. Those are stripped here: each file is
your current `develop` version with the string extraction applied on top,
nothing else.

Covered: the chore action hook and its toasts, activities and smart-insight
cards, the chore toolbar, chore history and its card, saved filters, the
timer details view, project and label modals, the notification picker,
the pending badge, the sync status indicator, the SSE settings and hook,
and the profile avatar menu.

All namespaces already exist, so `src/i18n/config.js` is untouched.
Keys added: 83 `chores`, 31 `common`, 10 `timer`, 9 `history`, 6 `labels`,
5 `projects`, 2 `filters`, 1 `settings`.

English only — no translations, no behaviour change. Every t() value is
checked to appear character-for-character in the code it replaces, or to
match the value already in your dictionary for the same key: 221 call
sites, no mismatches.

Five files from the same batch are deliberately left out. They build
translated labels in module-level constant tables, where the hook cannot
be called — `FilterBar`, `RepeatSection`, `RepeatPickerField`,
`FilterBuilderContent` and `AdvancedOptionsSection`. Those need the key
to travel as data and be resolved inside the component, which is a design
change rather than an extraction, so it deserves its own PR.
2026-08-14 11:26:24 +08:00
everysingletear
6e97c70570 i18n: extract the strings left behind in already-localized screens
Part of #145.

Two files you localized yourself, each with a handful of strings that the
pass didn't reach:

- `ChoreView` — the offline-sync notices ("You're offline — completion
  will sync when back online" and its skip/start/pause/restore siblings)
  and the error toasts they fall back to. 12 keys added to `en/chores.json`.
- `MFASettings` — the "Generate New Codes" button. 1 key added to
  `en/settings.json`.

Both extend namespaces that already exist, so `src/i18n/config.js` is
untouched and this cannot conflict with my other open PRs.

English only — no translations, no behaviour change. Every t() value is
checked to appear character-for-character in the code it replaces, or to
match the value already in your dictionary for the same key (126 call
sites total across both files).
2026-08-14 11:10:35 +08:00
Mo Tarbin
e2b41c85f6 feat: add policy update notice and handling
- Introduced a new policy update modal to inform users of changes to the Privacy Policy and Terms of Service.
- Implemented a service to manage the state of policy updates, including versioning and acknowledgment tracking.
- Added a prompt component that surfaces the policy update modal based on user profile and acknowledgment status.
- Updated Privacy Policy and Terms of Service documents with new effective dates and content.
- Enhanced developer settings to allow manual triggering of the policy update notice for testing purposes.
2026-08-13 22:31:21 -04:00
Mohamad Tarbin
677376983b Merge pull request #207 from donetick/l10n_develop
New Crowdin updates
2026-08-13 19:44:42 -04:00
Mohamad Tarbin
2918eda784 Merge pull request #211 from everysingletear/i18n/settings-remainder
i18n: extract the settings screens not covered by 8c9bf0d (Part of #145)
2026-08-13 19:40:49 -04:00
Mohamad Tarbin
ba8d8fbdb1 Merge pull request #212 from everysingletear/i18n/new-namespaces
i18n: add things, history, projects, labels, filters, timer and points namespaces (Part of #145)
2026-08-13 19:39:39 -04:00
Mo Tarbin
f193c46d73 missing comma 2026-08-13 19:38:07 -04:00
Mohamad Tarbin
da1c8f1068 Merge branch 'develop' into i18n/new-namespaces 2026-08-13 19:35:17 -04:00
Mohamad Tarbin
8b33ba7c5d Merge pull request #209 from everysingletear/i18n/common
i18n: extract shared strings into the existing `common` namespace (Part of #145)
2026-08-13 19:32:47 -04:00
Mohamad Tarbin
323ffe72a0 Merge pull request #208 from everysingletear/i18n/auth-v2
i18n: extract Authorization screens into a new `auth` namespace (Part of #145)
2026-08-13 19:31:56 -04:00
Mohamad Tarbin
5d2d696ddd Merge pull request #214 from donetick/0811-fixes
0811 fixes
2026-08-13 19:30:07 -04:00
Mohamad Tarbin
081942a6cd Merge branch 'develop' into 0811-fixes 2026-08-13 19:28:48 -04:00
Mo Tarbin
462af85a38 Update MUI Joy package version and refactor z-index usage across components 2026-08-13 19:24:23 -04:00
Mohamad Tarbin
2c31919533 New translations common.json (French)
[ci skip]
2026-08-13 12:57:37 -04:00
everysingletear
72aa57f018 i18n: extract things, history, projects, labels, filters, timer and points
Part of #145.

Fourteen files across seven feature areas that had no namespace yet: the
things create/edit modals and their history, the chore history detail and
edit modals, the activity feed, the points view and its redemption modal,
the project view with its selector and icon picker, the label view, the
advanced filter builder and the timer edit modal.

Seven new namespaces registered in `src/i18n/config.js` in one change
rather than one per PR, so the `ns:` array is touched once and my other
extraction PRs cannot conflict with this one. Namespaces stay
feature-scoped as described in #145; if you'd rather fold any of these
into `common` or `chores`, say which and I'll rework it.

Dictionaries: `history` 57 keys, `points` 50, `timer` 25, `projects` 16,
`things` 14, `filters` 13, `labels` 5.

English only — no translations, no behaviour change. Every t() value is
checked against this branch's base: the string must appear
character-for-character in the code it replaces (226 call sites).

Three values in `UserPoints` are matched loosely and worth naming. The
base builds the leaderboard heading and subtitle around a ternary —
`{mode === 'points' ? 'Points' : 'Tasks'} Leaderboard` and `Rankings based
on {…} during the selected time period` — so neither full sentence exists
contiguously in the source. Each key holds exactly what one branch
renders. The sentences are kept whole rather than split around the
ternary, since a sentence assembled from fragments cannot be reordered by
a translator.
2026-08-13 10:21:57 +08:00
everysingletear
0e98a556da i18n: extract the settings screens your pass didn't reach (settings)
Part of #145.

`8c9bf0d` localized the settings views themselves; these eight files sit
in the same zone but were not in that pass: the subscription modal, the
child-user creation and password-change modals, both payment result
views, developer settings, and the backup/restore and account-deletion
modals.

Extends the `settings` namespace you established, following its key
naming — 59 keys added to `en/settings.json`. `src/i18n/config.js` is
untouched.

English only — no translations, no behaviour change. Every t() value is
checked against this branch's base: the string must appear
character-for-character in the code it replaces (71 call sites).

One deliberate structural detail: in `BackupRestoreModal` the warning
reads `<strong>Warning:</strong> Restoring a backup will…`. The label and
the sentence are separate keys and the `<strong>` stays in the JSX, so the
emphasis survives and no markup ends up in the dictionary.

If you'd rather keep developer settings hardcoded, say so and I'll drop
that file — it is the one screen here an end user never sees.
2026-08-13 10:18:10 +08:00
everysingletear
bdac10ac57 i18n: extract shared strings into the existing common namespace
Part of #145.

Fourteen files whose user-facing strings are generic enough to belong in
`common` — loading and empty states, the confirmation modal, the shared
input modals (text/date/user/attachment viewer), the mobile nav bar, the
autocomplete input, the error screen and the file-upload error paths.

Extends the namespace that already exists, so `src/i18n/config.js` is
untouched and this cannot collide with any other extraction PR over the
`ns:` array. 28 keys added to `public/locales/en/common.json`.

English only — no translations, no behaviour change. Every t() value is
checked against this branch's base: the string must appear
character-for-character in the code it replaces (36 call sites).

One value is matched loosely and worth naming: `errorScreen.hideDetails`.
The base renders `{showDetails ? 'Hide' : 'Show'} error details`, so
neither full phrase exists contiguously in the source — only one branch of
the ternary can. Both keys hold exactly what each branch renders. The
sentence is kept whole rather than split around the ternary, because a
split sentence cannot be reordered by a translator.
2026-08-13 10:16:14 +08:00
everysingletear
6dc7c3d370 i18n: extract Authorization screens into a new auth namespace
Every user-facing string under src/views/Authorization/ moves to i18next.
English only — no translations in this PR, so it is language-agnostic and
reviewable as a pure refactor.

Covered:
- LoginView — primary/sub-account tabs, credential form, social and
  Authentik buttons, welcome-back state, every auth error toast
- Signup — account creation form and each field-validation message
- ForgotPasswordView / UpdatePasswordView — reset flow and its toasts
- Authenticating — the OAuth landing screen, including its MFA branches
- MFAVerificationModal — code entry, backup codes, error states
- AuthFields / LoginSettings — shared field labels and server settings

`auth` is registered in src/i18n/config.js; public/locales/en/auth.json
holds the 103 keys. The Crowdin config picks up /public/locales/en/*.json
by glob, so the namespace flows into the pipeline with no change to
crowdin.yml.

No behaviour change: every t() value is the string that rendered before,
character for character — checked mechanically against this branch's base.
The e2e suite selects auth controls by visible text ('Create account',
'Username must be at least 4 characters'), so a green run is the proof.

Two fixes fell out of the extraction:
- AuthPasswordField had label='Password' as a prop default, so neither
  LoginView nor Signup passed one. A default cannot be translated at module
  scope, so the label moves to the call sites and both now pass it.
- AuthDivider defaulted children to 'or' for the same reason; it now falls
  back to t('or').
2026-08-12 20:03:02 +08:00
Mo Tarbin
97a7b12d87 add global error handling and enhance error tracking capabilities 2026-08-12 00:32:38 -04:00
Mo Tarbin
c88790abac feat(analytics): enhance event tracking and add privacy settings for analytics 2026-08-11 23:28:04 -04:00
Mohamad Tarbin
0de0d3ebc0 New translations settings.json (Chinese Simplified)
[ci skip]
2026-08-11 23:05:23 -04:00
Mohamad Tarbin
7eba12e718 Merge pull request #202 from donetick/l10n_develop
New Crowdin updates
2026-08-11 21:45:51 -04:00
Mohamad Tarbin
41c58505fa Merge branch 'develop' into l10n_develop 2026-08-11 21:45:30 -04:00
Mo Tarbin
a58888f0e6 feat(analytics): integrate PostHog for event tracking and user consent management
- Added PostHog SDK to package.json for analytics tracking.
- Implemented analytics consent management with separate toggles for analytics and crash reporting.
- Created analytics module to handle initialization, event tracking, and user identification.
- Integrated analytics tracking into various components including onboarding, feedback prompts, and chore creation.
- Added PrivacyAnalyticsSettings view for managing user consent preferences.
- Enhanced feedback submission with analytics tracking for user interactions.
- Updated device information utility for better context in analytics events.
- Refactored existing code to utilize new analytics functions and ensure proper event sanitization.
2026-08-11 21:42:51 -04:00
Mo Tarbin
fb4d4efe66 Add report bug functionality and settings sections refactor 2026-08-11 17:42:23 -04:00
Mohamad Tarbin
5c9afa0b6e New translations settings.json (French)
[ci skip]
2026-08-11 10:50:28 -04:00
Mohamad Tarbin
e2816bc54d New translations settings.json (Portuguese, Brazilian)
[ci skip]
2026-08-11 10:50:26 -04:00
Mohamad Tarbin
9b71ce910b New translations common.json (Portuguese, Brazilian)
[ci skip]
2026-08-11 10:50:24 -04:00
Mohamad Tarbin
946190c9e0 New translations settings.json (Chinese Simplified)
[ci skip]
2026-08-11 10:50:23 -04:00
Mohamad Tarbin
3e4a44bd00 New translations common.json (Chinese Simplified)
[ci skip]
2026-08-11 10:50:22 -04:00
Mohamad Tarbin
1c1e8e218b New translations settings.json (Dutch)
[ci skip]
2026-08-11 10:50:20 -04:00
Mohamad Tarbin
236deb465a New translations common.json (Dutch)
[ci skip]
2026-08-11 10:50:19 -04:00
Mohamad Tarbin
f0cdcd4f45 New translations settings.json (Japanese)
[ci skip]
2026-08-11 10:50:18 -04:00
Mohamad Tarbin
e26100c308 New translations common.json (Japanese)
[ci skip]
2026-08-11 10:50:16 -04:00
Mohamad Tarbin
f1f6d45e41 New translations settings.json (German)
[ci skip]
2026-08-11 10:50:15 -04:00
Mohamad Tarbin
32b86c0dde New translations common.json (German)
[ci skip]
2026-08-11 10:50:14 -04:00
Mohamad Tarbin
6d968d6a2d New translations settings.json (Arabic)
[ci skip]
2026-08-11 10:50:12 -04:00
Mohamad Tarbin
4642ad1918 New translations common.json (Arabic)
[ci skip]
2026-08-11 10:50:11 -04:00
Mohamad Tarbin
0d7f178d68 New translations settings.json (Spanish)
[ci skip]
2026-08-11 10:50:10 -04:00
Mohamad Tarbin
8af2ed3702 New translations common.json (Spanish)
[ci skip]
2026-08-11 10:50:08 -04:00
Mohamad Tarbin
25eb13d12a New translations common.json (French)
[ci skip]
2026-08-11 10:50:07 -04:00
Mohamad Tarbin
7e8802052a Merge pull request #206 from donetick/localization-settings
feat: enhance localization across settings views
2026-08-11 02:24:59 -04:00
Mo Tarbin
5199240148 feat: update localization files and import moment locales for Hebrew and Japanese 2026-08-11 02:23:35 -04:00
Mo Tarbin
8c9bf0db1d feat: enhance localization across settings views
- Updated LocalizationSettings to use translation keys for titles and descriptions.
- Refactored MFASettings to utilize translation for error messages, success messages, and UI text.
- Improved NotificationSetting by integrating translation for all user-facing strings.
- Initial Hebrew translation ( testing AI translation ( some of crowdin)
- Adjusted ProfileSettings to use translation for the edit photo title.
- Enhanced SettingsOverview to utilize translation for the early access label.
- Updated SidepanelSettings to implement translation for card names and descriptions.
- Refined StorageSettings to use translation for all storage-related messages and titles.
- Improved ThemeSettings to utilize translation for theme descriptions and titles.
2026-08-11 02:19:40 -04:00
Mohamad Tarbin
d34a7e7d34 Merge pull request #205 from donetick/localization-rtl
Localization rtl
2026-08-11 02:11:35 -04:00
Mo Tarbin
4e5222758d Add RTL support for ArrowBack icon in NavBar component 2026-08-11 02:10:12 -04:00
Mo Tarbin
6ace462797 Add RTL support for JoyToggleButtonGroup and implement i18n for search actions 2026-08-11 02:09:56 -04:00
Mo Tarbin
7e4b4b8fd6 Refactor RTL support in contexts and NavBar; update CSS for improved direction handling 2026-08-11 01:47:34 -04:00
Mohamad Tarbin
9bc82b9dc5 Merge pull request #204 from donetick/e2e-add-tests
Add end-to-end tests and enhance authentication flow
2026-08-11 00:59:01 -04:00
Mo Tarbin
dbd1fa72c4 remove e2e workflow 2026-08-11 00:55:40 -04:00
Mo Tarbin
43ffb3da5a Add end-to-end tests for chore and project functionalities, enhance auth flow, and update package version 2026-08-11 00:55:17 -04:00
Mohamad Tarbin
6100738db6 Merge pull request #203 from donetick/e2e-test
Add end-to-end testing setup with Playwright and initial tests
2026-08-10 23:10:03 -04:00
Mohamad Tarbin
c8f97f6957 New translations settings.json (German)
[ci skip]
2026-08-10 22:49:57 -04:00
Mohamad Tarbin
5cbc8a4c37 New translations common.json (German)
[ci skip]
2026-08-10 22:49:55 -04:00
Mohamad Tarbin
2b7dfe68c2 Add way to run the e2e test 2026-08-10 22:47:36 -04:00
Mohamad Tarbin
30ab8d755d New translations settings.json (Portuguese, Brazilian)
[ci skip]
2026-08-10 10:26:47 -04:00
Mohamad Tarbin
77dd689cc6 New translations common.json (Portuguese, Brazilian)
[ci skip]
2026-08-10 10:26:46 -04:00
Mohamad Tarbin
8dc2340b46 New translations settings.json (Chinese Simplified)
[ci skip]
2026-08-10 10:26:44 -04:00
Mohamad Tarbin
950807693c New translations common.json (Chinese Simplified)
[ci skip]
2026-08-10 10:26:42 -04:00
Mohamad Tarbin
d760ede92f New translations settings.json (Dutch)
[ci skip]
2026-08-10 10:26:41 -04:00
Mohamad Tarbin
784b667e95 New translations common.json (Dutch)
[ci skip]
2026-08-10 10:26:40 -04:00
Mohamad Tarbin
677ba280a3 New translations settings.json (Japanese)
[ci skip]
2026-08-10 10:26:38 -04:00
Mohamad Tarbin
e154d3b80e New translations common.json (Japanese)
[ci skip]
2026-08-10 10:26:36 -04:00
Mohamad Tarbin
7f4c47475b New translations settings.json (German)
[ci skip]
2026-08-10 10:26:35 -04:00
Mohamad Tarbin
9921289c9d New translations common.json (German)
[ci skip]
2026-08-10 10:26:34 -04:00
Mohamad Tarbin
80d3b1b7e2 New translations settings.json (Arabic)
[ci skip]
2026-08-10 10:26:32 -04:00
Mohamad Tarbin
36f7dc79e0 New translations common.json (Arabic)
[ci skip]
2026-08-10 10:26:31 -04:00
Mohamad Tarbin
96fc6eb448 New translations settings.json (Spanish)
[ci skip]
2026-08-10 10:26:29 -04:00
Mohamad Tarbin
9244070e30 New translations common.json (Spanish)
[ci skip]
2026-08-10 10:26:28 -04:00
Mohamad Tarbin
c5130ef46a New translations common.json (French)
[ci skip]
2026-08-10 10:26:26 -04:00
Mo Tarbin
c474041c7c Release 1.2.45
Some checks failed
Build validation / build (push) Has been cancelled
2026-08-10 01:40:04 -04:00
Mohamad Tarbin
c649e051d8 Merge pull request #201 from donetick/search-improvments
Search improvments
2026-08-10 01:19:43 -04:00
Mohamad Tarbin
207888affe Merge pull request #198 from donetick/share-with
Share with for circle
2026-08-10 01:19:33 -04:00
Mo Tarbin
5f0c5cdc5d Implement onboarding flow and circle invite handling with new utilities 2026-08-10 01:14:31 -04:00
Mo Tarbin
5b7594ac64 Add apple-app-site-association and assetlinks configuration for app linking 2026-08-10 01:13:06 -04:00
Mo Tarbin
90a38bf091 Add Capacitor Share integration and implement Circle invite sharing feature 2026-08-10 01:13:06 -04:00
Mo Tarbin
eecfc98ebc Refactor ArchivedTasks component: reorder imports, adjust notification hooks, and clean up unused code 2026-08-10 00:55:31 -04:00
Mohamad Tarbin
6428239be2 Merge pull request #200 from donetick/0809-fixes
Fix autocomplete issues and enhance task input handling
2026-08-10 00:54:52 -04:00
Mo Tarbin
4c6e244069 fix: clean up unused imports and simplify component structure in AddTaskModal 2026-08-10 00:53:40 -04:00
Mo Tarbin
af926b033e improve keyboard shortcuts handling 2026-08-10 00:19:57 -04:00
Mo Tarbin
b7bdfa7885 Implement global search feature with context, UI components, and routing updates 2026-08-10 00:16:38 -04:00
Mo Tarbin
309fd32d50 Enhance task input highlighting with smooth appearance animation and improved CSS keyframes for better visual feedback. 2026-08-09 13:19:32 -04:00
Mo Tarbin
2442b0c441 Fix: autocomplete not visible. refactor: streamline task input handling and improve smart input suggestions and and it's performance 2026-08-09 13:19:16 -04:00
Mohamad Tarbin
5d8095bd9c Merge pull request #199 from donetick/0809-fixes
0809 fixes
2026-08-09 13:17:08 -04:00
Mo Tarbin
1a8aeb84cc fix: if user type @Ni and then click Nick then what display was NiNick
fix: if user select assignee keep then if we don't see assignee in the title
2026-08-09 12:28:12 -04:00
Mo Tarbin
b2529a0091 fix autocomplete in addtaskmodal not displaying 2026-08-09 12:22:30 -04:00
Mo Tarbin
841f83d6ae Enhance file upload functionality with document scanning support and improved error handling 2026-08-09 12:00:30 -04:00
Mo Tarbin
731c46b26a Refactor AssigneePickerField to improve value handling and enhance UI/UX for assignee selection 2026-08-09 11:50:51 -04:00
Mo Tarbin
612924e7c3 Add theme background support for light and dark modes in StatusBarManager and ThemeContext
Fix: https://github.com/donetick/donetick/issues/759
2026-08-09 11:50:51 -04:00
Mo Tarbin
78760ceebb Refactor status bar theme handling, attempt to remove the black spaces 2026-08-09 11:50:51 -04:00
Mo Tarbin
e05aa2d169 JoinCircleView: enhance UI/UX with improved messaging, animations, and layout adjustments 2026-08-08 16:01:10 -04:00
Mo Tarbin
1ddf2539e0 Improve circle joining experiance 2026-08-08 13:40:21 -04:00
Mo Tarbin
83a5760959 Enhance error reporting functionality: add bug report option in settings, update error report modal, and refine feedback descriptions 2026-08-07 20:25:06 -04:00
Mo Tarbin
0215a78404 Release 1.2.38
Some checks failed
Build validation / build (push) Has been cancelled
2026-08-07 00:43:15 -04:00
Mohamad Tarbin
475806ac29 Error reporting (#196)
* implement error reporting modal and service integration

* Add session diagnostics for error reporting and enhance API failure tracking
2026-08-07 00:10:51 -04:00
Mohamad Tarbin
903d022877 New Crowdin updates (#191)
* New translations common.json (French)

[ci skip]

* New translations settings.json (French)

[ci skip]

* New translations common.json (Spanish)

[ci skip]

* New translations settings.json (Spanish)

[ci skip]

* New translations common.json (Arabic)

[ci skip]

* New translations settings.json (Arabic)

[ci skip]

* New translations settings.json (German)

[ci skip]

* New translations common.json (Japanese)

[ci skip]

* New translations settings.json (Japanese)

[ci skip]

* New translations common.json (Dutch)

[ci skip]

* New translations settings.json (Dutch)

[ci skip]

* New translations common.json (Chinese Simplified)

[ci skip]

* New translations settings.json (Chinese Simplified)

[ci skip]

* New translations common.json (Portuguese, Brazilian)

[ci skip]

* New translations settings.json (Portuguese, Brazilian)

[ci skip]
2026-08-06 23:57:23 -04:00
Mohamad Tarbin
5fb0b5b656 implement Application and MarketingApp components, enhance routing and user context management (#194) 2026-08-06 23:57:08 -04:00
Mohamad Tarbin
0b4b21bbb0 Refactor chore filtering and search performance (#195)
* Refactor assignment strategy handling in ChoreEdit component

* performance improvement : Refactor chore filtering logic and improve search functionality in MyChores and useChoreFilters hooks. reduce rerender etc...

* making sure the filter clear when it supposed to

* formating
2026-08-06 23:56:15 -04:00
Mo Tarbin
555c9d0774 Release 1.2.37
Some checks failed
Build validation / build (push) Has been cancelled
2026-08-05 02:12:21 -04:00
Mohamad Tarbin
48dbc7a152 Fix button issues, improve layout, and enhance notification handling (#192)
* Fix approval /declien button issue. update action buttons and improve layout

* feat(notifications): enhance notification handling and default templates across components

* fix(sync): improve sync handling to coalesce concurrent requests and prevent lost writes

there is BUG that was very annoying cause by race condition we we update and sync happen as we update and doesn't return the latest added task. this basically fix it
2026-08-05 01:14:39 -04:00
Mohamad Tarbin
784ffbcd00 Fix action button layout and enhance notification handling (#190)
* Fix approval /declien button issue. update action buttons and improve layout

* feat(notifications): enhance notification handling and default templates across components
2026-08-04 21:22:09 -04:00
Mohamad Tarbin
412faf4ad5 Revert "feat(auth): hide password login when disable_password_auth is set (#4…" (#189)
This reverts commit a05f8dc4a4.
2026-08-03 00:39:01 -04:00
Louis-Michel Couture
a05f8dc4a4 feat(auth): hide password login when disable_password_auth is set (#438) (#121)
Thank yWhen the backend resource endpoint reports disable_password_auth, the login
view hides the username/password form, the 'or' divider and the 'Create new
account' button, leaving only the configured SSO button — so SSO-only
instances present a clean login.

Auth options render only once the resource query has settled, so the password
form never flashes before being hidden (no FOUC) on SSO-only instances.

Also drops a dead setUserProfile(user) call in the post-auth effect (the
symbol was removed earlier, leaving a no-undef that broke lint on this file);
the profile now comes from useUserProfile().

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-03 00:36:05 -04:00
Mo Tarbin
778ae72777 Release 1.2.36
Some checks failed
Build validation / build (push) Has been cancelled
2026-08-02 23:30:42 -04:00
Mohamad Tarbin
dc88fe9704 unifying the empty state across components (#184) 2026-08-02 23:16:32 -04:00
Mohamad Tarbin
5da8724a7b Update release script to add --upload-only option and adjust build conditions; enhance ChoreCard styles for better layout (#188) 2026-08-02 23:16:23 -04:00
Mohamad Tarbin
f57c24e312 New Crowdin updates (#150)
* New translations chores.json (French)

[ci skip]

* New translations chores.json (Spanish)

[ci skip]

* New translations chores.json (Arabic)

[ci skip]

* New translations common.json (French)

[ci skip]

* New translations common.json (Spanish)

[ci skip]

* New translations common.json (Arabic)

[ci skip]

* New translations settings.json (French)

[ci skip]

* New translations settings.json (Spanish)

[ci skip]

* New translations settings.json (Arabic)

[ci skip]

* New translations settings.json (French)

[ci skip]

* New translations chores.json (German)

[ci skip]

* New translations settings.json (German)

[ci skip]

* New translations chores.json (Portuguese)

[ci skip]

* New translations common.json (Portuguese)

[ci skip]

* New translations settings.json (Portuguese)

[ci skip]

* New translations chores.json (Chinese Simplified)

[ci skip]

* New translations common.json (Chinese Simplified)

[ci skip]

* New translations settings.json (Chinese Simplified)

[ci skip]

* New translations chores.json (Portuguese, Brazilian)

[ci skip]

* New translations common.json (Portuguese, Brazilian)

[ci skip]

* New translations settings.json (Portuguese, Brazilian)

[ci skip]

* New translations common.json (German)

[ci skip]
2026-08-02 22:37:51 -04:00
Mo Tarbin
a077afb291 Release 1.2.35 2026-08-02 21:13:15 -04:00
Mohamad Tarbin
66d742d091 Remove gray panel in add task modal and update label interaction (#187)
* Remove the gray panel in add task modal

* migration clicking label to the new advance filtered.

* Fix: Clicking x on filter chip remove the filter condition
2026-08-02 20:53:41 -04:00
Mohamad Tarbin
d641710a41 Reshape onboarding v1.3 (#185)
* Initialize Onboarding Flow

* Shorten Onboarding

* Add Setup circle  and what are the current way to add tasks to the onboarding.
update donetick logo

* improve wording

* Skip onboarding flow for now

* Enable onboarding flow
2026-08-02 20:53:28 -04:00
Mohamad Tarbin
46fb2d5475 Add support for in app feedback (#176) 2026-08-02 16:36:51 -04:00
Mohamad Tarbin
b2949b2dd9 Enhance project management UI and handle bulk changes (#186)
* enable hold to select, remove once x1 chip, unify reschedule modal

* project title still show fromwhen login from different account
it's was cache in local storage so makdikngsure we do clean up

* Fix: show the button for creating task on the modal instead of the panel

* Bulk moving project, Move the button in scanner to the modal

* - Handle bulk change for projects
- allow no assignee being selected
- choreactionmenu become modal on mobile
2026-08-02 16:14:52 -04:00
Scott Anderson
9478f7d0f5 eslint@9, prettier (#105)
* Update build.yml to allow pushes to any branch

Remove branch restriction for push events, which prevents forked projects from running CI checks prior to creating a PR.

* eslint, prettier config only
2026-08-02 02:43:54 -04:00
Mohamad Tarbin
2e0ff5a10c Merge pull request #172 from scottanderson/patch-1
Update build.yml to allow pushes to any branch
2026-08-02 02:40:26 -04:00
Mohamad Tarbin
85826d8068 Merge pull request #141 from everysingletear/fix/available-languages
fix: sync AVAILABLE_LANGUAGES with shipped locales (add zh-CN; drop duplicate ja)
2026-07-31 01:55:40 -04:00
Mohamad Tarbin
9b0af72d74 Merge pull request #182 from donetick/issue-758
allow create task assigned to anyone
2026-07-30 20:30:48 -04:00
Mo Tarbin
eb088a259a allow create task assigned to anyone 2026-07-30 20:29:17 -04:00
Scott Anderson
dcd157274a Update build.yml to allow pushes to any branch
Remove branch restriction for push events, which prevents forked projects from running CI checks prior to creating a PR.
2026-07-30 16:54:09 -04:00
Mohamad Tarbin
19ed0ba3ab Merge pull request #180 from donetick/reshape-onboarding-v1
Reshape onboarding v1
2026-07-30 02:21:25 -04:00
Mo Tarbin
b8fe2833b6 allow numbers in username 2026-07-30 02:02:08 -04:00
Mo Tarbin
c39bbe5984 Remove Logo from sign up and login flow 2026-07-30 02:02:08 -04:00
Mo Tarbin
9950d8ff6c Remove Logo from sign up and login flow 2026-07-30 02:02:08 -04:00
Mo Tarbin
99b467488f revamp login and signup screens 2026-07-30 02:02:08 -04:00
Mohamad Tarbin
8dfaa4d8c6 Merge pull request #179 from donetick/revamp-modals
Revamp modals
2026-07-30 01:34:26 -04:00
Mo Tarbin
000e4d0d61 update Button Radius 2026-07-30 01:32:21 -04:00
Mo Tarbin
0132408eec Unify modals and buttons style and layout 2026-07-29 10:38:20 -04:00
Mo Tarbin
b4d46caee4 Release 1.2.34 2026-07-29 02:45:24 -04:00
Scott Anderson
dfa07aa5d1 dependabot 2026-07-28 22:44:36 -04:00
Mo Tarbin
f92063dfe9 hotfix: OAuth login being aborted by a forced logout on Android
The app logged itself out in the middle of the OAuth code exchange,
tearing down the page before the callback request could finish.

On resume, the deep-link handler and the sync-on-resume listener fire in
the same tick. Sync ran first, hit a 401 (no session exists yet), and
ApiClient treated that as an expired session: it cleared tokens, wiped
the offline DB, and hard-navigated to /login via window.location.href.
That reload aborted the in-flight POST /auth/oauth2/callback, surfacing
as "Authentication request failed TypeError: Failed to fetch".
2026-07-28 20:45:29 -04:00
Mo Tarbin
ce07e467f3 release 1.2.33 2026-07-28 17:40:44 -04:00
Mohamad Tarbin
f6adc99187 Merge pull request #171 from donetick/support-audio-input
Support audio input
2026-07-28 02:31:38 -04:00
Mohamad Tarbin
950645cc96 Merge branch 'develop' into support-audio-input 2026-07-28 02:31:16 -04:00
Mo Tarbin
cb71b6074d Add Previous for the widgets 2026-07-28 02:15:56 -04:00
Mo Tarbin
bb87dc2e14 Add quick capture widget 2026-07-28 02:02:44 -04:00
Mo Tarbin
ce49e4afc6 Move cancel button to the modal 2026-07-28 02:02:24 -04:00
Mo Tarbin
ea89f90edb fix label picker keep open wnhe something chosen 2026-07-28 00:17:40 -04:00
Mo Tarbin
8cba22024a Fix Choretoolbar display active when it should always stay neutral 2026-07-28 00:03:04 -04:00
Mo Tarbin
9044193aa3 Fix Adding labels to match the way we have it in EditChore 2026-07-27 23:55:27 -04:00
Mo Tarbin
b145bb422b reduce the noise by making the color just a small badge 2026-07-27 23:43:39 -04:00
Mo Tarbin
5a4f67d70a Add back Quick access to the filters 2026-07-27 22:45:49 -04:00
Mo Tarbin
09c3dbbebc fix(voice): harden native recognition loop and match labels to existing ones only
Add heartbeat/timeout recovery for silently-dead Android recognizer sessions,
wait for and prefer the late-arriving final transcript over the interim
partial, and guard against duplicate commits when it lands after restart.
Bias native recognition toward circle member/label names via contextualStrings
so unfamiliar names aren't auto-corrected. Restrict spoken "label X" to
existing labels (exact or closest fuzzy match) instead of creating new ones,
removing the now-unused label-creation flow from AddTaskModal/VoicePanel/
parseVoiceTask.
2026-07-26 21:41:56 -04:00
Mo Tarbin
ad1f85ffcc Update Gemfile 2026-07-26 13:26:51 -04:00
Mohamad Tarbin
ec8eb39599 Merge pull request #165 from donetick/hotfix-07-24
Attempt to fix capacitor sqlite error and update social login to make…
2026-07-25 17:41:05 -04:00
Mo Tarbin
adca4c3af9 Attempt to fix capacitor sqlite error and update social login to make sure we exclude any facebook sdk 2026-07-25 17:35:43 -04:00
everysingletear
f5da57d7f7 fix: sync AVAILABLE_LANGUAGES with shipped locales (add zh-CN; drop duplicate ja)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-21 15:58:48 +08:00
Mo Tarbin
a133b271d0 ability to parse user and label 2026-07-20 09:54:39 -04:00
Mo Tarbin
e0257445ef Add Initial Voice Input 2026-07-20 01:41:42 -04:00
Mohamad Tarbin
65c774d0d0 Merge pull request #159 from donetick/respecting-ca-from-user-store
fix(android): trust user-installed CA certificates
2026-07-18 18:00:01 -04:00
Mo Tarbin
2e5f0a3725 fix(android): trust user-installed CA certificates 2026-07-18 17:58:41 -04:00
Mohamad Tarbin
ebae9b78b1 Merge pull request #158 from donetick/add-widget-ios-android
Add home-screen widgets for iOS and Android (Today, Next 7 Days, People)
2026-07-18 17:14:41 -04:00
Mo Tarbin
b73586f81b Add support for iOS and android widgets 2026-07-18 17:10:23 -04:00
Mo Tarbin
34aa76504e add end-to-end testing setup with Playwright and initial test cases 2026-05-30 23:04:40 -04:00
502 changed files with 47211 additions and 14311 deletions

View File

@@ -1,36 +0,0 @@
module.exports = {
root: true,
env: { browser: true, es2020: true },
extends: [
'eslint:recommended',
'plugin:react/recommended',
'plugin:react/jsx-runtime',
'plugin:react-hooks/recommended',
'plugin:prettier/recommended',
'plugin:tailwindcss/recommended',
],
ignorePatterns: [
'dist',
'.eslintrc.cjs',
'tailwind.config.js',
'postcss.config.js',
],
parserOptions: { ecmaVersion: 'latest', sourceType: 'module' },
settings: { react: { version: '18.2' } },
plugins: [
'react-refresh',
'simple-import-sort',
'sort-destructure-keys',
'sort-keys-fix',
'prettier',
'tailwindcss',
],
rules: {
'react-refresh/only-export-components': [
'warn',
{ allowConstantExport: true },
],
'react/prop-types': 'off',
},
}

View File

@@ -2,7 +2,6 @@ name: Build validation
on:
push:
branches: [main]
pull_request:
jobs:
@@ -10,12 +9,13 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Use Node.js 20
uses: actions/setup-node@v4
with:
node-version: 20.x
cache: 'npm'
- name: Install dependencies
run: npm i
- run: npm run build
- uses: actions/checkout@v7
- name: Use Node.js 20
uses: actions/setup-node@v7
with:
node-version: 20.x
cache: 'npm'
- name: Install dependencies
run: npm i
- run: npm run build
- run: npm run lint:ci

6
.gitignore vendored
View File

@@ -43,4 +43,8 @@ fastlane/report.xml
fastlane/Preview.html
fastlane/.env
fastlane/*.log
vendor/bundle
vendor/bundle
# Claude Code skills cache
.agents/
skills-lock.json

13
.prettierignore Normal file
View File

@@ -0,0 +1,13 @@
LICENSE.md
dist/
build/
vendor/
android/
ios/
resources/
.bundle/
.agents/
.impeccable/
package-lock.json
Gemfile.lock
skills-lock.json

View File

@@ -1,13 +0,0 @@
{
"arrowParens": "avoid",
"bracketSpacing": true,
"endOfLine": "auto",
"jsxBracketSameLine": false,
"printWidth": 80,
"semi": false,
"singleQuote": true,
"jsxSingleQuote": true,
"tabWidth": 2,
"trailingComma": "all",
"plugins": ["prettier-plugin-tailwindcss"]
}

View File

@@ -12,15 +12,16 @@ Builds and (optionally) uploads signed Android and iOS release artifacts from th
Common flags on `release.sh`:
| Flag | Effect |
|---|---|
| `--android` / `--ios` | Build only one platform |
| `--bump minor\|major` | Bump type (default: `patch`) |
| `--skip-bump` | Build with the current version, don't bump |
| `--upload` | Upload after building |
| `--track TRACK` | Play Store track for `--upload` (default: `internal`) |
| Flag | Effect |
| --------------------- | ----------------------------------------------------- |
| `--android` / `--ios` | Build only one platform |
| `--bump minor\|major` | Bump type (default: `patch`) |
| `--skip-bump` | Build with the current version, don't bump |
| `--upload` | Upload after building |
| `--track TRACK` | Play Store track for `--upload` (default: `internal`) |
Outputs:
- Android: `android/app/build/outputs/bundle/release/app-release.aab`
- iOS: `build/ios/Donetick.ipa`
@@ -36,15 +37,15 @@ Outputs:
All secrets live in Vaultwarden (`https://www.bitwarden.com`) as Secure Notes, named exactly as below:
| Vault item name | Written to | Encoding |
|---|---|---|
| Donetick Google Services Android | `android/app/google-services.json` | raw |
| Donetick Android Keystore | `android/app/release/donetick.jks` | base64 |
| Donetick Keystore Password | (used inline for `android/keystore.properties`) | raw |
| Donetick Google Play Service Account | `android/play-service-account.json` | raw |
| Donetick Google Services iOS | `ios/App/App/GoogleService-Info.plist` | raw |
| Donetick App Store Connect Key | `ios/AuthKey_84F695CDQ3.p8` | base64 |
| Donetick Env Production | `.env.production` | raw |
| Vault item name | Written to | Encoding |
| ------------------------------------ | ----------------------------------------------- | -------- |
| Donetick Google Services Android | `android/app/google-services.json` | raw |
| Donetick Android Keystore | `android/app/release/donetick.jks` | base64 |
| Donetick Keystore Password | (used inline for `android/keystore.properties`) | raw |
| Donetick Google Play Service Account | `android/play-service-account.json` | raw |
| Donetick Google Services iOS | `ios/App/App/GoogleService-Info.plist` | raw |
| Donetick App Store Connect Key | `ios/AuthKey_84F695CDQ3.p8` | base64 |
| Donetick Env Production | `.env.production` | raw |
None of these files are committed to git — all covered by `.gitignore`.
@@ -57,6 +58,7 @@ The App target uses **manual signing for Release**, not Automatic. This was a de
- `fastlane/Fastfile`'s `ios release` lane passes explicit `export_options: { signingStyle: "manual", provisioningProfiles: {...} }` to `build_app` — do **not** switch this back to `-allowProvisioningUpdates`/automatic without a good reason, it re-triggers the ambiguous cert selection.
**If you ever add/change an entitlement** (new Capability in Signing & Capabilities, e.g. a new permission), the existing provisioning profile becomes stale and exports will fail with an error like `"App.app" requires a provisioning profile with the X feature`. Fix:
1. developer.apple.com → Identifiers → `com.donetick.app` → confirm the new capability is checked
2. developer.apple.com → Profiles → find `Donetick App Store(fastline)` → regenerate it → download
3. Install it locally: get its UUID (`security cms -D -i <file>.mobileprovision | plutil -extract UUID xml1 -o - -`) and copy to `~/Library/MobileDevice/Provisioning Profiles/<uuid>.mobileprovision`

View File

@@ -1,7 +1,7 @@
GEM
remote: https://rubygems.org/
specs:
CFPropertyList (3.0.9)
CFPropertyList (3.0.8)
abbrev (0.1.2)
addressable (2.9.0)
public_suffix (>= 2.0.2, < 8.0)
@@ -230,4 +230,4 @@ DEPENDENCIES
fastlane
BUNDLED WITH
1.17.2
2.4.10

View File

@@ -13,8 +13,8 @@ android {
applicationId "com.donetick.app"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 37
versionName "1.2.16"
versionCode 70
versionName "1.2.49"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
aaptOptions {
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.

View File

@@ -9,15 +9,19 @@ android {
apply from: "../capacitor-cordova-android-plugins/cordova.variables.gradle"
dependencies {
implementation project(':capacitor-community-in-app-review')
implementation project(':capacitor-community-speech-recognition')
implementation project(':capacitor-community-sqlite')
implementation project(':capacitor-app')
implementation project(':capacitor-browser')
implementation project(':capacitor-device')
implementation project(':capacitor-haptics')
implementation project(':capacitor-local-llm')
implementation project(':capacitor-local-notifications')
implementation project(':capacitor-network')
implementation project(':capacitor-preferences')
implementation project(':capacitor-push-notifications')
implementation project(':capacitor-share')
implementation project(':capacitor-status-bar')
implementation project(':capgo-capacitor-document-scanner')
implementation project(':capgo-capacitor-nfc')

View File

@@ -21,17 +21,13 @@
{
"minApi": 28,
"maxApi": 30,
"baselineProfiles": [
"baselineProfiles/1/app-release.dm"
]
"baselineProfiles": ["baselineProfiles/1/app-release.dm"]
},
{
"minApi": 31,
"maxApi": 2147483647,
"baselineProfiles": [
"baselineProfiles/0/app-release.dm"
]
"baselineProfiles": ["baselineProfiles/0/app-release.dm"]
}
],
"minSdkVersionForDexing": 28
}
}

View File

@@ -8,6 +8,7 @@
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme"
android:networkSecurityConfig="@xml/network_security_config"
android:usesCleartextTraffic="true">
<activity
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|smallestScreenSize|screenLayout|uiMode|navigation"
@@ -29,6 +30,17 @@
<data android:scheme="donetick" />
</intent-filter>
<!-- Verified App Link for Circle invites hosted by Donetick Cloud. -->
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:scheme="https"
android:host="app.donetick.com"
android:pathPrefix="/circle/join" />
</intent-filter>
<!-- NFC NDEF dispatch: open app directly when a donetick:// tag is scanned -->
<intent-filter>
<action android:name="android.nfc.action.NDEF_DISCOVERED" />
@@ -37,6 +49,64 @@
</intent-filter>
</activity>
<!-- Home-screen widgets -->
<receiver
android:name=".widget.TodayWidgetProvider"
android:exported="false"
android:label="@string/widget_today_label">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
</intent-filter>
<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/widget_today_info" />
</receiver>
<receiver
android:name=".widget.WeekWidgetProvider"
android:exported="false"
android:label="@string/widget_week_label">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
</intent-filter>
<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/widget_week_info" />
</receiver>
<receiver
android:name=".widget.PeopleWidgetProvider"
android:exported="false"
android:label="@string/widget_people_label">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
</intent-filter>
<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/widget_people_info" />
</receiver>
<receiver
android:name=".widget.QuickCaptureWidgetProvider"
android:exported="false"
android:label="@string/widget_quick_label">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
</intent-filter>
<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/widget_quick_capture_info" />
</receiver>
<service
android:name=".widget.WidgetListService"
android:exported="false"
android:permission="android.permission.BIND_REMOTEVIEWS" />
<activity
android:name=".widget.WidgetConfigActivity"
android:exported="true"
android:theme="@style/AppTheme">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_CONFIGURE" />
</intent-filter>
</activity>
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.fileprovider"
@@ -66,7 +136,22 @@
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" tools:node="remove" />
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" tools:node="remove" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<!-- Remove advertising ID / Privacy Sandbox ad permissions if transitive SDKs inject them. -->
<uses-permission android:name="com.google.android.gms.permission.AD_ID" tools:node="remove" />
<uses-permission android:name="android.permission.ACCESS_ADSERVICES_ATTRIBUTION" tools:node="remove" />
<uses-permission android:name="android.permission.ACCESS_ADSERVICES_AD_ID" tools:node="remove" />
<uses-permission android:name="android.permission.ACCESS_ADSERVICES_CUSTOM_AUDIENCE" tools:node="remove" />
<uses-permission android:name="android.permission.ACCESS_ADSERVICES_TOPICS" tools:node="remove" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.NFC" />
<!-- Voice task capture -->
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<!-- Android 11+ package visibility for the system speech recognition service -->
<queries>
<intent>
<action android:name="android.speech.RecognitionService" />
</intent>
</queries>
</manifest>

View File

@@ -9,11 +9,20 @@ import com.getcapacitor.PluginHandle;
import com.getcapacitor.Plugin;
import android.content.Intent;
import android.nfc.NfcAdapter;
import android.os.Bundle;
import android.util.Log;
import com.donetick.app.widget.WidgetBridgePlugin;
public class MainActivity extends BridgeActivity implements ModifiedMainActivityForSocialLoginPlugin {
@Override
public void onCreate(Bundle savedInstanceState) {
registerPlugin(WidgetBridgePlugin.class);
super.onCreate(savedInstanceState);
}
// Capacitor only forwards ACTION_VIEW deep links, so normalize Donetick NFC intents before dispatch.
@Override
protected void onNewIntent(Intent intent) {

View File

@@ -0,0 +1,131 @@
package com.donetick.app.widget;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.BitmapShader;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Rect;
import android.graphics.Shader;
import android.graphics.Typeface;
import android.util.Log;
import java.io.File;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.Locale;
/**
* Member avatars for the widgets: downloads and circle-crops profile photos
* (cached on disk for a day) and falls back to a colored initials disc.
* Downloads must only happen from a background/binder thread —
* RemoteViewsFactory.onDataSetChanged qualifies.
*/
final class AvatarCache {
private static final String TAG = "DonetickWidget";
private static final String DIR = "widget_avatars";
private static final long MAX_AGE_MS = 24 * 60 * 60 * 1000;
private static final int SIZE_PX = 96;
// Joy-ish palette for initials discs; picked by hashing the member id so
// each person keeps a stable color.
private static final int[] PALETTE = {
0xFF0B6BCB, 0xFF147D57, 0xFF9C4DD3, 0xFFC2410C,
0xFF0E7490, 0xFFB02A5B, 0xFF5B21B6, 0xFF937800,
};
private AvatarCache() {}
/** Photo avatar or null; never throws, never touches the network on failure loops. */
static Bitmap photo(Context context, WidgetStore.Member member) {
if (member == null || member.image == null || !member.image.startsWith("http")) {
return null;
}
try {
File dir = new File(context.getCacheDir(), DIR);
if (!dir.exists()) dir.mkdirs();
File file = new File(dir, member.id + ".png");
if (file.exists()
&& System.currentTimeMillis() - file.lastModified() < MAX_AGE_MS) {
return BitmapFactory.decodeFile(file.getAbsolutePath());
}
Bitmap downloaded = download(member.image);
if (downloaded == null) {
// Keep serving a stale copy rather than nothing.
return file.exists() ? BitmapFactory.decodeFile(file.getAbsolutePath()) : null;
}
Bitmap circled = circleCrop(downloaded);
try (FileOutputStream out = new FileOutputStream(file)) {
circled.compress(Bitmap.CompressFormat.PNG, 100, out);
}
return circled;
} catch (Exception e) {
Log.w(TAG, "Avatar load failed for member " + member.id, e);
return null;
}
}
/** Colored disc with the member's first initial — the no-photo fallback. */
static Bitmap initials(WidgetStore.Member member) {
String name = member != null && member.name != null ? member.name.trim() : "";
String letter = name.isEmpty()
? "?"
: new String(Character.toChars(name.codePointAt(0))).toUpperCase(Locale.getDefault());
int color = PALETTE[Math.abs((member != null ? member.id : "?").hashCode()) % PALETTE.length];
Bitmap bitmap = Bitmap.createBitmap(SIZE_PX, SIZE_PX, Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(bitmap);
Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
paint.setColor(color);
canvas.drawCircle(SIZE_PX / 2f, SIZE_PX / 2f, SIZE_PX / 2f, paint);
Paint text = new Paint(Paint.ANTI_ALIAS_FLAG);
text.setColor(Color.WHITE);
text.setTypeface(Typeface.create(Typeface.DEFAULT, Typeface.BOLD));
text.setTextSize(SIZE_PX * 0.42f);
text.setTextAlign(Paint.Align.CENTER);
Rect bounds = new Rect();
text.getTextBounds(letter, 0, letter.length(), bounds);
canvas.drawText(letter, SIZE_PX / 2f, SIZE_PX / 2f + bounds.height() / 2f, text);
return bitmap;
}
/** Best avatar for a member: photo when available, initials otherwise. */
static Bitmap get(Context context, WidgetStore.Member member) {
Bitmap photo = photo(context, member);
return photo != null ? photo : initials(member);
}
private static Bitmap download(String imageUrl) {
HttpURLConnection connection = null;
try {
connection = (HttpURLConnection) new URL(imageUrl).openConnection();
connection.setConnectTimeout(8000);
connection.setReadTimeout(10000);
if (connection.getResponseCode() != 200) return null;
try (InputStream stream = connection.getInputStream()) {
Bitmap raw = BitmapFactory.decodeStream(stream);
if (raw == null) return null;
return Bitmap.createScaledBitmap(raw, SIZE_PX, SIZE_PX, true);
}
} catch (Exception e) {
Log.w(TAG, "Avatar download failed", e);
return null;
} finally {
if (connection != null) connection.disconnect();
}
}
private static Bitmap circleCrop(Bitmap source) {
Bitmap output = Bitmap.createBitmap(SIZE_PX, SIZE_PX, Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(output);
Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
paint.setShader(new BitmapShader(source, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP));
canvas.drawCircle(SIZE_PX / 2f, SIZE_PX / 2f, SIZE_PX / 2f, paint);
return output;
}
}

View File

@@ -0,0 +1,20 @@
package com.donetick.app.widget;
import android.appwidget.AppWidgetManager;
import android.appwidget.AppWidgetProvider;
import android.content.Context;
import com.donetick.app.R;
/** "People" home-screen widget: every circle member with their task load. */
public class PeopleWidgetProvider extends AppWidgetProvider {
@Override
public void onUpdate(Context context, AppWidgetManager manager, int[] appWidgetIds) {
for (int id : appWidgetIds) {
manager.updateAppWidget(id, WidgetUi.build(context, WidgetUi.MODE_PEOPLE, id));
}
// Reload rows; the factory refreshes from the server when stale, which
// is what keeps the widget current while the app stays closed.
manager.notifyAppWidgetViewDataChanged(appWidgetIds, R.id.widget_list);
}
}

View File

@@ -0,0 +1,47 @@
package com.donetick.app.widget;
import android.app.PendingIntent;
import android.appwidget.AppWidgetManager;
import android.appwidget.AppWidgetProvider;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.widget.RemoteViews;
import com.donetick.app.R;
/**
* "Quick Capture" home-screen widget: three shortcuts straight into the
* add-task flow (type, scan, speak). Purely a launcher — it shows no task
* data, so it never needs a refresh cycle.
*/
public class QuickCaptureWidgetProvider extends AppWidgetProvider {
// Handled in src/CapacitorListener.js → /chores?add_task=1[&mode=…]
private static final String URI_TYPE = "donetick://chores/add";
private static final String URI_SCAN = "donetick://chores/add?mode=scan";
private static final String URI_VOICE = "donetick://chores/add?mode=voice";
@Override
public void onUpdate(Context context, AppWidgetManager manager, int[] appWidgetIds) {
for (int id : appWidgetIds) {
manager.updateAppWidget(id, build(context));
}
}
private static RemoteViews build(Context context) {
RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.widget_quick_capture);
views.setOnClickPendingIntent(R.id.quick_type, deepLink(context, 10, URI_TYPE));
views.setOnClickPendingIntent(R.id.quick_scan, deepLink(context, 11, URI_SCAN));
views.setOnClickPendingIntent(R.id.quick_voice, deepLink(context, 12, URI_VOICE));
return views;
}
private static PendingIntent deepLink(Context context, int requestCode, String uri) {
Intent intent = new Intent(context, com.donetick.app.MainActivity.class);
intent.setAction(Intent.ACTION_VIEW);
// Distinct data per tile so the three PendingIntents aren't collapsed.
intent.setData(Uri.parse(uri));
return PendingIntent.getActivity(context, requestCode, intent,
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
}
}

View File

@@ -0,0 +1,27 @@
package com.donetick.app.widget;
import android.appwidget.AppWidgetManager;
import android.appwidget.AppWidgetProvider;
import android.content.Context;
import com.donetick.app.R;
/** "Today" home-screen widget: tasks due today plus anything awaiting approval. */
public class TodayWidgetProvider extends AppWidgetProvider {
@Override
public void onUpdate(Context context, AppWidgetManager manager, int[] appWidgetIds) {
for (int id : appWidgetIds) {
manager.updateAppWidget(id, WidgetUi.build(context, WidgetUi.MODE_TODAY, id));
}
// Reload rows; the factory refreshes from the server when stale, which
// is what keeps the widget current while the app stays closed.
manager.notifyAppWidgetViewDataChanged(appWidgetIds, R.id.widget_list);
}
@Override
public void onDeleted(Context context, int[] appWidgetIds) {
for (int id : appWidgetIds) {
WidgetStore.removeWidgetOptions(context, id);
}
}
}

View File

@@ -0,0 +1,25 @@
package com.donetick.app.widget;
import android.appwidget.AppWidgetManager;
import android.appwidget.AppWidgetProvider;
import android.content.Context;
import com.donetick.app.R;
/** "Next 7 Days" home-screen widget: upcoming tasks grouped by day. */
public class WeekWidgetProvider extends AppWidgetProvider {
@Override
public void onUpdate(Context context, AppWidgetManager manager, int[] appWidgetIds) {
for (int id : appWidgetIds) {
manager.updateAppWidget(id, WidgetUi.build(context, WidgetUi.MODE_WEEK, id));
}
manager.notifyAppWidgetViewDataChanged(appWidgetIds, R.id.widget_list);
}
@Override
public void onDeleted(Context context, int[] appWidgetIds) {
for (int id : appWidgetIds) {
WidgetStore.removeWidgetOptions(context, id);
}
}
}

View File

@@ -0,0 +1,31 @@
package com.donetick.app.widget;
import com.getcapacitor.Plugin;
import com.getcapacitor.PluginCall;
import com.getcapacitor.PluginMethod;
import com.getcapacitor.annotation.CapacitorPlugin;
/**
* JS bridge for the home-screen widgets (see src/service/WidgetService.js).
* Persists the task snapshot + API config and redraws the widgets.
*/
@CapacitorPlugin(name = "WidgetBridge")
public class WidgetBridgePlugin extends Plugin {
@PluginMethod
public void update(PluginCall call) {
String data = call.getString("data");
String config = call.getString("config");
if (data != null) WidgetStore.saveData(getContext(), data);
if (config != null) WidgetStore.saveConfig(getContext(), config);
WidgetUi.refreshAll(getContext());
call.resolve();
}
@PluginMethod
public void clear(PluginCall call) {
WidgetStore.clear(getContext());
WidgetUi.refreshAll(getContext());
call.resolve();
}
}

View File

@@ -0,0 +1,48 @@
package com.donetick.app.widget;
import android.app.Activity;
import android.appwidget.AppWidgetManager;
import android.content.Intent;
import android.os.Bundle;
import android.widget.Switch;
import com.donetick.app.R;
/**
* Placement / long-press configuration for the Today and Next 7 Days widgets.
* One option for now: include tasks assigned to other circle members
* (stored per appWidgetId so mixed setups work side by side).
*/
public class WidgetConfigActivity extends Activity {
private int appWidgetId = AppWidgetManager.INVALID_APPWIDGET_ID;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
appWidgetId = getIntent().getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID,
AppWidgetManager.INVALID_APPWIDGET_ID);
// Cancelled result until Save, so backing out never adds a half-configured widget.
setResult(RESULT_CANCELED, resultIntent());
if (appWidgetId == AppWidgetManager.INVALID_APPWIDGET_ID) {
finish();
return;
}
setContentView(R.layout.widget_config);
Switch includeOthers = findViewById(R.id.config_include_others);
includeOthers.setChecked(WidgetStore.includeOthers(this, appWidgetId));
findViewById(R.id.config_save).setOnClickListener(v -> {
WidgetStore.setIncludeOthers(this, appWidgetId, includeOthers.isChecked());
WidgetUi.refreshAll(this);
setResult(RESULT_OK, resultIntent());
finish();
});
}
private Intent resultIntent() {
return new Intent().putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId);
}
}

View File

@@ -0,0 +1,291 @@
package com.donetick.app.widget;
import android.appwidget.AppWidgetManager;
import android.content.Context;
import android.content.Intent;
import android.graphics.Bitmap;
import android.net.Uri;
import android.view.View;
import android.widget.RemoteViews;
import android.widget.RemoteViewsService;
import androidx.core.content.ContextCompat;
import com.donetick.app.R;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
/** Feeds task rows, day-group headers, and people rows to the widget ListView. */
public class WidgetListService extends RemoteViewsService {
@Override
public RemoteViewsFactory onGetViewFactory(Intent intent) {
String mode = intent.getStringExtra(WidgetUi.EXTRA_MODE);
int appWidgetId = intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID,
AppWidgetManager.INVALID_APPWIDGET_ID);
return new WidgetListFactory(getApplicationContext(),
mode != null ? mode : WidgetUi.MODE_TODAY, appWidgetId);
}
static class Row {
final String header; // non-null for day-group headers
final WidgetStore.Task task; // non-null for task rows
final WidgetStore.Member person; // non-null for people rows
int todayCount;
int weekCount;
Row(String header, WidgetStore.Task task, WidgetStore.Member person) {
this.header = header;
this.task = task;
this.person = person;
}
}
static class WidgetListFactory implements RemoteViewsFactory {
private final Context context;
private final String mode;
private final int appWidgetId;
private List<Row> rows = new ArrayList<>();
private final Map<String, Bitmap> avatars = new HashMap<>();
private String myUserId;
private boolean includeOthers;
WidgetListFactory(Context context, String mode, int appWidgetId) {
this.context = context;
this.mode = mode;
this.appWidgetId = appWidgetId;
}
@Override
public void onCreate() {}
@Override
public void onDataSetChanged() {
// Runs on a binder thread, so synchronous network work is allowed
// here. This is the background-refresh path: the 30-minute
// updatePeriodMillis cycle lands here via notifyAppWidgetViewDataChanged.
boolean refreshed = WidgetStore.refreshFromServerIfStale(context);
myUserId = WidgetStore.userId(context);
includeOthers = WidgetStore.includeOthers(context, appWidgetId);
List<WidgetStore.Task> allTasks = WidgetStore.loadTasks(context);
List<WidgetStore.Member> members = WidgetStore.loadMembers(context);
if (WidgetUi.MODE_PEOPLE.equals(mode)) {
rows = buildPeopleRows(allTasks, members);
loadAvatars(members);
} else {
List<WidgetStore.Task> tasks =
WidgetStore.visibleTasks(context, allTasks, includeOthers);
rows = WidgetUi.MODE_TODAY.equals(mode)
? buildTodayRows(tasks)
: buildWeekRows(tasks);
if (includeOthers) loadAvatars(members);
}
if (refreshed) {
// Counts and "Updated …" line live outside the list.
WidgetUi.updateHeaders(context);
}
}
/** Resolve avatars for everyone up front — getViewAt must not block. */
private void loadAvatars(List<WidgetStore.Member> members) {
avatars.clear();
for (WidgetStore.Member member : members) {
avatars.put(member.id, AvatarCache.get(context, member));
}
}
private List<Row> buildTodayRows(List<WidgetStore.Task> tasks) {
List<Row> result = new ArrayList<>();
for (WidgetStore.Task task : WidgetStore.todaySubset(tasks)) {
result.add(new Row(null, task, null));
}
return result;
}
private List<Row> buildWeekRows(List<WidgetStore.Task> tasks) {
List<Row> result = new ArrayList<>();
long startOfToday = WidgetStore.endOfDay(-1) + 1;
SimpleDateFormat dayFormat = new SimpleDateFormat("EEE, MMM d", Locale.getDefault());
List<WidgetStore.Task> approvals = new ArrayList<>();
List<WidgetStore.Task> scheduled = new ArrayList<>();
for (WidgetStore.Task task : tasks) {
if (task.approval) approvals.add(task);
else if (task.dueDate != null) scheduled.add(task);
}
if (!approvals.isEmpty()) {
result.add(new Row(context.getString(R.string.widget_group_approval), null, null));
for (WidgetStore.Task task : approvals) result.add(new Row(null, task, null));
}
String currentGroup = null;
for (WidgetStore.Task task : scheduled) {
String group;
if (task.dueDate < startOfToday) {
group = context.getString(R.string.widget_group_overdue);
} else if (task.dueDate <= WidgetStore.endOfDay(0)) {
group = context.getString(R.string.widget_group_today);
} else if (task.dueDate <= WidgetStore.endOfDay(1)) {
group = context.getString(R.string.widget_group_tomorrow);
} else {
group = dayFormat.format(new Date(task.dueDate));
}
if (!group.equals(currentGroup)) {
result.add(new Row(group, null, null));
currentGroup = group;
}
result.add(new Row(null, task, null));
}
return result;
}
/** One row per member with today/week workloads, busiest first. */
private List<Row> buildPeopleRows(List<WidgetStore.Task> tasks,
List<WidgetStore.Member> members) {
List<Row> result = new ArrayList<>();
List<WidgetStore.Task> todayTasks = WidgetStore.todaySubset(tasks);
for (WidgetStore.Member member : members) {
Row row = new Row(null, null, member);
for (WidgetStore.Task task : tasks) {
if (member.id.equals(task.assignedTo)) row.weekCount++;
}
for (WidgetStore.Task task : todayTasks) {
if (member.id.equals(task.assignedTo)) row.todayCount++;
}
result.add(row);
}
java.util.Collections.sort(result, (a, b) -> {
if (a.todayCount != b.todayCount) return b.todayCount - a.todayCount;
if (a.weekCount != b.weekCount) return b.weekCount - a.weekCount;
return a.person.name.compareToIgnoreCase(b.person.name);
});
return result;
}
@Override
public RemoteViews getViewAt(int position) {
if (position < 0 || position >= rows.size()) return null;
Row row = rows.get(position);
if (row.header != null) {
RemoteViews views = new RemoteViews(context.getPackageName(),
R.layout.widget_row_day_header);
views.setTextViewText(R.id.row_day, row.header);
return views;
}
if (row.person != null) {
return buildPersonRow(row);
}
WidgetStore.Task task = row.task;
RemoteViews views = new RemoteViews(context.getPackageName(),
R.layout.widget_row_task);
views.setTextViewText(R.id.row_title, task.name);
boolean overdue = !task.approval && task.dueDate != null
&& task.dueDate < System.currentTimeMillis();
int secondary = ContextCompat.getColor(context, R.color.widget_text_secondary);
int warning = ContextCompat.getColor(context, R.color.widget_warning);
int danger = ContextCompat.getColor(context, R.color.widget_overdue);
String meta;
int metaColor;
if (task.approval) {
meta = context.getString(R.string.widget_meta_approval);
metaColor = warning;
} else if (overdue) {
meta = context.getString(R.string.widget_meta_overdue);
metaColor = danger;
} else {
meta = android.text.format.DateFormat.getTimeFormat(context)
.format(new Date(task.dueDate));
metaColor = secondary;
}
views.setTextViewText(R.id.row_meta, meta);
views.setTextColor(R.id.row_meta, metaColor);
int ringColor;
if (task.approval) ringColor = warning;
else if (overdue || task.priority == 1) ringColor = danger;
else if (task.priority == 2) ringColor = warning;
else ringColor = ContextCompat.getColor(context, R.color.widget_ring_neutral);
views.setInt(R.id.row_ring, "setColorFilter", ringColor);
// In "everyone" mode, show who a task belongs to (own tasks stay clean).
Bitmap avatar = includeOthers && task.assignedTo != null
&& !task.assignedTo.equals(myUserId)
? avatars.get(task.assignedTo)
: null;
if (avatar != null) {
views.setImageViewBitmap(R.id.row_avatar, avatar);
views.setViewVisibility(R.id.row_avatar, View.VISIBLE);
} else {
views.setViewVisibility(R.id.row_avatar, View.GONE);
}
Intent fillIn = new Intent();
fillIn.setData(Uri.parse("donetick://chores/" + task.id));
views.setOnClickFillInIntent(R.id.row_root, fillIn);
return views;
}
private RemoteViews buildPersonRow(Row row) {
RemoteViews views = new RemoteViews(context.getPackageName(),
R.layout.widget_row_person);
Bitmap avatar = avatars.get(row.person.id);
if (avatar == null) avatar = AvatarCache.initials(row.person);
views.setImageViewBitmap(R.id.person_avatar, avatar);
views.setTextViewText(R.id.person_name, row.person.name);
views.setTextViewText(R.id.person_counts, context.getString(
R.string.widget_person_counts, row.todayCount, row.weekCount));
// Highlight the today-count when someone has work due today.
int accent = ContextCompat.getColor(context, R.color.widget_accent);
int secondary = ContextCompat.getColor(context, R.color.widget_text_secondary);
views.setTextColor(R.id.person_counts, row.todayCount > 0 ? accent : secondary);
Intent fillIn = new Intent();
fillIn.setData(Uri.parse("donetick://chores"));
views.setOnClickFillInIntent(R.id.person_root, fillIn);
return views;
}
@Override
public RemoteViews getLoadingView() {
return null;
}
@Override
public int getViewTypeCount() {
return 3;
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public boolean hasStableIds() {
return false;
}
@Override
public int getCount() {
return rows.size();
}
@Override
public void onDestroy() {}
}
}

View File

@@ -0,0 +1,346 @@
package com.donetick.app.widget;
import android.content.Context;
import android.content.SharedPreferences;
import android.util.Log;
import org.json.JSONArray;
import org.json.JSONObject;
import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.time.OffsetDateTime;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collections;
import java.util.List;
/**
* Shared storage + refresh logic for the home-screen widgets.
*
* The web app pushes a pre-filtered snapshot through WidgetBridgePlugin
* whenever its chores cache changes. When the app has not run for a while,
* {@link #refreshFromServerIfStale} re-fetches the list directly so the
* widget stays current in the background (driven by the 30-minute
* updatePeriodMillis cycle).
*
* Snapshot JSON: {version, lastUpdated,
* tasks:[{id, name, dueDate, priority, approval, assignedTo}],
* members:[{id, name, image}]}
* Config JSON: {serverUrl, token, userId}
*
* Since v2 the snapshot holds every member's tasks; widgets narrow it down to
* the current user unless the per-widget "include others" option is on.
*/
public final class WidgetStore {
private static final String TAG = "DonetickWidget";
private static final String PREFS = "donetick_widget";
private static final String KEY_DATA = "widget_tasks";
private static final String KEY_CONFIG = "widget_config";
private static final String KEY_INCLUDE_OTHERS_PREFIX = "include_others_";
// Same filtering window as src/service/WidgetService.js
private static final int WINDOW_DAYS = 7;
private static final int MAX_TASKS = 100;
// Don't hit the network if the app (or a previous refresh) updated the
// snapshot recently; also guards against notify->refresh loops.
private static final long STALE_MS = 10 * 60 * 1000;
private static final Object REFRESH_LOCK = new Object();
private WidgetStore() {}
public static class Task {
public String id;
public String name;
public Long dueDate; // epoch millis, null when unscheduled
public int priority;
public boolean approval;
public String assignedTo; // member userId, null when unassigned
}
public static class Member {
public String id;
public String name;
public String image; // avatar URL, may be null
}
private static SharedPreferences prefs(Context context) {
return context.getApplicationContext()
.getSharedPreferences(PREFS, Context.MODE_PRIVATE);
}
public static void saveData(Context context, String json) {
prefs(context).edit().putString(KEY_DATA, json).apply();
}
public static void saveConfig(Context context, String json) {
prefs(context).edit().putString(KEY_CONFIG, json).apply();
}
public static void clear(Context context) {
prefs(context).edit().clear().apply();
}
public static boolean hasConfig(Context context) {
return prefs(context).getString(KEY_CONFIG, null) != null;
}
/** The signed-in user's id from the pushed config, or null. */
public static String userId(Context context) {
try {
String raw = prefs(context).getString(KEY_CONFIG, null);
if (raw == null) return null;
Object id = new JSONObject(raw).opt("userId");
return id == null ? null : String.valueOf(id);
} catch (Exception e) {
return null;
}
}
/** Per-widget "include tasks assigned to others" option (default off). */
public static boolean includeOthers(Context context, int appWidgetId) {
return prefs(context).getBoolean(KEY_INCLUDE_OTHERS_PREFIX + appWidgetId, false);
}
public static void setIncludeOthers(Context context, int appWidgetId, boolean value) {
prefs(context).edit()
.putBoolean(KEY_INCLUDE_OTHERS_PREFIX + appWidgetId, value)
.apply();
}
public static void removeWidgetOptions(Context context, int appWidgetId) {
prefs(context).edit()
.remove(KEY_INCLUDE_OTHERS_PREFIX + appWidgetId)
.apply();
}
public static long lastUpdated(Context context) {
try {
String raw = prefs(context).getString(KEY_DATA, null);
if (raw == null) return 0;
return new JSONObject(raw).optLong("lastUpdated", 0);
} catch (Exception e) {
return 0;
}
}
public static List<Task> loadTasks(Context context) {
List<Task> tasks = new ArrayList<>();
try {
String raw = prefs(context).getString(KEY_DATA, null);
if (raw == null) return tasks;
JSONArray arr = new JSONObject(raw).optJSONArray("tasks");
if (arr == null) return tasks;
for (int i = 0; i < arr.length(); i++) {
JSONObject obj = arr.optJSONObject(i);
if (obj == null) continue;
Task task = new Task();
task.id = String.valueOf(obj.opt("id"));
task.name = obj.optString("name", "");
task.dueDate = obj.isNull("dueDate") ? null : obj.optLong("dueDate");
task.priority = obj.optInt("priority", 0);
task.approval = obj.optBoolean("approval", false);
// v1 snapshots carried only the user's own tasks and had no
// assignedTo — treat those rows as "mine".
task.assignedTo = obj.has("assignedTo")
? (obj.isNull("assignedTo") ? null : String.valueOf(obj.opt("assignedTo")))
: userId(context);
tasks.add(task);
}
} catch (Exception e) {
Log.e(TAG, "Failed to parse widget snapshot", e);
}
return tasks;
}
public static List<Member> loadMembers(Context context) {
List<Member> members = new ArrayList<>();
try {
String raw = prefs(context).getString(KEY_DATA, null);
if (raw == null) return members;
JSONArray arr = new JSONObject(raw).optJSONArray("members");
if (arr == null) return members;
for (int i = 0; i < arr.length(); i++) {
JSONObject obj = arr.optJSONObject(i);
if (obj == null) continue;
Member member = new Member();
member.id = String.valueOf(obj.opt("id"));
member.name = obj.optString("name", "");
member.image = obj.isNull("image") ? null : obj.optString("image", null);
members.add(member);
}
} catch (Exception e) {
Log.e(TAG, "Failed to parse widget members", e);
}
return members;
}
/**
* Tasks a today/week widget should render: everything when includeOthers,
* otherwise the user's own tasks plus approvals (which wait on them).
*/
public static List<Task> visibleTasks(Context context, List<Task> tasks, boolean includeOthers) {
if (includeOthers) return tasks;
String me = userId(context);
List<Task> mine = new ArrayList<>();
for (Task task : tasks) {
if (task.approval || (me != null && me.equals(task.assignedTo))) {
mine.add(task);
}
}
return mine;
}
/** Tasks the Today widget shows: awaiting approval, overdue, or due today. */
public static List<Task> todaySubset(List<Task> tasks) {
long endOfToday = endOfDay(0);
List<Task> subset = new ArrayList<>();
for (Task task : tasks) {
if (task.approval || (task.dueDate != null && task.dueDate <= endOfToday)) {
subset.add(task);
}
}
return subset;
}
public static long endOfDay(int daysFromNow) {
Calendar cal = Calendar.getInstance();
cal.add(Calendar.DAY_OF_YEAR, daysFromNow);
cal.set(Calendar.HOUR_OF_DAY, 23);
cal.set(Calendar.MINUTE, 59);
cal.set(Calendar.SECOND, 59);
cal.set(Calendar.MILLISECOND, 999);
return cal.getTimeInMillis();
}
/**
* Fetch /chores/ from the configured server and rebuild the snapshot.
* Returns true when a network refresh actually happened and succeeded.
* Safe to call from RemoteViewsFactory.onDataSetChanged (binder thread).
*/
public static boolean refreshFromServerIfStale(Context context) {
synchronized (REFRESH_LOCK) {
long age = System.currentTimeMillis() - lastUpdated(context);
if (age < STALE_MS) return false;
String rawConfig = prefs(context).getString(KEY_CONFIG, null);
if (rawConfig == null) return false;
HttpURLConnection connection = null;
try {
JSONObject config = new JSONObject(rawConfig);
String serverUrl = config.optString("serverUrl", "");
String token = config.optString("token", "");
if (serverUrl.isEmpty() || token.isEmpty()) return false;
URL url = new URL(serverUrl + "/chores/");
connection = (HttpURLConnection) url.openConnection();
connection.setConnectTimeout(10000);
connection.setReadTimeout(15000);
connection.setRequestProperty("Authorization", "Bearer " + token);
connection.setRequestProperty("Accept", "application/json");
if (connection.getResponseCode() != 200) {
// Expired token or server trouble — keep the last snapshot,
// the UI surfaces staleness via the "Updated …" line.
Log.w(TAG, "Widget refresh got HTTP " + connection.getResponseCode());
return false;
}
JSONArray chores = new JSONObject(readAll(connection.getInputStream()))
.optJSONArray("res");
if (chores == null) return false;
// The chores endpoint has no member profiles, so carry the
// member list over from the previous snapshot (it changes
// rarely and the app re-pushes it on every open).
JSONArray members = null;
String previous = prefs(context).getString(KEY_DATA, null);
if (previous != null) {
members = new JSONObject(previous).optJSONArray("members");
}
JSONObject snapshot = new JSONObject();
snapshot.put("version", 2);
snapshot.put("lastUpdated", System.currentTimeMillis());
snapshot.put("tasks", filterChores(chores));
snapshot.put("members", members == null ? new JSONArray() : members);
saveData(context, snapshot.toString());
return true;
} catch (Exception e) {
Log.w(TAG, "Widget background refresh failed", e);
return false;
} finally {
if (connection != null) connection.disconnect();
}
}
}
/** Mirror of buildWidgetTasks in src/service/WidgetService.js. */
private static JSONArray filterChores(JSONArray chores) throws Exception {
long cutoff = endOfDay(WINDOW_DAYS);
List<JSONObject> selected = new ArrayList<>();
for (int i = 0; i < chores.length(); i++) {
JSONObject chore = chores.optJSONObject(i);
if (chore == null || chore.opt("id") == null) continue;
boolean approval = chore.optInt("status", 0) == 3;
Long dueDate = parseDate(chore.optString("nextDueDate", null));
boolean inWindow = dueDate != null && dueDate <= cutoff;
if (!approval && !inWindow) continue;
JSONObject task = new JSONObject();
task.put("id", chore.opt("id"));
task.put("name", chore.optString("name", ""));
task.put("dueDate", dueDate == null ? JSONObject.NULL : dueDate);
task.put("priority", chore.optInt("priority", 0));
task.put("approval", approval);
task.put("assignedTo", chore.isNull("assignedTo")
? JSONObject.NULL
: String.valueOf(chore.opt("assignedTo")));
selected.add(task);
}
Collections.sort(selected, (a, b) -> {
boolean aApproval = a.optBoolean("approval");
boolean bApproval = b.optBoolean("approval");
if (aApproval != bApproval) return aApproval ? -1 : 1;
long aDue = a.isNull("dueDate") ? Long.MAX_VALUE : a.optLong("dueDate");
long bDue = b.isNull("dueDate") ? Long.MAX_VALUE : b.optLong("dueDate");
return Long.compare(aDue, bDue);
});
JSONArray result = new JSONArray();
for (int i = 0; i < selected.size() && i < MAX_TASKS; i++) {
result.put(selected.get(i));
}
return result;
}
private static Long parseDate(String value) {
if (value == null || value.isEmpty() || "null".equals(value)) return null;
try {
return OffsetDateTime.parse(value).toInstant().toEpochMilli();
} catch (Exception e) {
return null;
}
}
private static String readAll(InputStream stream) throws Exception {
StringBuilder builder = new StringBuilder();
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(stream, StandardCharsets.UTF_8))) {
String line;
while ((line = reader.readLine()) != null) {
builder.append(line);
}
}
return builder.toString();
}
}

View File

@@ -0,0 +1,141 @@
package com.donetick.app.widget;
import android.app.PendingIntent;
import android.appwidget.AppWidgetManager;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.view.View;
import android.widget.RemoteViews;
import com.donetick.app.MainActivity;
import com.donetick.app.R;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;
import java.util.Locale;
/** Builds the shell RemoteViews shared by the widgets and pushes updates. */
public final class WidgetUi {
public static final String MODE_TODAY = "today";
public static final String MODE_WEEK = "week";
public static final String MODE_PEOPLE = "people";
public static final String EXTRA_MODE = "com.donetick.app.widget.MODE";
private WidgetUi() {}
/** Full refresh: redraw headers and reload list content (used by the JS bridge). */
public static void refreshAll(Context context) {
updateHeaders(context);
AppWidgetManager manager = AppWidgetManager.getInstance(context);
manager.notifyAppWidgetViewDataChanged(
widgetIds(context, manager, TodayWidgetProvider.class), R.id.widget_list);
manager.notifyAppWidgetViewDataChanged(
widgetIds(context, manager, WeekWidgetProvider.class), R.id.widget_list);
manager.notifyAppWidgetViewDataChanged(
widgetIds(context, manager, PeopleWidgetProvider.class), R.id.widget_list);
}
/** Redraw title/count/subtitle only — safe to call from the list factory. */
public static void updateHeaders(Context context) {
AppWidgetManager manager = AppWidgetManager.getInstance(context);
for (int id : widgetIds(context, manager, TodayWidgetProvider.class)) {
manager.updateAppWidget(id, build(context, MODE_TODAY, id));
}
for (int id : widgetIds(context, manager, WeekWidgetProvider.class)) {
manager.updateAppWidget(id, build(context, MODE_WEEK, id));
}
for (int id : widgetIds(context, manager, PeopleWidgetProvider.class)) {
manager.updateAppWidget(id, build(context, MODE_PEOPLE, id));
}
}
private static int[] widgetIds(Context context, AppWidgetManager manager, Class<?> provider) {
return manager.getAppWidgetIds(new ComponentName(context, provider));
}
public static RemoteViews build(Context context, String mode, int appWidgetId) {
boolean today = MODE_TODAY.equals(mode);
boolean people = MODE_PEOPLE.equals(mode);
RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.widget_shell);
int titleRes = today ? R.string.widget_today_title
: people ? R.string.widget_people_title
: R.string.widget_week_title;
views.setTextViewText(R.id.widget_title, context.getString(titleRes));
if (people) {
views.setViewVisibility(R.id.widget_count, View.GONE);
} else {
boolean includeOthers = WidgetStore.includeOthers(context, appWidgetId);
List<WidgetStore.Task> tasks = WidgetStore.visibleTasks(
context, WidgetStore.loadTasks(context), includeOthers);
int count = (today ? WidgetStore.todaySubset(tasks) : tasks).size();
views.setTextViewText(R.id.widget_count, String.valueOf(count));
views.setViewVisibility(R.id.widget_count, count > 0 ? View.VISIBLE : View.GONE);
}
views.setTextViewText(R.id.widget_subtitle, subtitle(context));
if (!WidgetStore.hasConfig(context)) {
views.setTextViewText(R.id.widget_empty,
context.getString(R.string.widget_signed_out));
} else {
int emptyRes = today ? R.string.widget_empty_today
: people ? R.string.widget_empty_people
: R.string.widget_empty_week;
views.setTextViewText(R.id.widget_empty, context.getString(emptyRes));
}
// The Today widget gets a quick-add button that deep links straight
// into the in-app AddTaskModal.
if (today) {
Intent addTask = new Intent(context, MainActivity.class);
addTask.setAction(Intent.ACTION_VIEW);
addTask.setData(Uri.parse("donetick://chores/add"));
views.setOnClickPendingIntent(R.id.widget_add, PendingIntent.getActivity(
context, 2, addTask,
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE));
views.setViewVisibility(R.id.widget_add, View.VISIBLE);
} else {
views.setViewVisibility(R.id.widget_add, View.GONE);
}
// List content comes from WidgetListService; mode travels in the intent
// and the unique data URI keeps the adapters from being collapsed.
Intent adapterIntent = new Intent(context, WidgetListService.class);
adapterIntent.putExtra(EXTRA_MODE, mode);
adapterIntent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId);
adapterIntent.setData(Uri.parse("donetickwidget://" + mode + "/" + appWidgetId));
views.setRemoteAdapter(R.id.widget_list, adapterIntent);
views.setEmptyView(R.id.widget_list, R.id.widget_empty);
// Row taps deep link into the chore view; the row's fill-in intent
// supplies the donetick://chores/<id> data URI.
Intent rowTemplate = new Intent(context, MainActivity.class);
rowTemplate.setAction(Intent.ACTION_VIEW);
views.setPendingIntentTemplate(R.id.widget_list, PendingIntent.getActivity(
context, 1, rowTemplate,
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_MUTABLE));
// Anywhere else on the widget simply opens the app.
Intent openApp = new Intent(context, MainActivity.class);
views.setOnClickPendingIntent(R.id.widget_container, PendingIntent.getActivity(
context, 0, openApp,
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE));
return views;
}
private static String subtitle(Context context) {
String date = new SimpleDateFormat("EEE, MMM d", Locale.getDefault())
.format(new Date());
long lastUpdated = WidgetStore.lastUpdated(context);
if (lastUpdated <= 0) return date;
String time = android.text.format.DateFormat.getTimeFormat(context)
.format(new Date(lastUpdated));
return date + " · " + context.getString(R.string.widget_updated_at, time);
}
}

View File

@@ -0,0 +1 @@
{ "version": 1, "sessions": {} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
<solid android:color="@color/widget_bg" />
<!-- Match the launcher's system-wide widget corner radius on Android 12+ -->
<corners android:radius="@android:dimen/system_app_widget_background_radius" />
</shape>

View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="@color/widget_accent">
<path
android:fillColor="@android:color/white"
android:pathData="M19,13H13V19H11V13H5V11H11V5H13V11H19V13Z" />
</vector>

View File

@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="@color/widget_accent">
<path
android:fillColor="@android:color/white"
android:pathData="M12,14c1.66,0 3,-1.34 3,-3V5c0,-1.66 -1.34,-3 -3,-3S9,3.34 9,5v6C9,12.66 10.34,14 12,14z" />
<path
android:fillColor="@android:color/white"
android:pathData="M17,11c0,2.76 -2.24,5 -5,5s-5,-2.24 -5,-5H5c0,3.53 2.61,6.43 6,6.92V21h2v-3.08c3.39,-0.49 6,-3.39 6,-6.92H17z" />
</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="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="@color/widget_accent">
<!-- Framing corners plus a scan line: reads as "scan" at widget size. -->
<path
android:fillColor="@android:color/white"
android:pathData="M4,4h5v2H6v3H4V4zM15,4h5v5h-2V6h-3V4zM4,15h2v3h3v2H4V15zM18,15h2v5h-5v-2h3V15z" />
<path
android:fillColor="@android:color/white"
android:pathData="M3,11h18v2H3V11z" />
</vector>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
<solid android:color="@color/widget_bg" />
<corners android:radius="24dp" />
</shape>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
<solid android:color="@color/widget_accent_soft" />
<corners android:radius="10dp" />
</shape>

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Initials disc stand-in for the picker previews; placed widgets get the
real cached avatars from AvatarCache. -->
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="oval">
<solid android:color="@color/widget_accent_soft" />
<size android:width="28dp" android:height="28dp" />
</shape>

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="oval">
<solid android:color="@android:color/transparent" />
<stroke android:width="2dp" android:color="#FFFFFF" />
<size android:width="18dp" android:height="18dp" />
</shape>

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Tappable tile on the Quick Capture widget. -->
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
<solid android:color="@color/widget_accent_soft" />
<corners android:radius="18dp" />
</shape>

View File

@@ -0,0 +1,43 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/widget_bg"
android:orientation="vertical"
android:padding="24dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/widget_config_title"
android:textColor="@color/widget_text_primary"
android:textSize="22sp"
android:textStyle="bold" />
<Switch
android:id="@+id/config_include_others"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:minHeight="48dp"
android:text="@string/widget_config_include_others"
android:textColor="@color/widget_text_primary"
android:textSize="16sp" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:text="@string/widget_config_include_others_hint"
android:textColor="@color/widget_text_secondary"
android:textSize="13sp" />
<Button
android:id="@+id/config_save"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="32dp"
android:backgroundTint="@color/widget_accent"
android:text="@string/widget_config_save"
android:textColor="#FFFFFF" />
</LinearLayout>

View File

@@ -0,0 +1,182 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Widget-picker preview for the People widget. See widget_preview_today.xml
for why the real shell can't be previewed directly. Avatars are initials
discs here; placed widgets get the real photos from AvatarCache.
Keep in visual sync with widget_shell.xml + widget_row_person.xml. -->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/widget_background"
android:orientation="vertical"
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:paddingTop="14dp"
android:paddingBottom="10dp">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_centerVertical="true"
android:layout_toStartOf="@+id/preview_count"
android:ellipsize="end"
android:maxLines="1"
android:text="@string/widget_people_title"
android:textColor="@color/widget_text_primary"
android:textSize="15sp"
android:textStyle="bold" />
<TextView
android:id="@+id/preview_count"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_centerVertical="true"
android:background="@drawable/widget_count_bg"
android:gravity="center"
android:minWidth="22dp"
android:paddingStart="7dp"
android:paddingEnd="7dp"
android:paddingTop="2dp"
android:paddingBottom="2dp"
android:text="@string/widget_preview_count_people"
android:textColor="@color/widget_accent"
android:textSize="12sp"
android:textStyle="bold" />
</RelativeLayout>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="1dp"
android:ellipsize="end"
android:maxLines="1"
android:text="@string/widget_preview_updated"
android:textColor="@color/widget_text_secondary"
android:textSize="11sp" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="6dp"
android:gravity="center_vertical"
android:minHeight="44dp"
android:orientation="horizontal"
android:paddingTop="5dp"
android:paddingBottom="5dp">
<TextView
android:layout_width="28dp"
android:layout_height="28dp"
android:background="@drawable/widget_preview_avatar"
android:gravity="center"
android:text="@string/widget_preview_initials_1"
android:textColor="@color/widget_accent"
android:textSize="12sp"
android:textStyle="bold" />
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:layout_marginEnd="8dp"
android:layout_weight="1"
android:ellipsize="end"
android:maxLines="1"
android:text="@string/widget_preview_person_1"
android:textColor="@color/widget_text_primary"
android:textSize="14sp"
android:textStyle="bold" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/widget_preview_counts_1"
android:textColor="@color/widget_text_secondary"
android:textSize="12sp" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:minHeight="44dp"
android:orientation="horizontal"
android:paddingTop="5dp"
android:paddingBottom="5dp">
<TextView
android:layout_width="28dp"
android:layout_height="28dp"
android:background="@drawable/widget_preview_avatar"
android:gravity="center"
android:text="@string/widget_preview_initials_2"
android:textColor="@color/widget_accent"
android:textSize="12sp"
android:textStyle="bold" />
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:layout_marginEnd="8dp"
android:layout_weight="1"
android:ellipsize="end"
android:maxLines="1"
android:text="@string/widget_preview_person_2"
android:textColor="@color/widget_text_primary"
android:textSize="14sp"
android:textStyle="bold" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/widget_preview_counts_2"
android:textColor="@color/widget_text_secondary"
android:textSize="12sp" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:minHeight="44dp"
android:orientation="horizontal"
android:paddingTop="5dp"
android:paddingBottom="5dp">
<TextView
android:layout_width="28dp"
android:layout_height="28dp"
android:background="@drawable/widget_preview_avatar"
android:gravity="center"
android:text="@string/widget_preview_initials_3"
android:textColor="@color/widget_accent"
android:textSize="12sp"
android:textStyle="bold" />
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:layout_marginEnd="8dp"
android:layout_weight="1"
android:ellipsize="end"
android:maxLines="1"
android:text="@string/widget_preview_person_3"
android:textColor="@color/widget_text_primary"
android:textSize="14sp"
android:textStyle="bold" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/widget_preview_counts_3"
android:textColor="@color/widget_text_secondary"
android:textSize="12sp" />
</LinearLayout>
</LinearLayout>

View File

@@ -0,0 +1,182 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Widget-picker preview for the Today widget. Mirrors widget_shell.xml, but
with literal sample rows: the real shell fills its ListView from a
RemoteViewsFactory, which the picker never binds, so it would render blank.
Keep in visual sync with widget_shell.xml + widget_row_task.xml. -->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/widget_background"
android:orientation="vertical"
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:paddingTop="14dp"
android:paddingBottom="10dp">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_centerVertical="true"
android:layout_toStartOf="@+id/preview_count"
android:ellipsize="end"
android:maxLines="1"
android:text="@string/widget_today_title"
android:textColor="@color/widget_text_primary"
android:textSize="15sp"
android:textStyle="bold" />
<TextView
android:id="@+id/preview_count"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:layout_toStartOf="@+id/preview_add"
android:background="@drawable/widget_count_bg"
android:gravity="center"
android:minWidth="22dp"
android:paddingStart="7dp"
android:paddingEnd="7dp"
android:paddingTop="2dp"
android:paddingBottom="2dp"
android:text="@string/widget_preview_count_today"
android:textColor="@color/widget_accent"
android:textSize="12sp"
android:textStyle="bold" />
<ImageView
android:id="@+id/preview_add"
android:layout_width="26dp"
android:layout_height="26dp"
android:layout_alignParentEnd="true"
android:layout_centerVertical="true"
android:layout_marginStart="6dp"
android:background="@drawable/widget_count_bg"
android:contentDescription="@string/widget_add_task"
android:padding="5dp"
android:src="@drawable/ic_widget_add" />
</RelativeLayout>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="1dp"
android:ellipsize="end"
android:maxLines="1"
android:text="@string/widget_preview_updated"
android:textColor="@color/widget_text_secondary"
android:textSize="11sp" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="6dp"
android:gravity="center_vertical"
android:minHeight="40dp"
android:orientation="horizontal"
android:paddingTop="4dp"
android:paddingBottom="4dp">
<ImageView
android:layout_width="18dp"
android:layout_height="18dp"
android:importantForAccessibility="no"
android:src="@drawable/widget_ring"
android:tint="@color/widget_overdue" />
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:layout_marginEnd="8dp"
android:layout_weight="1"
android:ellipsize="end"
android:maxLines="1"
android:text="@string/widget_preview_task_1"
android:textColor="@color/widget_text_primary"
android:textSize="14sp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/widget_meta_overdue"
android:textColor="@color/widget_overdue"
android:textSize="12sp" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:minHeight="40dp"
android:orientation="horizontal"
android:paddingTop="4dp"
android:paddingBottom="4dp">
<ImageView
android:layout_width="18dp"
android:layout_height="18dp"
android:importantForAccessibility="no"
android:src="@drawable/widget_ring"
android:tint="@color/widget_ring_neutral" />
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:layout_marginEnd="8dp"
android:layout_weight="1"
android:ellipsize="end"
android:maxLines="1"
android:text="@string/widget_preview_task_2"
android:textColor="@color/widget_text_primary"
android:textSize="14sp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/widget_preview_meta_1"
android:textColor="@color/widget_text_secondary"
android:textSize="12sp" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:minHeight="40dp"
android:orientation="horizontal"
android:paddingTop="4dp"
android:paddingBottom="4dp">
<ImageView
android:layout_width="18dp"
android:layout_height="18dp"
android:importantForAccessibility="no"
android:src="@drawable/widget_ring"
android:tint="@color/widget_ring_neutral" />
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:layout_marginEnd="8dp"
android:layout_weight="1"
android:ellipsize="end"
android:maxLines="1"
android:text="@string/widget_preview_task_3"
android:textColor="@color/widget_text_primary"
android:textSize="14sp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/widget_preview_meta_2"
android:textColor="@color/widget_text_secondary"
android:textSize="12sp" />
</LinearLayout>
</LinearLayout>

View File

@@ -0,0 +1,193 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Widget-picker preview for the Next 7 Days widget. See widget_preview_today.xml
for why the real shell can't be previewed directly. Keep in visual sync with
widget_shell.xml + widget_row_day_header.xml + widget_row_task.xml. -->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/widget_background"
android:orientation="vertical"
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:paddingTop="14dp"
android:paddingBottom="10dp">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_centerVertical="true"
android:layout_toStartOf="@+id/preview_count"
android:ellipsize="end"
android:maxLines="1"
android:text="@string/widget_week_title"
android:textColor="@color/widget_text_primary"
android:textSize="15sp"
android:textStyle="bold" />
<TextView
android:id="@+id/preview_count"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_centerVertical="true"
android:background="@drawable/widget_count_bg"
android:gravity="center"
android:minWidth="22dp"
android:paddingStart="7dp"
android:paddingEnd="7dp"
android:paddingTop="2dp"
android:paddingBottom="2dp"
android:text="@string/widget_preview_count_week"
android:textColor="@color/widget_accent"
android:textSize="12sp"
android:textStyle="bold" />
</RelativeLayout>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="1dp"
android:ellipsize="end"
android:maxLines="1"
android:text="@string/widget_preview_updated"
android:textColor="@color/widget_text_secondary"
android:textSize="11sp" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="6dp"
android:letterSpacing="0.08"
android:paddingTop="8dp"
android:paddingBottom="2dp"
android:text="@string/widget_group_today"
android:textAllCaps="true"
android:textColor="@color/widget_text_secondary"
android:textSize="10sp"
android:textStyle="bold" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:minHeight="40dp"
android:orientation="horizontal"
android:paddingTop="4dp"
android:paddingBottom="4dp">
<ImageView
android:layout_width="18dp"
android:layout_height="18dp"
android:importantForAccessibility="no"
android:src="@drawable/widget_ring"
android:tint="@color/widget_ring_neutral" />
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:layout_marginEnd="8dp"
android:layout_weight="1"
android:ellipsize="end"
android:maxLines="1"
android:text="@string/widget_preview_task_1"
android:textColor="@color/widget_text_primary"
android:textSize="14sp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/widget_preview_meta_1"
android:textColor="@color/widget_text_secondary"
android:textSize="12sp" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:minHeight="40dp"
android:orientation="horizontal"
android:paddingTop="4dp"
android:paddingBottom="4dp">
<ImageView
android:layout_width="18dp"
android:layout_height="18dp"
android:importantForAccessibility="no"
android:src="@drawable/widget_ring"
android:tint="@color/widget_ring_neutral" />
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:layout_marginEnd="8dp"
android:layout_weight="1"
android:ellipsize="end"
android:maxLines="1"
android:text="@string/widget_preview_task_2"
android:textColor="@color/widget_text_primary"
android:textSize="14sp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/widget_preview_meta_2"
android:textColor="@color/widget_text_secondary"
android:textSize="12sp" />
</LinearLayout>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:letterSpacing="0.08"
android:paddingTop="8dp"
android:paddingBottom="2dp"
android:text="@string/widget_group_tomorrow"
android:textAllCaps="true"
android:textColor="@color/widget_text_secondary"
android:textSize="10sp"
android:textStyle="bold" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:minHeight="40dp"
android:orientation="horizontal"
android:paddingTop="4dp"
android:paddingBottom="4dp">
<ImageView
android:layout_width="18dp"
android:layout_height="18dp"
android:importantForAccessibility="no"
android:src="@drawable/widget_ring"
android:tint="@color/widget_ring_neutral" />
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:layout_marginEnd="8dp"
android:layout_weight="1"
android:ellipsize="end"
android:maxLines="1"
android:text="@string/widget_preview_task_3"
android:textColor="@color/widget_text_primary"
android:textSize="14sp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/widget_preview_meta_1"
android:textColor="@color/widget_text_secondary"
android:textSize="12sp" />
</LinearLayout>
</LinearLayout>

View File

@@ -0,0 +1,110 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Quick Capture widget: three one-tap ways into the add-task flow. -->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/quick_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/widget_background"
android:orientation="vertical"
android:paddingStart="12dp"
android:paddingEnd="12dp"
android:paddingTop="12dp"
android:paddingBottom="12dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:baselineAligned="false"
android:orientation="horizontal">
<LinearLayout
android:id="@+id/quick_type"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:background="@drawable/widget_tile_bg"
android:contentDescription="@string/widget_quick_type"
android:gravity="center"
android:orientation="vertical"
android:paddingTop="8dp"
android:paddingBottom="8dp">
<ImageView
android:layout_width="24dp"
android:layout_height="24dp"
android:contentDescription="@string/widget_quick_type"
android:src="@drawable/ic_widget_add" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="5dp"
android:maxLines="1"
android:text="@string/widget_quick_type"
android:textColor="@color/widget_accent"
android:textSize="11sp"
android:textStyle="bold" />
</LinearLayout>
<LinearLayout
android:id="@+id/quick_scan"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_marginStart="8dp"
android:layout_weight="1"
android:background="@drawable/widget_tile_bg"
android:contentDescription="@string/widget_quick_scan"
android:gravity="center"
android:orientation="vertical"
android:paddingTop="8dp"
android:paddingBottom="8dp">
<ImageView
android:layout_width="24dp"
android:layout_height="24dp"
android:contentDescription="@string/widget_quick_scan"
android:src="@drawable/ic_widget_scan" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="5dp"
android:maxLines="1"
android:text="@string/widget_quick_scan"
android:textColor="@color/widget_accent"
android:textSize="11sp"
android:textStyle="bold" />
</LinearLayout>
<LinearLayout
android:id="@+id/quick_voice"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_marginStart="8dp"
android:layout_weight="1"
android:background="@drawable/widget_tile_bg"
android:contentDescription="@string/widget_quick_voice"
android:gravity="center"
android:orientation="vertical"
android:paddingTop="8dp"
android:paddingBottom="8dp">
<ImageView
android:layout_width="24dp"
android:layout_height="24dp"
android:contentDescription="@string/widget_quick_voice"
android:src="@drawable/ic_widget_mic" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="5dp"
android:maxLines="1"
android:text="@string/widget_quick_voice"
android:textColor="@color/widget_accent"
android:textSize="11sp"
android:textStyle="bold" />
</LinearLayout>
</LinearLayout>
</LinearLayout>

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/row_day"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:letterSpacing="0.08"
android:paddingTop="8dp"
android:paddingBottom="2dp"
android:textAllCaps="true"
android:textColor="@color/widget_text_secondary"
android:textSize="10sp"
android:textStyle="bold" />

View File

@@ -0,0 +1,37 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/person_root"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:minHeight="44dp"
android:orientation="horizontal"
android:paddingTop="5dp"
android:paddingBottom="5dp">
<ImageView
android:id="@+id/person_avatar"
android:layout_width="28dp"
android:layout_height="28dp"
android:importantForAccessibility="no" />
<TextView
android:id="@+id/person_name"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:layout_marginEnd="8dp"
android:layout_weight="1"
android:ellipsize="end"
android:maxLines="1"
android:textColor="@color/widget_text_primary"
android:textSize="14sp"
android:textStyle="bold" />
<TextView
android:id="@+id/person_counts"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/widget_text_secondary"
android:textSize="12sp" />
</LinearLayout>

View File

@@ -0,0 +1,46 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/row_root"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:minHeight="40dp"
android:orientation="horizontal"
android:paddingTop="4dp"
android:paddingBottom="4dp">
<ImageView
android:id="@+id/row_ring"
android:layout_width="18dp"
android:layout_height="18dp"
android:importantForAccessibility="no"
android:src="@drawable/widget_ring" />
<TextView
android:id="@+id/row_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:layout_marginEnd="8dp"
android:layout_weight="1"
android:ellipsize="end"
android:maxLines="1"
android:textColor="@color/widget_text_primary"
android:textSize="14sp" />
<TextView
android:id="@+id/row_meta"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/widget_text_secondary"
android:textSize="12sp" />
<!-- Assignee avatar, shown only in "everyone" mode for others' tasks. -->
<ImageView
android:id="@+id/row_avatar"
android:layout_width="20dp"
android:layout_height="20dp"
android:layout_marginStart="8dp"
android:importantForAccessibility="no"
android:visibility="gone" />
</LinearLayout>

View File

@@ -0,0 +1,93 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/widget_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/widget_background"
android:orientation="vertical"
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:paddingTop="14dp"
android:paddingBottom="10dp">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/widget_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_centerVertical="true"
android:layout_toStartOf="@id/widget_count"
android:ellipsize="end"
android:maxLines="1"
android:textColor="@color/widget_text_primary"
android:textSize="15sp"
android:textStyle="bold" />
<TextView
android:id="@+id/widget_count"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_toStartOf="@+id/widget_add"
android:layout_centerVertical="true"
android:background="@drawable/widget_count_bg"
android:minWidth="22dp"
android:gravity="center"
android:paddingStart="7dp"
android:paddingEnd="7dp"
android:paddingTop="2dp"
android:paddingBottom="2dp"
android:textColor="@color/widget_accent"
android:textSize="12sp"
android:textStyle="bold" />
<!-- Quick-add, shown on the Today widget only (deep links to the
in-app AddTaskModal). Gone by default so the other widgets keep
the count pill flush with the edge. -->
<ImageView
android:id="@+id/widget_add"
android:layout_width="26dp"
android:layout_height="26dp"
android:layout_alignParentEnd="true"
android:layout_centerVertical="true"
android:layout_marginStart="6dp"
android:background="@drawable/widget_count_bg"
android:contentDescription="@string/widget_add_task"
android:padding="5dp"
android:src="@drawable/ic_widget_add"
android:visibility="gone" />
</RelativeLayout>
<TextView
android:id="@+id/widget_subtitle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="1dp"
android:ellipsize="end"
android:maxLines="1"
android:textColor="@color/widget_text_secondary"
android:textSize="11sp" />
<ListView
android:id="@+id/widget_list"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="6dp"
android:divider="@null"
android:dividerHeight="0dp"
android:listSelector="@android:color/transparent"
android:scrollbars="none" />
<TextView
android:id="@+id/widget_empty"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:lineSpacingExtra="3dp"
android:textColor="@color/widget_text_secondary"
android:textSize="13sp"
android:visibility="gone" />
</LinearLayout>

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="widget_bg">#181C20</color>
<color name="widget_text_primary">#F0F4F8</color>
<color name="widget_text_secondary">#9FA6AD</color>
<color name="widget_accent">#4B9BE8</color>
<color name="widget_accent_soft">#12395F</color>
<color name="widget_overdue">#F09898</color>
<color name="widget_warning">#F3C896</color>
<color name="widget_ring_neutral">#555E68</color>
<color name="widget_divider">#252B31</color>
</resources>

View File

@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Home-screen widget palette (values-night overrides for dark mode) -->
<color name="widget_bg">#FFFFFF</color>
<color name="widget_text_primary">#171A1C</color>
<color name="widget_text_secondary">#6B7681</color>
<color name="widget_accent">#0B6BCB</color>
<color name="widget_accent_soft">#E3EFFB</color>
<color name="widget_overdue">#C41C1C</color>
<color name="widget_warning">#B26A00</color>
<color name="widget_ring_neutral">#B8C0C9</color>
<color name="widget_divider">#EDF1F5</color>
</resources>

View File

@@ -4,4 +4,58 @@
<string name="title_activity_main">Donetick</string>
<string name="package_name">com.donetick.app</string>
<string name="custom_url_scheme">com.donetick.app</string>
<!-- Home-screen widgets -->
<string name="widget_today_label">Today</string>
<string name="widget_week_label">Next 7 Days</string>
<string name="widget_today_description">Your tasks due today, plus anything waiting on you.</string>
<string name="widget_week_description">Your tasks for the next 7 days, grouped by day.</string>
<string name="widget_today_title">Today</string>
<string name="widget_week_title">Next 7 days</string>
<string name="widget_empty_today">All caught up!\nNothing due today</string>
<string name="widget_empty_week">All caught up!\nNothing due this week</string>
<string name="widget_signed_out">Open Donetick and sign in\nto see your tasks</string>
<string name="widget_meta_overdue">Overdue</string>
<string name="widget_meta_approval">Approve</string>
<string name="widget_group_approval">Needs approval</string>
<string name="widget_group_overdue">Overdue</string>
<string name="widget_group_today">Today</string>
<string name="widget_group_tomorrow">Tomorrow</string>
<string name="widget_updated_at">Updated %s</string>
<string name="widget_add_task">Add task</string>
<string name="widget_people_label">People</string>
<string name="widget_people_description">Everyone in your circle with their tasks for today and the week ahead.</string>
<string name="widget_people_title">People</string>
<string name="widget_empty_people">No circle members yet</string>
<string name="widget_person_counts">%1$d today · %2$d this week</string>
<string name="widget_quick_label">Quick Capture</string>
<string name="widget_quick_description">Capture a task in one tap — type it, scan it, or say it.</string>
<string name="widget_quick_type">Type</string>
<string name="widget_quick_scan">Scan</string>
<string name="widget_quick_voice">Speak</string>
<string name="widget_config_title">Widget options</string>
<string name="widget_config_include_others">Show everyone\'s tasks</string>
<string name="widget_config_include_others_hint">Include tasks assigned to other members of your circle. Their avatar appears next to their tasks.</string>
<string name="widget_config_save">Save</string>
<!-- Sample data for the widget-picker previews (never shown on a placed
widget). Kept generic so it reads well in any locale. -->
<string name="widget_preview_updated">Updated just now</string>
<string name="widget_preview_task_1">Take out the bins</string>
<string name="widget_preview_task_2">Water the plants</string>
<string name="widget_preview_task_3">Book the vet</string>
<string name="widget_preview_meta_1">6:00 PM</string>
<string name="widget_preview_meta_2">8:30 PM</string>
<string name="widget_preview_count_today">3</string>
<string name="widget_preview_count_week">7</string>
<string name="widget_preview_count_people">4</string>
<string name="widget_preview_person_1">Alex</string>
<string name="widget_preview_person_2">Sam</string>
<string name="widget_preview_person_3">Jordan</string>
<string name="widget_preview_initials_1">A</string>
<string name="widget_preview_initials_2">S</string>
<string name="widget_preview_initials_3">J</string>
<string name="widget_preview_counts_1">2 today · 5 this week</string>
<string name="widget_preview_counts_2">1 today · 3 this week</string>
<string name="widget_preview_counts_3">0 today · 2 this week</string>
</resources>

View File

@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<!--
Trust both the system CA store and user-installed CA certificates.
This lets self-hosted Donetick instances that use custom or self-signed
certificates work once the user installs their CA on the device, without
having to modify/repackage the APK. See donetick/donetick#727.
-->
<base-config cleartextTrafficPermitted="true">
<trust-anchors>
<certificates src="system" />
<certificates src="user" />
</trust-anchors>
</base-config>
</network-security-config>

View File

@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
android:description="@string/widget_people_description"
android:initialLayout="@layout/widget_shell"
android:previewLayout="@layout/widget_preview_people"
android:previewImage="@drawable/widget_preview_people"
android:minWidth="180dp"
android:minHeight="110dp"
android:minResizeWidth="110dp"
android:minResizeHeight="110dp"
android:resizeMode="horizontal|vertical"
android:targetCellWidth="3"
android:targetCellHeight="2"
android:updatePeriodMillis="1800000"
android:widgetCategory="home_screen" />

View File

@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- The layout is fully static (no bound data), so it doubles as its own
API 31+ preview. -->
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
android:description="@string/widget_quick_description"
android:initialLayout="@layout/widget_quick_capture"
android:previewLayout="@layout/widget_quick_capture"
android:previewImage="@drawable/widget_preview_quick"
android:minWidth="180dp"
android:minHeight="70dp"
android:minResizeWidth="180dp"
android:minResizeHeight="60dp"
android:resizeMode="horizontal|vertical"
android:targetCellWidth="4"
android:targetCellHeight="1"
android:updatePeriodMillis="0"
android:widgetCategory="home_screen" />

View File

@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
android:description="@string/widget_today_description"
android:initialLayout="@layout/widget_shell"
android:previewLayout="@layout/widget_preview_today"
android:previewImage="@drawable/widget_preview_today"
android:minWidth="180dp"
android:minHeight="110dp"
android:minResizeWidth="110dp"
android:minResizeHeight="110dp"
android:resizeMode="horizontal|vertical"
android:targetCellWidth="3"
android:targetCellHeight="2"
android:updatePeriodMillis="1800000"
android:configure="com.donetick.app.widget.WidgetConfigActivity"
android:widgetFeatures="reconfigurable|configuration_optional"
android:widgetCategory="home_screen" />

View File

@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
android:description="@string/widget_week_description"
android:initialLayout="@layout/widget_shell"
android:previewLayout="@layout/widget_preview_week"
android:previewImage="@drawable/widget_preview_week"
android:minWidth="250dp"
android:minHeight="180dp"
android:minResizeWidth="180dp"
android:minResizeHeight="110dp"
android:resizeMode="horizontal|vertical"
android:targetCellWidth="4"
android:targetCellHeight="3"
android:updatePeriodMillis="1800000"
android:configure="com.donetick.app.widget.WidgetConfigActivity"
android:widgetFeatures="reconfigurable|configuration_optional"
android:widgetCategory="home_screen" />

View File

@@ -22,6 +22,12 @@ allprojects {
google()
mavenCentral()
}
configurations.configureEach {
// The app does not use Advertising ID. Exclude RevenueCat's transitive
// ads identifier dependency so Play Console does not require an AD_ID declaration.
exclude group: 'com.google.android.gms', module: 'play-services-ads-identifier'
}
}
task clean(type: Delete) {

View File

@@ -2,6 +2,12 @@
include ':capacitor-android'
project(':capacitor-android').projectDir = new File('../node_modules/@capacitor/android/capacitor')
include ':capacitor-community-in-app-review'
project(':capacitor-community-in-app-review').projectDir = new File('../node_modules/@capacitor-community/in-app-review/android')
include ':capacitor-community-speech-recognition'
project(':capacitor-community-speech-recognition').projectDir = new File('../node_modules/@capacitor-community/speech-recognition/android')
include ':capacitor-community-sqlite'
project(':capacitor-community-sqlite').projectDir = new File('../node_modules/@capacitor-community/sqlite/android')
@@ -14,6 +20,9 @@ project(':capacitor-browser').projectDir = new File('../node_modules/@capacitor/
include ':capacitor-device'
project(':capacitor-device').projectDir = new File('../node_modules/@capacitor/device/android')
include ':capacitor-haptics'
project(':capacitor-haptics').projectDir = new File('../node_modules/@capacitor/haptics/android')
include ':capacitor-local-llm'
project(':capacitor-local-llm').projectDir = new File('../node_modules/@capacitor/local-llm/android')
@@ -29,6 +38,9 @@ project(':capacitor-preferences').projectDir = new File('../node_modules/@capaci
include ':capacitor-push-notifications'
project(':capacitor-push-notifications').projectDir = new File('../node_modules/@capacitor/push-notifications/android')
include ':capacitor-share'
project(':capacitor-share').projectDir = new File('../node_modules/@capacitor/share/android')
include ':capacitor-status-bar'
project(':capacitor-status-bar').projectDir = new File('../node_modules/@capacitor/status-bar/android')

View File

@@ -16,6 +16,22 @@ const config: CapacitorConfig = {
iconColor: '#488AFF',
sound: 'beep.wav',
},
CapacitorSQLite: {
// The offline cache opens databases with encrypted: false / no-encryption.
// @capacitor-community/sqlite defaults native encryption to true when this
// block is absent; on Android that can make plugin load fail with
// "CapacitorSQLitePlugin: null" before JS gets a chance to open the DB.
iosIsEncryption: false,
androidIsEncryption: false,
},
SocialLogin: {
providers: {
google: true,
facebook: false,
apple: true,
twitter: false,
},
},
// GoogleAuth: {
// scopes: ['profile', 'email', 'openid'],
// clientId: process.env.VITE_APP_GOOGLE_CLIENT_ID,

View File

@@ -1,7 +1,7 @@
# Crowdin configuration for Donetick
# See: https://support.crowdin.com/configuration-file/
project_id: "donetick"
project_id: 'donetick'
api_token_env: CROWDIN_API_TOKEN
preserve_hierarchy: true
@@ -13,3 +13,4 @@ files:
- source: /public/locales/en/*.json
translation: /public/locales/%two_letters_code%/%original_file_name%
update_option: update_as_unapproved
skip_untranslated_strings: true

5
e2e/.gitignore vendored Normal file
View File

@@ -0,0 +1,5 @@
node_modules/
.auth/
playwright-report/
test-results/
.e2e-run/

21
e2e/debug.mjs Normal file
View File

@@ -0,0 +1,21 @@
import { chromium } from '@playwright/test'
import { readFile } from 'fs/promises'
const state = JSON.parse(await readFile('.auth/state.json', 'utf8'))
const browser = await chromium.launch()
const context = await browser.newContext({ storageState: state })
const page = await context.newPage()
page.on('console', msg => console.log('[console]', msg.type(), msg.text()))
page.on('pageerror', err =>
console.log('[pageerror]', err.message, '\n', err.stack),
)
await page.goto('http://localhost:5173/chores/create')
await page.getByTestId('chore-name-input').fill('Debug Chore ' + Date.now())
await page.getByLabel('Repeat this task').click()
await page.getByLabel('Daily').click()
await page.getByRole('button', { name: 'Create' }).click()
await page.waitForURL('**/chores', { timeout: 15000 })
await page.waitForTimeout(2000)
await browser.close()

59
e2e/fixtures/auth.js Normal file
View File

@@ -0,0 +1,59 @@
import { expect, test as base } from '@playwright/test'
import path from 'path'
import { fileURLToPath } from 'url'
const __dirname = path.dirname(fileURLToPath(import.meta.url))
/**
* Fill and submit the signup form through the UI.
* After successful signup the app auto-logs in and walks through the
* onboarding flow (/circle-setup, then /ready) before landing on /chores.
*/
export async function signUpViaUI(
page,
{ displayName, email, password, username },
) {
await page.goto('/signup')
await page.locator('#username').fill(username)
await page.locator('#email').fill(email)
await page.locator('#password').fill(password)
await page.locator('#displayName').fill(displayName)
await page.getByRole('button', { name: 'Create account' }).click()
await page.waitForURL('**/circle-setup', { timeout: 10_000 })
await page.getByRole('button', { name: 'Continue' }).click()
await page.waitForURL('**/ready', { timeout: 10_000 })
await page.getByRole('button', { name: 'Continue' }).click()
await page.waitForURL('**/chores', { timeout: 10_000 })
}
/**
* Fill and submit the login form through the UI.
* After successful login the app redirects to /chores.
*/
export async function loginViaUI(page, { password, username }) {
await page.goto('/login')
await page.locator('#username').fill(username)
await page.locator('#password').fill(password)
await page.getByRole('button', { name: 'Sign In' }).click()
await page.waitForURL('**/chores', { timeout: 10_000 })
}
/**
* A Playwright test fixture that provides a page already authenticated as the
* shared E2E user (via persisted storage state, no UI interaction required).
*/
export const test = base.extend({
authenticatedPage: async ({ browser }, callback) => {
const ctx = await browser.newContext({
storageState: path.join(__dirname, '..', '.auth', 'state.json'),
})
const page = await ctx.newPage()
await callback(page)
await ctx.close()
},
})
export { expect }

100
e2e/global-setup.js Normal file
View File

@@ -0,0 +1,100 @@
import { mkdir, writeFile } from 'fs/promises'
import path from 'path'
import { fileURLToPath } from 'url'
const __dirname = path.dirname(fileURLToPath(import.meta.url))
// FRONTEND_URL: what the browser navigates to (Playwright baseURL)
// API_URL: where the Go API lives (may be the same in production builds)
export const FRONTEND_URL =
process.env.E2E_FRONTEND_URL ||
process.env.E2E_BASE_URL ||
'http://localhost:5173'
export const API_URL =
process.env.E2E_API_URL || process.env.E2E_BASE_URL || 'http://localhost:2021'
export const TEST_USER = {
username: 'e2e.user',
email: 'e2e@donetick.test',
password: 'E2ePassword123!',
displayName: 'E2E User',
}
/**
* Global setup: create the shared E2E test user (idempotent) and persist
* the JWT token as a Playwright storage state so authenticated tests can
* skip the login UI.
*/
export default async function globalSetup() {
await waitForServer(API_URL)
// Create user ignore 409/conflict so re-runs are safe
const signupRes = await fetch(`${API_URL}/api/v1/auth/`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(TEST_USER),
})
if (!signupRes.ok) {
const body = await signupRes.text()
// Treat "already exists" errors as success so re-runs are idempotent
if (!body.includes('already exists') && !body.includes('already taken')) {
throw new Error(`Signup failed (${signupRes.status}): ${body}`)
}
}
// Login and grab the access token
const loginRes = await fetch(`${API_URL}/api/v1/auth/login`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
username: TEST_USER.username,
password: TEST_USER.password,
}),
})
if (!loginRes.ok) {
const body = await loginRes.text()
throw new Error(`Login failed (${loginRes.status}): ${body}`)
}
const { expire, token } = await loginRes.json()
// Write a Playwright storage-state file containing the token in localStorage
const stateDir = path.join(__dirname, '.auth')
await mkdir(stateDir, { recursive: true })
await writeFile(
path.join(stateDir, 'state.json'),
JSON.stringify({
cookies: [],
origins: [
{
origin: FRONTEND_URL,
localStorage: [
{ name: 'token', value: token },
{ name: 'token_expiry', value: expire },
],
},
],
}),
'utf8',
)
console.log('[global-setup] Test user ready, storage state saved.')
}
async function waitForServer(url, retries = 20, delayMs = 1000) {
for (let i = 0; i < retries; i++) {
try {
const res = await fetch(`${url}/api/v1/auth/login`, {
method: 'POST',
body: '{}',
headers: { 'Content-Type': 'application/json' },
})
if (res.status < 500) return
} catch {
// server not up yet
}
console.log(`[global-setup] Waiting for server… (${i + 1}/${retries})`)
await new Promise(r => setTimeout(r, delayMs))
}
throw new Error(`Server at ${url} did not become ready in time`)
}

78
e2e/package-lock.json generated Normal file
View File

@@ -0,0 +1,78 @@
{
"name": "donetick-e2e",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "donetick-e2e",
"version": "1.0.0",
"devDependencies": {
"@playwright/test": "^1.44.0"
}
},
"node_modules/@playwright/test": {
"version": "1.59.1",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.59.1.tgz",
"integrity": "sha512-PG6q63nQg5c9rIi4/Z5lR5IVF7yU5MqmKaPOe0HSc0O2cX1fPi96sUQu5j7eo4gKCkB2AnNGoWt7y4/Xx3Kcqg==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"playwright": "1.59.1"
},
"bin": {
"playwright": "cli.js"
},
"engines": {
"node": ">=18"
}
},
"node_modules/fsevents": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/playwright": {
"version": "1.59.1",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.59.1.tgz",
"integrity": "sha512-C8oWjPR3F81yljW9o5OxcWzfh6avkVwDD2VYdwIGqTkl+OGFISgypqzfu7dOe4QNLL2aqcWBmI3PMtLIK233lw==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"playwright-core": "1.59.1"
},
"bin": {
"playwright": "cli.js"
},
"engines": {
"node": ">=18"
},
"optionalDependencies": {
"fsevents": "2.3.2"
}
},
"node_modules/playwright-core": {
"version": "1.59.1",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.59.1.tgz",
"integrity": "sha512-HBV/RJg81z5BiiZ9yPzIiClYV/QMsDCKUyogwH9p3MCP6IYjUFu/MActgYAvK0oWyV9NlwM3GLBjADyWgydVyg==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"playwright-core": "cli.js"
},
"engines": {
"node": ">=18"
}
}
}
}

15
e2e/package.json Normal file
View File

@@ -0,0 +1,15 @@
{
"name": "donetick-e2e",
"version": "1.0.0",
"private": true,
"type": "module",
"scripts": {
"test": "playwright test",
"test:headed": "playwright test --headed",
"test:report": "playwright show-report",
"test:full": "./run-e2e.sh"
},
"devDependencies": {
"@playwright/test": "^1.44.0"
}
}

32
e2e/playwright.config.js Normal file
View File

@@ -0,0 +1,32 @@
import { defineConfig, devices } from '@playwright/test'
// When frontend and backend are the same origin (embedded binary) set E2E_BASE_URL.
// For dev, set E2E_FRONTEND_URL (default 5173) and E2E_API_URL (default 2021) separately.
const FRONTEND_URL =
process.env.E2E_FRONTEND_URL ||
process.env.E2E_BASE_URL ||
'http://localhost:5173'
export default defineConfig({
testDir: './tests',
timeout: 30_000,
retries: process.env.CI ? 2 : 0,
workers: 1, // sequential single SQLite DB
reporter: process.env.CI ? 'github' : 'list',
use: {
baseURL: FRONTEND_URL,
headless: true,
trace: 'on-first-retry',
screenshot: 'only-on-failure',
},
globalSetup: './global-setup.js',
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
],
})

118
e2e/run-e2e.sh Executable file
View File

@@ -0,0 +1,118 @@
#!/usr/bin/env bash
# Starts an isolated backend + frontend for this repo, runs the Playwright
# suite against them, then tears both servers down. Uses dedicated ports
# (2022/5180) so it never touches dev servers you may have running elsewhere
# (e.g. another worktree on the default 2021/5173).
set -uo pipefail
set -m # each backgrounded job gets its own process group, so we can kill it as a unit (portable alternative to setsid, which isn't available on macOS)
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
FRONTEND_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
BACKEND_DIR="$HOME/workspace/donetick-core"
BACKEND_PORT=2022
FRONTEND_PORT=5180
BACKEND_URL="http://localhost:$BACKEND_PORT"
FRONTEND_URL="http://localhost:$FRONTEND_PORT"
LOG_DIR="$SCRIPT_DIR/.e2e-run"
mkdir -p "$LOG_DIR"
BACKEND_PID=""
FRONTEND_PID=""
cleanup() {
echo "--- Stopping e2e servers ---"
[[ -n "$FRONTEND_PID" ]] && kill -TERM -"$FRONTEND_PID" 2>/dev/null
[[ -n "$BACKEND_PID" ]] && kill -TERM -"$BACKEND_PID" 2>/dev/null
sleep 1
[[ -n "$FRONTEND_PID" ]] && kill -KILL -"$FRONTEND_PID" 2>/dev/null
[[ -n "$BACKEND_PID" ]] && kill -KILL -"$BACKEND_PID" 2>/dev/null
wait 2>/dev/null
}
trap cleanup EXIT INT TERM
kill_port() {
local port=$1
local pids
pids=$(lsof -ti tcp:"$port" 2>/dev/null || true)
if [[ -n "$pids" ]]; then
echo "Port $port in use by leftover process(es) $pids — killing."
kill -TERM $pids 2>/dev/null
sleep 1
kill -KILL $pids 2>/dev/null || true
fi
}
wait_for_backend() {
for ((i = 1; i <= 40; i++)); do
if curl -s -o /dev/null -w '%{http_code}' -X POST "$BACKEND_URL/api/v1/auth/login" \
-H 'Content-Type: application/json' -d '{}' 2>/dev/null | grep -qE '^[0-9]+$'; then
return 0
fi
sleep 1
done
return 1
}
wait_for_frontend() {
for ((i = 1; i <= 40; i++)); do
if curl -sf -o /dev/null "$FRONTEND_URL" 2>/dev/null; then
return 0
fi
sleep 1
done
return 1
}
kill_port "$BACKEND_PORT"
kill_port "$FRONTEND_PORT"
echo "--- Starting backend on :$BACKEND_PORT (log: $LOG_DIR/backend.log) ---"
(
cd "$BACKEND_DIR"
exec env \
DT_NAME=e2e-frontend-repo \
DT_IS_DONE_TICK_DOT_COM=false \
DT_IS_USER_CREATION_DISABLED=false \
DT_DATABASE_TYPE=sqlite \
DT_DATABASE_MIGRATION=true \
DT_JWT_SECRET=e2e_test_secret_change_this_32chars \
DT_JWT_SESSION_TIME=168h \
DT_JWT_MAX_REFRESH=168h \
DT_SERVER_PORT="$BACKEND_PORT" \
DT_SERVER_READ_TIMEOUT=10s \
DT_SERVER_WRITE_TIMEOUT=10s \
DT_SERVER_RATE_PERIOD=60s \
DT_SERVER_RATE_LIMIT=300 \
DT_SERVER_CORS_ALLOW_ORIGINS="$FRONTEND_URL" \
DT_SERVER_SERVE_FRONTEND=false \
go run .
) > "$LOG_DIR/backend.log" 2>&1 &
BACKEND_PID=$!
echo "--- Starting frontend on :$FRONTEND_PORT (log: $LOG_DIR/frontend.log) ---"
(
cd "$FRONTEND_DIR"
exec env VITE_APP_API_URL="$BACKEND_URL" npx vite --port "$FRONTEND_PORT" --strictPort
) > "$LOG_DIR/frontend.log" 2>&1 &
FRONTEND_PID=$!
echo "Waiting for backend..."
if ! wait_for_backend; then
echo "Backend did not become ready. See $LOG_DIR/backend.log" >&2
exit 1
fi
echo "Waiting for frontend..."
if ! wait_for_frontend; then
echo "Frontend did not become ready. See $LOG_DIR/frontend.log" >&2
exit 1
fi
echo "--- Running Playwright tests ---"
cd "$SCRIPT_DIR"
E2E_FRONTEND_URL="$FRONTEND_URL" E2E_API_URL="$BACKEND_URL" npx playwright test "$@"
TEST_EXIT=$?
exit $TEST_EXIT

View File

@@ -0,0 +1,174 @@
import { expect, test } from '../fixtures/auth.js'
import { API_URL } from '../global-setup.js'
// Fetch the full chore list for the authenticated user via the API — used to
// verify a task's persisted fields without relying on list-page badge markup.
async function fetchChores(page) {
const token = await page.evaluate(() => localStorage.getItem('token'))
const apiRes = await fetch(`${API_URL}/api/v1/chores/`, {
headers: { Authorization: `Bearer ${token}` },
})
expect(apiRes.ok).toBe(true)
const { res: chores } = await apiRes.json()
return chores
}
test.describe('AddTaskModal (quick-add)', () => {
// All tests in this suite run as the pre-authenticated E2E user
test.use({
storageState: '.auth/state.json',
})
test('creates a task from plain text title only and it appears in the list', async ({
page,
}) => {
const taskName = `E2E Quick Task ${Date.now()}`
// ── Open the quick-add modal from the chores list ───────────────────────
await page.goto('/chores')
await page.getByTestId('open-add-task-modal').click()
const dialog = page.getByRole('dialog', { name: 'Create new task' })
await expect(dialog).toBeVisible()
// ── Type a plain title (no parseable tokens) and submit with Enter ──────
const input = dialog.locator('textarea')
await input.fill(taskName)
await input.press('Enter')
// ── Modal closes and the task shows up in the list ──────────────────────
await expect(dialog).toBeHidden()
await expect(page.getByText(taskName)).toBeVisible({ timeout: 10_000 })
// ── Verify it exists via the API too ─────────────────────────────────────
const chores = await fetchChores(page)
const created = chores.find(c => c.name === taskName)
expect(created).toBeDefined()
})
test('smart-parses a due date and priority from typed text', async ({
page,
}) => {
// Kept lowercase: parsePriority's cleanup step lowercases the whole
// sentence when a priority token matches, so an already-lowercase prefix
// sidesteps that quirk and round-trips unchanged.
const taskName = `e2e parse task ${Date.now()}`
await page.goto('/chores')
await page.getByTestId('open-add-task-modal').click()
const dialog = page.getByRole('dialog', { name: 'Create new task' })
await expect(dialog).toBeVisible()
const input = dialog.locator('textarea')
await input.fill(`${taskName} !p1 tomorrow`)
// ── Confirm the smart parse is reflected in the pickers before saving ───
await expect(
dialog.getByRole('button', { name: 'P1', exact: true }),
).toBeVisible()
// The due-date trigger only ever reads "Due" in its empty state, so once
// parsing lands a date the button relabels itself away from that text.
await expect(
dialog.getByRole('button', { name: 'Due', exact: true }),
).toHaveCount(0)
await input.press('Enter')
await expect(dialog).toBeHidden()
await expect(page.getByText(taskName)).toBeVisible({ timeout: 10_000 })
// ── Verify the parsed priority + due date persisted via the API ─────────
const chores = await fetchChores(page)
const created = chores.find(c => c.name === taskName)
expect(created).toBeDefined()
expect(created.priority).toBe(1)
expect(created.nextDueDate).toBeTruthy()
})
test('creates a task using the picker UI directly, skipping smart-parse tokens', async ({
page,
}) => {
const taskName = `E2E Picker Task ${Date.now()}`
await page.goto('/chores')
await page.getByTestId('open-add-task-modal').click()
const dialog = page.getByRole('dialog', { name: 'Create new task' })
await expect(dialog).toBeVisible()
// Plain title only — none of "due date"/"priority"/"repeat" tokens.
await dialog.locator('textarea').fill(taskName)
// ── Set the due date via the picker (Due → Tomorrow → Apply) ────────────
await dialog.getByRole('button', { name: 'Due', exact: true }).click()
const dueDateDialog = page.getByRole('dialog', { name: 'Due Date' })
await dueDateDialog
.getByRole('checkbox', { name: 'Tomorrow', exact: true })
.click()
await dueDateDialog
.getByRole('button', { name: 'Apply', exact: true })
.click()
// ── Set the priority via the picker (Priority → P2) ──────────────────────
await dialog.getByRole('button', { name: 'Priority', exact: true }).click()
await page.getByRole('button', { name: 'P2', exact: true }).click()
await expect(
dialog.getByRole('button', { name: 'P2', exact: true }),
).toBeVisible()
// ── Set the repeat schedule via the picker (Repeat → Daily → Apply) ─────
await dialog.getByRole('button', { name: 'Repeat', exact: true }).click()
const repeatDialog = page.getByRole('dialog', { name: 'Repeat Schedule' })
await repeatDialog
.getByRole('checkbox', { name: 'Daily', exact: true })
.click()
await repeatDialog
.getByRole('button', { name: 'Apply', exact: true })
.click()
await dialog.getByRole('button', { name: 'Create', exact: true }).click()
await expect(dialog).toBeHidden()
await expect(page.getByText(taskName)).toBeVisible({ timeout: 10_000 })
// ── Verify the picker-set values persisted via the API ───────────────────
const chores = await fetchChores(page)
const created = chores.find(c => c.name === taskName)
expect(created).toBeDefined()
expect(created.priority).toBe(2)
expect(created.frequencyType).toBe('daily')
expect(created.nextDueDate).toBeTruthy()
})
test('Create button is disabled until the title is non-empty', async ({
page,
}) => {
await page.goto('/chores')
await page.getByTestId('open-add-task-modal').click()
const dialog = page.getByRole('dialog', { name: 'Create new task' })
await expect(dialog).toBeVisible()
const createButton = dialog.getByRole('button', {
name: 'Create',
exact: true,
})
const input = dialog.locator('textarea')
// ── Empty title ───────────────────────────────────────────────────────
await expect(createButton).toBeDisabled()
// ── Whitespace-only title still counts as empty ─────────────────────────
await input.fill(' ')
await expect(createButton).toBeDisabled()
// ── Real text enables the button ─────────────────────────────────────────
await input.fill('E2E Enable Check')
await expect(createButton).toBeEnabled()
// No task should be created by this test — back out via Cancel.
await dialog.getByRole('button', { name: 'Cancel', exact: true }).click()
await expect(dialog).toBeHidden()
})
})

68
e2e/tests/auth.spec.js Normal file
View File

@@ -0,0 +1,68 @@
import { expect, test } from '@playwright/test'
import { loginViaUI, signUpViaUI } from '../fixtures/auth.js'
// Username must match /^[a-z.-]+$/ — no digits allowed.
// Generate a random lowercase-only suffix for uniqueness across runs.
function randomSuffix(len = 8) {
const chars = 'abcdefghijklmnopqrstuvwxyz'
return Array.from(
{ length: len },
() => chars[Math.floor(Math.random() * 26)],
).join('')
}
test.describe('Auth Sign Up', () => {
test('creates a new account and lands on /chores', async ({ page }) => {
const suffix = randomSuffix()
const user = {
username: `test.signup.${suffix}`,
email: `signup.${suffix}@donetick.test`,
password: 'TestPassword123!',
displayName: 'Test Signup User',
}
await signUpViaUI(page, user)
await expect(page).toHaveURL(/\/chores/)
})
test('shows an error when username is too short', async ({ page }) => {
await page.goto('/signup')
await page.locator('#username').fill('ab') // < 4 chars
await page.locator('#email').fill('short@donetick.test')
await page.locator('#password').fill('ValidPass123!')
await page.locator('#displayName').fill('Short User')
await page.getByRole('button', { name: 'Create account' }).click()
await expect(
page.getByText('Username must be at least 4 characters'),
).toBeVisible()
})
})
test.describe('Auth Login', () => {
// Re-use the shared E2E user that global-setup already created
test('logs in with valid credentials and lands on /chores', async ({
page,
}) => {
await loginViaUI(page, {
username: 'e2e.user',
password: 'E2ePassword123!',
})
await expect(page).toHaveURL(/\/chores/)
})
test('shows an error for wrong password', async ({ page }) => {
await page.goto('/login')
await page.locator('#username').fill('e2e.user')
await page.locator('#password').fill('WrongPassword!')
await page.getByRole('button', { name: 'Sign In' }).click()
// The notification snackbar / error message should appear
await expect(
page.getByText(/Login Failed|invalid|incorrect/i).first(),
).toBeVisible({ timeout: 5_000 })
})
})

View File

@@ -0,0 +1,150 @@
import { expect, test } from '../fixtures/auth.js'
import { API_URL } from '../global-setup.js'
test.describe('ChoreEdit', () => {
// All tests in this suite run as the pre-authenticated E2E user
test.use({
storageState: '.auth/state.json',
})
test('blocks save and shows an error when the name is left blank', async ({
page,
}) => {
// ── Navigate to the create chore page, leave the form at defaults ──────
await page.goto('/chores/create')
// ── Attempt to save without a name ──────────────────────────────────────
await page.getByRole('button', { name: 'Create' }).click()
// ── The validation error is shown and we never leave the create page ───
await expect(page.getByText('Name is required').first()).toBeVisible({
timeout: 5_000,
})
await expect(page).toHaveURL(/\/chores\/create/)
})
test('days-of-the-week repeat: blocks save with no days selected, then succeeds once days are chosen', async ({
page,
}) => {
const choreName = `E2E Weekly Chore ${Date.now()}`
// ── Navigate to the create chore page ────────────────────────────────
await page.goto('/chores/create')
// ── Fill in the chore name ────────────────────────────────────────────
await page.locator('input').first().fill(choreName)
// ── Enable recurrence and switch to a custom "days of the week" schedule
await page.getByLabel('Repeat this task').click()
await page.getByLabel('Custom').click()
await page.getByLabel('Days of the Week').click()
// ── Attempt to save with zero days selected ─────────────────────────────
await page.getByRole('button', { name: 'Create' }).click()
await expect(
page.getByText('Please select at least one day of the week').first(),
).toBeVisible({ timeout: 5_000 })
await expect(page).toHaveURL(/\/chores\/create/)
// ── Select Monday and Wednesday, then save successfully ────────────────
await page.getByLabel('Monday').click()
await page.getByLabel('Wednesday').click()
await page.getByRole('button', { name: 'Create' }).click()
await page.waitForURL('**/chores', { timeout: 15_000 })
await expect(page.getByText(choreName)).toBeVisible({ timeout: 10_000 })
// ── Verify the chore exists in the API with the selected days ─────────
const token = await page.evaluate(() => localStorage.getItem('token'))
const apiRes = await fetch(`${API_URL}/api/v1/chores/`, {
headers: { Authorization: `Bearer ${token}` },
})
expect(apiRes.ok).toBe(true)
const { res: chores } = await apiRes.json()
const created = chores.find(c => c.name === choreName)
expect(created).toBeDefined()
expect(created.frequencyType).toBe('days_of_the_week')
expect([...(created.frequencyMetadata?.days || [])].sort()).toEqual([
'monday',
'wednesday',
])
})
test('creates a chore, edits it after reload, and toggles Anyone assignment', async ({
page,
}) => {
const originalName = `E2E Edit Chore ${Date.now()}`
const updatedName = `${originalName} Updated`
const updatedDescription = `Updated by e2e ${Date.now()}`
// One week out, formatted as YYYY-MM-DD for the native date input
const updatedDueDate = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000)
.toISOString()
.slice(0, 10)
// ── Create a simple one-off chore with a due date ───────────────────────
await page.goto('/chores/create')
await page.locator('input').first().fill(originalName)
// Toggle "Anyone" on then off again — confirm it snaps back to the
// default self-assignment before we submit.
await page.getByLabel('Anyone').click()
await expect(page.getByLabel('Anyone')).toBeChecked()
await page.getByLabel('Anyone').click()
await expect(page.getByLabel('Anyone')).not.toBeChecked()
// Give it a due date so it's a simple one-off chore
await page.getByLabel('Give this task a due date').click()
await page.getByRole('button', { name: 'Create' }).click()
await page.waitForURL('**/chores', { timeout: 15_000 })
await expect(page.getByText(originalName)).toBeVisible({ timeout: 10_000 })
// ── Look up the created chore via the API (id + self-assignment state) ─
const token = await page.evaluate(() => localStorage.getItem('token'))
const listRes = await fetch(`${API_URL}/api/v1/chores/`, {
headers: { Authorization: `Bearer ${token}` },
})
expect(listRes.ok).toBe(true)
const { res: chores } = await listRes.json()
const created = chores.find(c => c.name === originalName)
expect(created).toBeDefined()
// Default self-assignment: exactly one assignee, matching assignedTo.
expect(created.assignees?.length).toBe(1)
expect(created.assignedTo).toBe(created.assignees[0].userId)
// ── Edit: change name, description, due date, and switch to Anyone ─────
await page.goto(`/chores/${created.id}/edit`)
await page.locator('input').first().fill(updatedName)
await page.locator('.ql-editor').fill(updatedDescription)
await page.locator('input[type="date"]').fill(updatedDueDate)
await page.getByLabel('Anyone').click()
await page.getByRole('button', { name: 'Save' }).click()
await page.waitForURL('**/chores', { timeout: 15_000 })
// ── Reload the edit page and confirm the new values persisted in the UI
await page.goto(`/chores/${created.id}/edit`)
await expect(page.locator('input').first()).toHaveValue(updatedName)
await expect(page.locator('.ql-editor')).toContainText(updatedDescription)
await expect(page.locator('input[type="date"]')).toHaveValue(updatedDueDate)
await expect(page.getByLabel('Anyone')).toBeChecked()
// ── Confirm persisted via the API too ───────────────────────────────────
const detailRes = await fetch(`${API_URL}/api/v1/chores/${created.id}`, {
headers: { Authorization: `Bearer ${token}` },
})
expect(detailRes.ok).toBe(true)
const { res: updated } = await detailRes.json()
expect(updated.name).toBe(updatedName)
expect(updated.description).toContain(updatedDescription)
// Due-date persistence is already confirmed via the reloaded date input
// above; comparing the raw UTC `nextDueDate` string here would be
// timezone-fragile since the app stores it as an end-of-local-day UTC
// instant.
// "Anyone" assignment: no specific assignees, no fixed assignedTo.
expect(updated.assignees ?? []).toHaveLength(0)
expect(updated.assignedTo).toBeFalsy()
})
})

53
e2e/tests/chores.spec.js Normal file
View File

@@ -0,0 +1,53 @@
import { expect, test } from '../fixtures/auth.js'
import { API_URL } from '../global-setup.js'
test.describe('Chores Create', () => {
// All tests in this suite run as the pre-authenticated E2E user
test.use({
storageState: '.auth/state.json',
})
test('creates a daily recurring chore and confirms it appears in the list', async ({
page,
}) => {
const choreName = `E2E Daily Chore ${Date.now()}`
// ── Navigate to the create chore page ────────────────────────────────
await page.goto('/chores/create')
// ── Fill in the chore name ────────────────────────────────────────────
// Name input is the first <input> on the page (no id/placeholder)
await page.locator('input').first().fill(choreName)
// ── Enable recurrence ─────────────────────────────────────────────────
// Check the "Repeat this task" checkbox to reveal frequency options
await page.getByLabel('Repeat this task').click()
// Select "Daily" from the frequency chips
await page.getByLabel('Daily').click()
// The due date is auto-populated to today once a repeating type is selected.
// No manual date entry required unless testing date-specific behaviour.
// ── Save ──────────────────────────────────────────────────────────────
await page.getByRole('button', { name: 'Create' }).click()
// After save the app navigates back to the chore list
await page.waitForURL('**/chores', { timeout: 15_000 })
// ── Verify the chore appears in the UI ───────────────────────────────
await expect(page.getByText(choreName)).toBeVisible({ timeout: 10_000 })
// ── Verify the chore exists in the API ───────────────────────────────
const token = await page.evaluate(() => localStorage.getItem('token'))
const apiRes = await fetch(`${API_URL}/api/v1/chores/`, {
headers: { Authorization: `Bearer ${token}` },
})
expect(apiRes.ok).toBe(true)
const { res: chores } = await apiRes.json()
const created = chores.find(c => c.name === choreName)
expect(created).toBeDefined()
expect(created.frequencyType).toBe('daily')
})
})

View File

@@ -0,0 +1,164 @@
import { expect, test } from '../fixtures/auth.js'
import { API_URL } from '../global-setup.js'
test.describe('Projects & Filters Create and navigate', () => {
// All tests in this suite run as the pre-authenticated E2E user
test.use({
storageState: '.auth/state.json',
})
test('creates a project via the modal, tracks its task count, and filters chores by it', async ({
page,
}) => {
const projectName = `E2E Project ${Date.now()}`
const choreName = `E2E Project Chore ${Date.now()}`
// ── Create the project via the modal ─────────────────────────────────
await page.goto('/projects')
await page.getByTestId('open-add-project-modal').click()
const projectDialog = page.getByRole('dialog')
await projectDialog.getByLabel('Project Name').fill(projectName)
await projectDialog.getByRole('button', { name: 'Create' }).click()
// ── Verify the project appears in the list with a 0 task count ───────
// Scope to the row: the name Typography's parent Box also holds the
// "N tasks" chip as a sibling, so one level up covers both.
const projectRow = page
.getByText(projectName, { exact: true })
.locator('xpath=..')
await expect(projectRow.getByText('0 tasks')).toBeVisible({
timeout: 10_000,
})
// ── Look up the created project via the API to get its id ────────────
const token = await page.evaluate(() => localStorage.getItem('token'))
const projectsRes = await fetch(`${API_URL}/api/v1/projects`, {
headers: { Authorization: `Bearer ${token}` },
})
expect(projectsRes.ok).toBe(true)
const projects = await projectsRes.json()
const project = projects.find(p => p.name === projectName)
expect(project).toBeDefined()
// ── Create a chore inside the project directly via the API ───────────
// Cheaper than driving the full chore-create UI, and this project is
// brand new so its task count is guaranteed to go from 0 to 1.
const choreRes = await fetch(`${API_URL}/api/v1/chores/`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`,
},
body: JSON.stringify({
name: choreName,
frequencyType: 'once',
assignStrategy: 'no_assignee',
projectId: project.id,
}),
})
expect(choreRes.ok).toBe(true)
// ── Reload so the project list reflects the new task count ───────────
await page.reload()
await expect(projectRow.getByText('1 tasks')).toBeVisible({
timeout: 10_000,
})
// ── Click the project card and confirm navigation + filtered results ─
await page.getByText(projectName, { exact: true }).click()
await page.waitForURL(new RegExp(`/chores\\?project=${project.id}(&|$)`), {
timeout: 10_000,
})
await expect(page.getByText(choreName)).toBeVisible({ timeout: 10_000 })
})
test('creates a filter via AdvancedFilterBuilder, tracks its task count, and filters chores by it', async ({
page,
}) => {
const filterProjectName = `E2E Filter Project ${Date.now()}`
const filterName = `E2E Filter ${Date.now()}`
const choreName = `E2E Filter Chore ${Date.now()}`
// ── Seed a throwaway project via the API to use as the filter's ──────
// condition target. A brand-new project has zero chores, so the
// filter's task count is guaranteed to start at 0 regardless of
// whatever else has accumulated in the shared test database.
await page.goto('/filters')
const token = await page.evaluate(() => localStorage.getItem('token'))
const createProjectRes = await fetch(`${API_URL}/api/v1/projects`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`,
},
body: JSON.stringify({ name: filterProjectName }),
})
expect(createProjectRes.ok).toBe(true)
const { res: filterProject } = await createProjectRes.json()
// Reload so the modal's project list (fetched on page load) includes it
await page.reload()
// ── Create the filter via AdvancedFilterBuilder ───────────────────────
await page.getByTestId('open-add-filter-modal').click()
const filterDialog = page.getByRole('dialog')
await filterDialog
.getByPlaceholder('e.g. Overdue tasks for Alice')
.fill(filterName)
// Condition: Projects is <the seeded project> — one condition is enough
await filterDialog
.getByRole('button', { name: filterProjectName, exact: true })
.click()
await filterDialog.getByRole('button', { name: 'Save Filter' }).click()
// ── Verify the filter appears in the list with a 0 task count ────────
// The name Typography sits inside a name-row Box, itself inside the
// content Box that also holds the "N tasks" chip — two levels up.
const filterRow = page
.getByText(filterName, { exact: true })
.locator('xpath=../..')
await expect(filterRow.getByText('0 tasks')).toBeVisible({
timeout: 10_000,
})
// ── Look up the created filter via the API to get its id ─────────────
const filtersRes = await fetch(`${API_URL}/api/v1/filters`, {
headers: { Authorization: `Bearer ${token}` },
})
expect(filtersRes.ok).toBe(true)
const filters = await filtersRes.json()
const filter = filters.find(f => f.name === filterName)
expect(filter).toBeDefined()
// ── Create a chore matching the filter's project condition ───────────
const choreRes = await fetch(`${API_URL}/api/v1/chores/`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`,
},
body: JSON.stringify({
name: choreName,
frequencyType: 'once',
assignStrategy: 'no_assignee',
projectId: filterProject.id,
}),
})
expect(choreRes.ok).toBe(true)
// ── Reload so the filter list reflects the new task count ────────────
await page.reload()
await expect(filterRow.getByText('1 tasks')).toBeVisible({
timeout: 10_000,
})
// ── Click the filter card and confirm navigation + filtered results ──
await page.getByText(filterName, { exact: true }).click()
await page.waitForURL(new RegExp(`/chores\\?filterId=${filter.id}(&|$)`), {
timeout: 10_000,
})
await expect(page.getByText(choreName)).toBeVisible({ timeout: 10_000 })
})
})

210
eslint.config.mjs Normal file
View File

@@ -0,0 +1,210 @@
import js from '@eslint/js'
import stylistic from '@stylistic/eslint-plugin'
import queryPlugin from '@tanstack/eslint-plugin-query'
import typescriptEslint from '@typescript-eslint/eslint-plugin'
import tsParser from '@typescript-eslint/parser'
import eslintConfigPrettier from 'eslint-config-prettier/flat'
import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended'
import react from 'eslint-plugin-react'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
import simpleImportSort from 'eslint-plugin-simple-import-sort'
import sortDestructureKeys from 'eslint-plugin-sort-destructure-keys'
import tailwind from 'eslint-plugin-tailwindcss'
import globals from 'globals'
export default [
{
ignores: [
'dist',
'build',
'vendor',
'android',
'ios',
'resources',
'public/assets',
'.agents',
'.impeccable',
],
},
js.configs.recommended,
react.configs.flat.recommended,
{
...react.configs.flat['jsx-runtime'],
settings: {
react: {
version: 'detect',
},
},
},
reactHooks.configs.flat.recommended,
reactRefresh.configs.recommended,
{
plugins: {
'simple-import-sort': simpleImportSort,
},
rules: {
'simple-import-sort/exports': 'error',
'simple-import-sort/imports': 'error',
},
},
{
plugins: {
'sort-destructure-keys': sortDestructureKeys,
},
rules: {
'sort-destructure-keys/sort-destructure-keys': 'error',
},
},
...tailwind.configs['flat/recommended'],
...queryPlugin.configs['flat/recommended'],
eslintConfigPrettier, // Disable any rules that conflict with prettier
eslintPluginPrettierRecommended,
{
files: [
'*.{js,mjs,cjs,ts}',
'scripts/**/*.{js,mjs,cjs,ts}',
'workers/**/*.{js,mjs,cjs,ts}',
],
languageOptions: {
globals: globals.node,
},
},
{
files: ['e2e/**/*.{js,mjs}'],
languageOptions: {
globals: {
...globals.node,
...globals.browser,
},
},
},
{
files: ['*.config.{mjs,ts}'],
languageOptions: {
ecmaVersion: 'latest',
sourceType: 'module',
},
rules: {
// Formatting rules (indent, space-infix-ops) are owned by prettier here —
// re-enabling them fights prettier/prettier and makes --fix unable to converge.
eqeqeq: 'error',
},
},
{
files: ['src/**/*.{js,jsx,ts,tsx}'],
languageOptions: {
ecmaVersion: 'latest',
globals: globals.browser,
sourceType: 'module',
},
plugins: {
'@stylistic': stylistic,
},
rules: {
'@stylistic/brace-style': 'error',
'@stylistic/comma-dangle': ['error', 'always-multiline'],
'@stylistic/keyword-spacing': 'error',
'@stylistic/no-multiple-empty-lines': ['error', { max: 1 }],
'@stylistic/no-trailing-spaces': 'error',
'@stylistic/object-curly-spacing': ['error', 'always'],
'@stylistic/semi': ['error', 'never'],
'@stylistic/space-in-parens': 'error',
'@stylistic/space-infix-ops': ['error', { int32Hint: false }],
'@stylistic/type-annotation-spacing': [
'error',
{
after: true,
before: true,
overrides: {
colon: {
before: false,
},
},
},
],
eqeqeq: 'warn',
'max-depth': ['error', { max: 5 }],
'max-len': ['warn', { code: 120 }],
'max-lines': [
'error',
{ max: 2000, skipBlankLines: true, skipComments: true },
],
'max-lines-per-function': ['warn', { max: 500 }],
'no-constant-condition': 'warn',
'no-loss-of-precision': 'error',
'no-undef': 'warn',
'no-unused-vars': 'off', // @typescript-eslint/no-unused-vars
'space-infix-ops': 'off', // @stylistic/space-infix-ops
},
},
{
files: ['*.{ts,tsx}'],
languageOptions: {
parser: tsParser,
parserOptions: {
project: 'tsconfig.json',
},
},
plugins: {
'@typescript-eslint': typescriptEslint,
},
rules: {
'@typescript-eslint/consistent-type-definitions': ['error', 'type'],
'@typescript-eslint/no-duplicate-enum-values': 'error',
'@typescript-eslint/no-explicit-any': 'error',
'@typescript-eslint/no-inferrable-types': 'error',
'@typescript-eslint/no-mixed-enums': 'error',
'@typescript-eslint/no-require-imports': 'error',
'@typescript-eslint/no-type-alias': [
'error',
{
allowAliases: 'always',
allowCallbacks: 'always',
allowConditionalTypes: 'never',
allowConstructors: 'never',
allowGenerics: 'always',
allowLiterals: 'always',
allowMappedTypes: 'never',
allowTupleTypes: 'always',
},
],
'@typescript-eslint/no-unnecessary-type-assertion': 'error',
'@typescript-eslint/no-unsafe-argument': 'warn',
'@typescript-eslint/no-unsafe-assignment': 'warn',
'@typescript-eslint/no-unsafe-member-access': 'warn',
'@typescript-eslint/no-unused-vars': [
'warn',
{ argsIgnorePattern: '^_' },
],
'@typescript-eslint/prefer-as-const': 'error',
'@typescript-eslint/prefer-for-of': 'warn',
'@typescript-eslint/prefer-function-type': 'error',
'@typescript-eslint/prefer-includes': 'error',
'@typescript-eslint/prefer-literal-enum-member': 'error',
'@typescript-eslint/prefer-nullish-coalescing': 'warn',
},
},
{
// Reduce existing errors to warnings
plugins: {
'@tanstack/query': queryPlugin,
},
rules: {
'@tanstack/query/exhaustive-deps': 'warn',
'no-case-declarations': 'warn',
'no-empty': 'warn',
'no-redeclare': 'warn',
'react-hooks/immutability': 'warn',
'react-hooks/preserve-manual-memoization': 'warn',
'react-hooks/purity': 'warn',
'react-hooks/refs': 'warn',
'react-hooks/set-state-in-effect': 'warn',
'react-hooks/static-components': 'warn',
'react-refresh/only-export-components': 'warn',
'react/jsx-no-undef': 'warn',
'react/no-unescaped-entities': 'warn',
'react/prop-types': 'warn',
},
},
]

View File

@@ -26,6 +26,7 @@ platform :ios do
signingStyle: "manual",
provisioningProfiles: {
"com.donetick.app" => "Donetick App Store(fastline)",
"com.donetick.app.widget" => "Donetick Widget App Store(fastline)",
},
},
)

View File

@@ -31,8 +31,7 @@ Build a signed, App Store-ready .ipa
Upload the built .ipa to TestFlight
----
---
## Android
@@ -60,7 +59,7 @@ Build a signed release .apk (for sideloading/testing)
Upload the built .aab to the Play Store (internal testing track by default)
----
---
This README.md is auto-generated and will be re-generated every time [_fastlane_](https://fastlane.tools) is run.

View File

@@ -16,6 +16,28 @@
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="#5bbad5" />
<meta name="msapplication-TileColor" content="#da532c" />
<meta name="theme-color" content="#ffffff" />
<!--
Plausible. This index.html is shared by the app and the marketing bundles
(see src/main.jsx), so the tag is gated rather than hardcoded: it loads
only when the page host matches VITE_APP_PLAUSIBLE_DOMAIN, which is the
site registered in Plausible. On app.donetick.com the host never matches,
so nothing loads. Unset the variable to disable it entirely.
-->
<script>
;(function () {
var domain = 'donetick.com'
// Vite leaves the literal placeholder when the variable is unset.
var host = window.location.hostname
if (host !== domain) return
var s = document.createElement('script')
s.defer = true
// s.setAttribute('data-domain', domain)
s.setAttribute('data-domain', 'donetick.com')
s.src = 'https://collector.wannaknow.link/js/script.js'
document.head.appendChild(s)
})()
</script>
</head>
<body>
<div id="root"></div>

View File

@@ -4,4 +4,4 @@
"capacitor": {}
},
"type": "custom"
}
}

View File

@@ -17,6 +17,10 @@
72FA9293C4A649D1BA0E5917 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 71866EB277374608AD02C137 /* PrivacyInfo.xcprivacy */; };
A084ECDBA7D38E1E42DFC39D /* Pods_App.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = AF277DCFFFF123FFC6DF26C7 /* Pods_App.framework */; };
D1115B492C653D60004C6043 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = D1115B482C653D60004C6043 /* GoogleService-Info.plist */; };
D0AC000000000000000000B1 /* WidgetBridgePlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0AC000000000000000000A1 /* WidgetBridgePlugin.swift */; };
D0AC000000000000000000B2 /* MainViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0AC000000000000000000A2 /* MainViewController.swift */; };
D0AC000000000000000000B3 /* DonetickWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0AC000000000000000000A3 /* DonetickWidget.swift */; };
D0AC000000000000000000B4 /* DonetickWidgetExtension.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = D0AC000000000000000000A4 /* DonetickWidgetExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
/* End PBXBuildFile section */
/* Begin PBXFileReference section */
@@ -35,8 +39,38 @@
D1115B482C653D60004C6043 /* GoogleService-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = "<group>"; };
D1115B4A2C659D51004C6043 /* App.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = App.entitlements; sourceTree = "<group>"; };
FC68EB0AF532CFC21C3344DD /* Pods-App.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App.debug.xcconfig"; path = "Pods/Target Support Files/Pods-App/Pods-App.debug.xcconfig"; sourceTree = "<group>"; };
D0AC000000000000000000A1 /* WidgetBridgePlugin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WidgetBridgePlugin.swift; sourceTree = "<group>"; };
D0AC000000000000000000A2 /* MainViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainViewController.swift; sourceTree = "<group>"; };
D0AC000000000000000000A3 /* DonetickWidget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DonetickWidget.swift; sourceTree = "<group>"; };
D0AC000000000000000000A4 /* DonetickWidgetExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = DonetickWidgetExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; };
D0AC000000000000000000A5 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
D0AC000000000000000000A6 /* DonetickWidget.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DonetickWidget.entitlements; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXCopyFilesBuildPhase section */
D0AC000000000000000000C2 /* Embed App Extensions */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "";
dstSubfolderSpec = 13;
files = (
D0AC000000000000000000B4 /* DonetickWidgetExtension.appex in Embed App Extensions */,
);
name = "Embed App Extensions";
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXContainerItemProxy section */
D0AC000000000000000000E2 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 504EC2FC1FED79650016851F /* Project object */;
proxyType = 1;
remoteGlobalIDString = D0AC000000000000000000D1;
remoteInfo = DonetickWidgetExtension;
};
/* End PBXContainerItemProxy section */
/* Begin PBXFrameworksBuildPhase section */
504EC3011FED79650016851F /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
@@ -46,6 +80,13 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
D0AC000000000000000000D3 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
@@ -61,6 +102,7 @@
isa = PBXGroup;
children = (
504EC3061FED79650016851F /* App */,
D0AC000000000000000000C1 /* DonetickWidget */,
504EC3051FED79650016851F /* Products */,
7F8756D8B27F46E3366F6CEA /* Pods */,
27E2DDA53C4D2A4D1A88CE4A /* Frameworks */,
@@ -68,10 +110,21 @@
);
sourceTree = "<group>";
};
D0AC000000000000000000C1 /* DonetickWidget */ = {
isa = PBXGroup;
children = (
D0AC000000000000000000A3 /* DonetickWidget.swift */,
D0AC000000000000000000A5 /* Info.plist */,
D0AC000000000000000000A6 /* DonetickWidget.entitlements */,
);
path = DonetickWidget;
sourceTree = "<group>";
};
504EC3051FED79650016851F /* Products */ = {
isa = PBXGroup;
children = (
504EC3041FED79650016851F /* App.app */,
D0AC000000000000000000A4 /* DonetickWidgetExtension.appex */,
);
name = Products;
sourceTree = "<group>";
@@ -82,6 +135,8 @@
D1115B4A2C659D51004C6043 /* App.entitlements */,
50379B222058CBB4000EE86E /* capacitor.config.json */,
504EC3071FED79650016851F /* AppDelegate.swift */,
D0AC000000000000000000A2 /* MainViewController.swift */,
D0AC000000000000000000A1 /* WidgetBridgePlugin.swift */,
504EC30B1FED79650016851F /* Main.storyboard */,
504EC30E1FED79650016851F /* Assets.xcassets */,
504EC3101FED79650016851F /* LaunchScreen.storyboard */,
@@ -114,16 +169,35 @@
504EC3011FED79650016851F /* Frameworks */,
504EC3021FED79650016851F /* Resources */,
9592DBEFFC6D2A0C8D5DEB22 /* [CP] Embed Pods Frameworks */,
D0AC000000000000000000C2 /* Embed App Extensions */,
);
buildRules = (
);
dependencies = (
D0AC000000000000000000E1 /* PBXTargetDependency */,
);
name = App;
productName = App;
productReference = 504EC3041FED79650016851F /* App.app */;
productType = "com.apple.product-type.application";
};
D0AC000000000000000000D1 /* DonetickWidgetExtension */ = {
isa = PBXNativeTarget;
buildConfigurationList = D0AC000000000000000000F1 /* Build configuration list for PBXNativeTarget "DonetickWidgetExtension" */;
buildPhases = (
D0AC000000000000000000D2 /* Sources */,
D0AC000000000000000000D3 /* Frameworks */,
D0AC000000000000000000D4 /* Resources */,
);
buildRules = (
);
dependencies = (
);
name = DonetickWidgetExtension;
productName = DonetickWidgetExtension;
productReference = D0AC000000000000000000A4 /* DonetickWidgetExtension.appex */;
productType = "com.apple.product-type.app-extension";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
@@ -138,6 +212,10 @@
LastSwiftMigration = 1100;
ProvisioningStyle = Automatic;
};
D0AC000000000000000000D1 = {
CreatedOnToolsVersion = 15.0;
ProvisioningStyle = Automatic;
};
};
};
buildConfigurationList = 504EC2FF1FED79650016851F /* Build configuration list for PBXProject "App" */;
@@ -156,6 +234,7 @@
projectRoot = "";
targets = (
504EC3031FED79650016851F /* App */,
D0AC000000000000000000D1 /* DonetickWidgetExtension */,
);
};
/* End PBXProject section */
@@ -176,6 +255,13 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
D0AC000000000000000000D4 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
@@ -220,11 +306,29 @@
buildActionMask = 2147483647;
files = (
504EC3081FED79650016851F /* AppDelegate.swift in Sources */,
D0AC000000000000000000B2 /* MainViewController.swift in Sources */,
D0AC000000000000000000B1 /* WidgetBridgePlugin.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
D0AC000000000000000000D2 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
D0AC000000000000000000B3 /* DonetickWidget.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin PBXTargetDependency section */
D0AC000000000000000000E1 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = D0AC000000000000000000D1 /* DonetickWidgetExtension */;
targetProxy = D0AC000000000000000000E2 /* PBXContainerItemProxy */;
};
/* End PBXTargetDependency section */
/* Begin PBXVariantGroup section */
504EC30B1FED79650016851F /* Main.storyboard */ = {
isa = PBXVariantGroup;
@@ -358,12 +462,12 @@
CODE_SIGN_ALLOW_ENTITLEMENTS_MODIFICATION = YES;
CODE_SIGN_ENTITLEMENTS = App/App.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 37;
CURRENT_PROJECT_VERSION = 70;
DEVELOPMENT_TEAM = 6UJJ78R3BS;
INFOPLIST_FILE = App/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
MARKETING_VERSION = 1.2.16;
MARKETING_VERSION = 1.2.49;
PRODUCT_BUNDLE_IDENTIFIER = com.donetick.app;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
@@ -381,12 +485,12 @@
CODE_SIGN_IDENTITY = "Apple Distribution";
CODE_SIGN_STYLE = Manual;
PROVISIONING_PROFILE_SPECIFIER = "Donetick App Store(fastline)";
CURRENT_PROJECT_VERSION = 37;
CURRENT_PROJECT_VERSION = 70;
DEVELOPMENT_TEAM = 6UJJ78R3BS;
INFOPLIST_FILE = App/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
MARKETING_VERSION = 1.2.16;
MARKETING_VERSION = 1.2.49;
PRODUCT_BUNDLE_IDENTIFIER = com.donetick.app;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "";
@@ -395,6 +499,48 @@
};
name = Release;
};
D0AC000000000000000000F2 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
CODE_SIGN_ENTITLEMENTS = DonetickWidget/DonetickWidget.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 70;
DEVELOPMENT_TEAM = 6UJJ78R3BS;
INFOPLIST_FILE = DonetickWidget/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks";
MARKETING_VERSION = 1.2.49;
PRODUCT_BUNDLE_IDENTIFIER = com.donetick.app.widget;
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
};
D0AC000000000000000000F3 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
CODE_SIGN_ENTITLEMENTS = DonetickWidget/DonetickWidget.entitlements;
CODE_SIGN_IDENTITY = "Apple Distribution";
CODE_SIGN_STYLE = Manual;
PROVISIONING_PROFILE_SPECIFIER = "Donetick Widget App Store(fastline)";
CURRENT_PROJECT_VERSION = 70;
DEVELOPMENT_TEAM = 6UJJ78R3BS;
INFOPLIST_FILE = DonetickWidget/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks";
MARKETING_VERSION = 1.2.49;
PRODUCT_BUNDLE_IDENTIFIER = com.donetick.app.widget;
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Release;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
@@ -416,6 +562,15 @@
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
D0AC000000000000000000F1 /* Build configuration list for PBXNativeTarget "DonetickWidgetExtension" */ = {
isa = XCConfigurationList;
buildConfigurations = (
D0AC000000000000000000F2 /* Debug */,
D0AC000000000000000000F3 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
};
rootObject = 504EC2FC1FED79650016851F /* Project object */;

View File

@@ -4,6 +4,14 @@
<dict>
<key>aps-environment</key>
<string>development</string>
<key>com.apple.developer.associated-domains</key>
<array>
<string>applinks:app.donetick.com</string>
</array>
<key>com.apple.security.application-groups</key>
<array>
<string>group.com.donetick.app</string>
</array>
<key>com.apple.developer.applesignin</key>
<array>
<string>Default</string>

View File

@@ -11,4 +11,4 @@
"author": "xcode",
"version": 1
}
}
}

View File

@@ -1,6 +1,6 @@
{
"info" : {
"version" : 1,
"author" : "xcode"
"info": {
"version": 1,
"author": "xcode"
}
}
}

View File

@@ -53,4 +53,4 @@
"version": 1,
"author": "xcode"
}
}
}

View File

@@ -11,7 +11,7 @@
<!--Bridge View Controller-->
<scene sceneID="tne-QT-ifu">
<objects>
<viewController id="BYZ-38-t0r" customClass="CAPBridgeViewController" customModule="Capacitor" sceneMemberID="viewController"/>
<viewController id="BYZ-38-t0r" customClass="MainViewController" customModule="App" customModuleProvider="target" sceneMemberID="viewController"/>
<placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
</objects>
</scene>

View File

@@ -28,6 +28,10 @@
<string>This app needs access to camera to take photos to attach to task or use as profile photo</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>This app needs access to photo library to select images to attach to task or use as profile photo</string>
<key>NSMicrophoneUsageDescription</key>
<string>This app uses the microphone to let you create tasks by speaking</string>
<key>NSSpeechRecognitionUsageDescription</key>
<string>This app uses on-device speech recognition to turn your voice into tasks</string>
<key>UIBackgroundModes</key>
<array/>
<key>UILaunchStoryboardName</key>

View File

@@ -0,0 +1,9 @@
import Capacitor
import UIKit
/// Registers app-local Capacitor plugins; referenced from Main.storyboard.
class MainViewController: CAPBridgeViewController {
override func capacitorDidLoad() {
bridge?.registerPluginInstance(WidgetBridgePlugin())
}
}

View File

@@ -0,0 +1,46 @@
import Capacitor
import Foundation
import WidgetKit
/// JS bridge for the home-screen widgets (see src/service/WidgetService.js).
/// Persists the task snapshot + API config in the shared App Group so the
/// widget extension can read them, then asks WidgetKit to redraw.
@objc(WidgetBridgePlugin)
public class WidgetBridgePlugin: CAPPlugin, CAPBridgedPlugin {
public let identifier = "WidgetBridgePlugin"
public let jsName = "WidgetBridge"
public let pluginMethods: [CAPPluginMethod] = [
CAPPluginMethod(name: "update", returnType: CAPPluginReturnPromise),
CAPPluginMethod(name: "clear", returnType: CAPPluginReturnPromise),
]
static let appGroup = "group.com.donetick.app"
static let dataKey = "widget_tasks"
static let configKey = "widget_config"
@objc func update(_ call: CAPPluginCall) {
guard let defaults = UserDefaults(suiteName: Self.appGroup) else {
call.reject("App Group \(Self.appGroup) unavailable")
return
}
if let data = call.getString("data") {
defaults.set(data, forKey: Self.dataKey)
}
if let config = call.getString("config") {
defaults.set(config, forKey: Self.configKey)
}
WidgetCenter.shared.reloadAllTimelines()
call.resolve()
}
@objc func clear(_ call: CAPPluginCall) {
guard let defaults = UserDefaults(suiteName: Self.appGroup) else {
call.reject("App Group \(Self.appGroup) unavailable")
return
}
defaults.removeObject(forKey: Self.dataKey)
defaults.removeObject(forKey: Self.configKey)
WidgetCenter.shared.reloadAllTimelines()
call.resolve()
}
}

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.application-groups</key>
<array>
<string>group.com.donetick.app</string>
</array>
</dict>
</plist>

File diff suppressed because it is too large Load Diff

View File

@@ -5,7 +5,7 @@
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
<string>DoneTick Widget</string>
<string>Donetick</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>

View File

@@ -11,15 +11,19 @@ install! 'cocoapods', :disable_input_output_paths => true
def capacitor_pods
pod 'Capacitor', :path => '../../node_modules/@capacitor/ios'
pod 'CapacitorCordova', :path => '../../node_modules/@capacitor/ios'
pod 'CapacitorCommunityInAppReview', :path => '../../node_modules/@capacitor-community/in-app-review'
pod 'CapacitorCommunitySpeechRecognition', :path => '../../node_modules/@capacitor-community/speech-recognition'
pod 'CapacitorCommunitySqlite', :path => '../../node_modules/@capacitor-community/sqlite'
pod 'CapacitorApp', :path => '../../node_modules/@capacitor/app'
pod 'CapacitorBrowser', :path => '../../node_modules/@capacitor/browser'
pod 'CapacitorDevice', :path => '../../node_modules/@capacitor/device'
pod 'CapacitorHaptics', :path => '../../node_modules/@capacitor/haptics'
pod 'CapacitorLocalLlm', :path => '../../node_modules/@capacitor/local-llm'
pod 'CapacitorLocalNotifications', :path => '../../node_modules/@capacitor/local-notifications'
pod 'CapacitorNetwork', :path => '../../node_modules/@capacitor/network'
pod 'CapacitorPreferences', :path => '../../node_modules/@capacitor/preferences'
pod 'CapacitorPushNotifications', :path => '../../node_modules/@capacitor/push-notifications'
pod 'CapacitorShare', :path => '../../node_modules/@capacitor/share'
pod 'CapacitorStatusBar', :path => '../../node_modules/@capacitor/status-bar'
pod 'CapgoCapacitorDocumentScanner', :path => '../../node_modules/@capgo/capacitor-document-scanner'
pod 'CapgoCapacitorNfc', :path => '../../node_modules/@capgo/capacitor-nfc'

View File

@@ -12,22 +12,28 @@ PODS:
- PromisesObjC (~> 2.4)
- PromisesSwift (~> 2.4)
- RecaptchaInterop (~> 101.0)
- Capacitor (8.4.1):
- Capacitor (8.4.2):
- CapacitorCordova
- CapacitorApp (8.1.0):
- CapacitorApp (8.1.1):
- Capacitor
- CapacitorBrowser (8.0.3):
- CapacitorBrowser (8.0.4):
- Capacitor
- CapacitorCommunityInAppReview (8.0.0):
- Capacitor
- CapacitorCommunitySpeechRecognition (7.0.1):
- Capacitor
- CapacitorCommunitySqlite (8.1.0):
- Capacitor
- SQLCipher
- ZIPFoundation
- CapacitorCordova (8.4.1)
- CapacitorDevice (8.0.2):
- CapacitorCordova (8.4.2)
- CapacitorDevice (8.0.3):
- Capacitor
- CapacitorHaptics (8.0.2):
- Capacitor
- CapacitorLocalLlm (1.0.0):
- Capacitor
- CapacitorLocalNotifications (8.2.0):
- CapacitorLocalNotifications (8.2.1):
- Capacitor
- CapacitorNetwork (8.0.1):
- Capacitor
@@ -35,28 +41,20 @@ PODS:
- Capacitor
- CapacitorPreferences (8.0.1):
- Capacitor
- CapacitorPushNotifications (8.1.1):
- CapacitorPushNotifications (8.1.2):
- Capacitor
- CapacitorStatusBar (8.0.2):
- CapacitorShare (8.0.1):
- Capacitor
- CapgoCapacitorDocumentScanner (8.4.0):
- CapacitorStatusBar (8.0.3):
- Capacitor
- CapgoCapacitorDocumentScanner (8.4.2):
- Capacitor
- CapgoCapacitorNfc (8.2.2):
- Capacitor
- CapgoCapacitorSocialLogin (8.3.36):
- CapgoCapacitorSocialLogin (8.3.39):
- Alamofire (~> 5.10.2)
- Capacitor
- FBSDKCoreKit (~> 18.0)
- FBSDKLoginKit (~> 18.0)
- GoogleSignIn (~> 9.0.0)
- FBAEMKit (18.1.0):
- FBSDKCoreKit_Basics (= 18.1.0)
- FBSDKCoreKit (18.1.0):
- FBAEMKit (= 18.1.0)
- FBSDKCoreKit_Basics (= 18.1.0)
- FBSDKCoreKit_Basics (18.1.0)
- FBSDKLoginKit (18.1.0):
- FBSDKCoreKit (= 18.1.0)
- FirebaseCore (12.15.0):
- FirebaseCoreInternal (~> 12.15.0)
- GoogleUtilities/Environment (~> 8.1)
@@ -149,15 +147,19 @@ DEPENDENCIES:
- "Capacitor (from `../../node_modules/@capacitor/ios`)"
- "CapacitorApp (from `../../node_modules/@capacitor/app`)"
- "CapacitorBrowser (from `../../node_modules/@capacitor/browser`)"
- "CapacitorCommunityInAppReview (from `../../node_modules/@capacitor-community/in-app-review`)"
- "CapacitorCommunitySpeechRecognition (from `../../node_modules/@capacitor-community/speech-recognition`)"
- "CapacitorCommunitySqlite (from `../../node_modules/@capacitor-community/sqlite`)"
- "CapacitorCordova (from `../../node_modules/@capacitor/ios`)"
- "CapacitorDevice (from `../../node_modules/@capacitor/device`)"
- "CapacitorHaptics (from `../../node_modules/@capacitor/haptics`)"
- "CapacitorLocalLlm (from `../../node_modules/@capacitor/local-llm`)"
- "CapacitorLocalNotifications (from `../../node_modules/@capacitor/local-notifications`)"
- "CapacitorNetwork (from `../../node_modules/@capacitor/network`)"
- CapacitorPluginSafeArea (from `../../node_modules/capacitor-plugin-safe-area`)
- "CapacitorPreferences (from `../../node_modules/@capacitor/preferences`)"
- "CapacitorPushNotifications (from `../../node_modules/@capacitor/push-notifications`)"
- "CapacitorShare (from `../../node_modules/@capacitor/share`)"
- "CapacitorStatusBar (from `../../node_modules/@capacitor/status-bar`)"
- "CapgoCapacitorDocumentScanner (from `../../node_modules/@capgo/capacitor-document-scanner`)"
- "CapgoCapacitorNfc (from `../../node_modules/@capgo/capacitor-nfc`)"
@@ -172,10 +174,6 @@ SPEC REPOS:
- Alamofire
- AppAuth
- AppCheckCore
- FBAEMKit
- FBSDKCoreKit
- FBSDKCoreKit_Basics
- FBSDKLoginKit
- FirebaseCore
- FirebaseCoreInternal
- FirebaseInstallations
@@ -203,12 +201,18 @@ EXTERNAL SOURCES:
:path: "../../node_modules/@capacitor/app"
CapacitorBrowser:
:path: "../../node_modules/@capacitor/browser"
CapacitorCommunityInAppReview:
:path: "../../node_modules/@capacitor-community/in-app-review"
CapacitorCommunitySpeechRecognition:
:path: "../../node_modules/@capacitor-community/speech-recognition"
CapacitorCommunitySqlite:
:path: "../../node_modules/@capacitor-community/sqlite"
CapacitorCordova:
:path: "../../node_modules/@capacitor/ios"
CapacitorDevice:
:path: "../../node_modules/@capacitor/device"
CapacitorHaptics:
:path: "../../node_modules/@capacitor/haptics"
CapacitorLocalLlm:
:path: "../../node_modules/@capacitor/local-llm"
CapacitorLocalNotifications:
@@ -221,6 +225,8 @@ EXTERNAL SOURCES:
:path: "../../node_modules/@capacitor/preferences"
CapacitorPushNotifications:
:path: "../../node_modules/@capacitor/push-notifications"
CapacitorShare:
:path: "../../node_modules/@capacitor/share"
CapacitorStatusBar:
:path: "../../node_modules/@capacitor/status-bar"
CapgoCapacitorDocumentScanner:
@@ -240,26 +246,26 @@ SPEC CHECKSUMS:
Alamofire: 7193b3b92c74a07f85569e1a6c4f4237291e7496
AppAuth: ef4da5a3fc2e10b90c09a0a94a9baeaedc0341d5
AppCheckCore: 214137f5c378d1dec88a68425c467fe65aaff637
Capacitor: 35242afe195b1e53c58ca1b827d1b444c5e6602b
CapacitorApp: 449ffe26375e96f8aaaee625ac6e01e5c57c8650
CapacitorBrowser: c987c73d09d8bd3b5ec13f06338b1e14d5d2be69
Capacitor: 52f999235b8bd6a7d01694753f4f4d54d182e3a3
CapacitorApp: 305bd13c44f6f9d164e2ee66a4faa956ed8c0e06
CapacitorBrowser: 752e0208aa07c7aa63d1f7cde895a3e9519b2656
CapacitorCommunityInAppReview: 4492bdd34aad4d27ed87949376022cc93b294ea1
CapacitorCommunitySpeechRecognition: 3e03566c44c2bb3b52391a33d4518b1adbdeb38f
CapacitorCommunitySqlite: eac6acfb852f46e7988fc59604d7f900498d354e
CapacitorCordova: eebe6bcf807b1b06f3f48237650f96bbcd0eef09
CapacitorDevice: 14cba6f88d1c3074cbf825fea977c8c526453ff8
CapacitorCordova: 345eacdc4c8282415446aea4acd8c77d82480bd4
CapacitorDevice: 708e742b60a61572cd593e44c809592e8f31da14
CapacitorHaptics: 296f771ecd89c7a1bd92a7b6826a7d268e2e70f5
CapacitorLocalLlm: a05516151a02923a9e7dae9949d3817e85e321f0
CapacitorLocalNotifications: 2615aa008f608b95d3921a778ee1988abf1e6148
CapacitorLocalNotifications: 908a69a3cf7ae345426de3223d4980befb0103c6
CapacitorNetwork: 8812ce60d11fb63d8f2e4ba51a49b2e59892ebe2
CapacitorPluginSafeArea: 874619c00586248f1694210e72038123d422c2d9
CapacitorPreferences: cca2021f386efb75947c850334447d9ff22b14f1
CapacitorPushNotifications: ec08d589c226a2c0db7c032ec1bf5b044ec85f8e
CapacitorStatusBar: 01d5763b4ed720de5ce2edbc938de6a98f4c8f32
CapgoCapacitorDocumentScanner: 7ad9e8ed9c054d551bfc948660d995b9545723d8
CapacitorPushNotifications: 32a7f840815f319fd9ba1c1c9b0b914be9d95237
CapacitorShare: 0c58305114538568059bfc07111f22dcb9cb2a82
CapacitorStatusBar: eca7bc2b58d9f886f1ef9edb66e57a9b29c121af
CapgoCapacitorDocumentScanner: 262bb84b73707f9e2e59071ca58013e3f9acf942
CapgoCapacitorNfc: 8ea158143c441e1cf6d231a971e375511558d027
CapgoCapacitorSocialLogin: adffda2bec52a765bdaebf3fa9a6083bd9341d60
FBAEMKit: 64088ff0380f50e4a56e2ee07d984f7f1aef7595
FBSDKCoreKit: 8390ea3a8fac186f444275f31d7512e760b47cba
FBSDKCoreKit_Basics: 3a0005c78b355388bf2df5c6dbe6381b77ea4be3
FBSDKLoginKit: d8e2711a3a03b0026703735c8d28a2b5a0e1f4fd
CapgoCapacitorSocialLogin: 5ea6b14b8e9b1bd74533e580d4cf0e2b85d80793
FirebaseCore: 2e86a4ea1684d4381707069e4a6d89ac808e901e
FirebaseCoreInternal: 6ab6a02c94446c026d2cf35cf5383842ebaa4992
FirebaseInstallations: eb29ccbf64eaedf86fd5b2ccc7fabde567660b52
@@ -283,6 +289,6 @@ SPEC CHECKSUMS:
SQLCipher: eb79c64049cb002b4e9fcb30edb7979bf4706dfc
ZIPFoundation: dfd3d681c4053ff7e2f7350bc4e53b5dba3f5351
PODFILE CHECKSUM: 21b805bdbbb6ac4b8a3527ee8ef346ba0a55ef65
PODFILE CHECKSUM: 0170e6b548e03117ef7d6814596b8b140a6acce4
COCOAPODS: 1.16.2

5020
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
{
"name": "donetick",
"private": true,
"version": "1.2.16",
"version": "1.2.49",
"type": "module",
"engines": {
"node": ">=20.0.0",
@@ -20,7 +20,10 @@
"build-cf": "rm -rf package-lock.json && npm install && npm install --force @rollup/rollup-linux-x64-gnu@4.34.9 @swc/core-linux-x64-gnu && vite build",
"build-selfhosted": "rm -rf package-lock.json && npm install && npm install --force @rollup/rollup-linux-x64-gnu@4.34.9 @swc/core-linux-x64-gnu && vite build --mode selfhosted",
"build-win": "del package-lock.json && npm install && npm install --force @rollup/rollup-win32-x64-msvc @swc/core-win32-x64-msvc && vite build --mode selfhosted",
"lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0",
"lint": "eslint && prettier -c . && npm run lint:i18n",
"lint:i18n": "node scripts/i18n-audit.mjs",
"lint:ci": "eslint -f gha && prettier -c . && npm run lint:i18n",
"lint:fix": "eslint --fix && prettier -w .",
"preview": "vite preview",
"setup-m1": "rm -rf node_modules package-lock.json && npm install && npm install --force @rollup/rollup-darwin-arm64 @swc/core-darwin-arm64",
"setup-apple-silicon": "npm install --no-optional && npm install --force @rollup/rollup-darwin-arm64 @swc/core-darwin-arm64",
@@ -35,41 +38,51 @@
"bump": "node bump-version.js patch",
"bump:minor": "node bump-version.js minor",
"bump:major": "node bump-version.js major",
"bump:patch": "node bump-version.js patch"
"bump:patch": "node bump-version.js patch",
"postinstall": "patch-package"
},
"dependencies": {
"@capacitor-community/in-app-review": "^8.0.0",
"@capacitor-community/speech-recognition": "^7.0.1",
"@capacitor-community/sqlite": "^8.0.0",
"@capacitor/android": "^8.0.0",
"@capacitor/app": "^8.0.0",
"@capacitor/browser": "^8.0.0",
"@capacitor/core": "^8.0.0",
"@capacitor/device": "^8.0.0",
"@capacitor/haptics": "^8.0.2",
"@capacitor/ios": "^8.0.0",
"@capacitor/local-llm": "^1.0.0",
"@capacitor/local-notifications": "^8.0.0",
"@capacitor/network": "^8.0.0",
"@capacitor/preferences": "^8.0.0",
"@capacitor/push-notifications": "^8.0.0",
"@capacitor/share": "^8.0.1",
"@capacitor/status-bar": "^8.0.0",
"@capgo/capacitor-document-scanner": "^8.4.0",
"@capgo/capacitor-nfc": "^8.0.0",
"@capgo/capacitor-social-login": "^8.0.0",
"@capgo/capacitor-social-login": "^8.3.38",
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2",
"@emotion/cache": "^11.14.0",
"@emotion/react": "^11.11.3",
"@emotion/styled": "^11.11.0",
"@hello-pangea/dnd": "^18.0.1",
"@jcesarmobile/capacitor-ocr": "^0.3.0",
"@meauxt/react-swipeable-list": "^1.0.0",
"@mui/icons-material": "^5.16.13",
"@mui/joy": "^5.0.0-beta.20",
"@mui/joy": "5.0.0-beta.52",
"@mui/material": "^5.15.2",
"@openreplay/tracker": "^14.0.4",
"@posthog/rollup-plugin": "^1.4.9",
"@revenuecat/purchases-capacitor": "^12.0.0",
"@revenuecat/purchases-capacitor-ui": "^12.0.0",
"@stylistic/eslint-plugin": "^5.10.0",
"@swc/core": "^1.12.5",
"@tanstack/react-query": "^5.17.0",
"@typescript-eslint/eslint-plugin": "^8.59.4",
"@typescript-eslint/parser": "^8.59.4",
"aos": "^2.3.4",
"browser-image-compression": "^2.0.2",
"caniuse-lite": "^1.0.30001769",
@@ -85,6 +98,7 @@
"js-cookie": "^3.0.5",
"moment": "^2.30.1",
"murmurhash": "^2.0.1",
"posthog-js": "^1.415.5",
"prop-types": "^15.8.1",
"qrcode": "^1.5.4",
"quill": "^2.0.3",
@@ -99,6 +113,8 @@
"reactjs-social-login": "^2.6.3",
"recharts": "^2.15.0",
"reusify": "^1.0.4",
"stylis": "^4.4.0",
"stylis-plugin-rtl": "^2.1.1",
"tesseract.js": "^7.0.0",
"vite-plugin-pwa": "^0.20.0"
},
@@ -113,19 +129,21 @@
"autoprefixer": "^10.4.16",
"baseline-browser-mapping": "^2.9.19",
"capacitor-set-version": "^2.2.0",
"eslint": "^8.56.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-prettier": "^5.1.2",
"eslint-plugin-react": "^7.33.2",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.4.5",
"eslint-plugin-simple-import-sort": "^10.0.0",
"eslint-plugin-sort-destructure-keys": "^1.5.0",
"eslint-plugin-sort-keys-fix": "^1.1.2",
"eslint-plugin-tailwindcss": "^3.13.1",
"eslint": "^9.39.4",
"eslint-config-prettier": "^10.1.8",
"eslint-formatter-gha": "^2.0.1",
"eslint-plugin-prettier": "^5.5.6",
"eslint-plugin-react": "^7.37.5",
"eslint-plugin-react-hooks": "^7.1.1",
"eslint-plugin-react-refresh": "^0.5.2",
"eslint-plugin-simple-import-sort": "^13.0.0",
"eslint-plugin-sort-destructure-keys": "^3.0.0",
"eslint-plugin-tailwindcss": "^3.18.3",
"husky": "^8.0.3",
"lint-staged": "^16.4.0",
"patch-package": "^8.0.1",
"postcss": "^8.4.32",
"prettier": "^3.1.1",
"prettier": "^3.8.3",
"prettier-plugin-tailwindcss": "^0.5.10",
"tailwindcss": "^3.4.0",
"typescript": "^5.8.0",

View File

@@ -0,0 +1,484 @@
diff --git a/node_modules/@capacitor-community/speech-recognition/android/build/.transforms/3b668565d422d3defdbffac23d2ae7a9/results.bin b/node_modules/@capacitor-community/speech-recognition/android/build/.transforms/3b668565d422d3defdbffac23d2ae7a9/results.bin
new file mode 100644
index 0000000..7ed749e
--- /dev/null
+++ b/node_modules/@capacitor-community/speech-recognition/android/build/.transforms/3b668565d422d3defdbffac23d2ae7a9/results.bin
@@ -0,0 +1 @@
+o/bundleLibRuntimeToDirDebug
diff --git a/node_modules/@capacitor-community/speech-recognition/android/build/.transforms/3b668565d422d3defdbffac23d2ae7a9/transformed/bundleLibRuntimeToDirDebug/bundleLibRuntimeToDirDebug_dex/com/getcapacitor/community/speechrecognition/Constants.dex b/node_modules/@capacitor-community/speech-recognition/android/build/.transforms/3b668565d422d3defdbffac23d2ae7a9/transformed/bundleLibRuntimeToDirDebug/bundleLibRuntimeToDirDebug_dex/com/getcapacitor/community/speechrecognition/Constants.dex
new file mode 100644
index 0000000..720cd1e
Binary files /dev/null and b/node_modules/@capacitor-community/speech-recognition/android/build/.transforms/3b668565d422d3defdbffac23d2ae7a9/transformed/bundleLibRuntimeToDirDebug/bundleLibRuntimeToDirDebug_dex/com/getcapacitor/community/speechrecognition/Constants.dex differ
diff --git a/node_modules/@capacitor-community/speech-recognition/android/build/.transforms/3b668565d422d3defdbffac23d2ae7a9/transformed/bundleLibRuntimeToDirDebug/bundleLibRuntimeToDirDebug_dex/com/getcapacitor/community/speechrecognition/Receiver.dex b/node_modules/@capacitor-community/speech-recognition/android/build/.transforms/3b668565d422d3defdbffac23d2ae7a9/transformed/bundleLibRuntimeToDirDebug/bundleLibRuntimeToDirDebug_dex/com/getcapacitor/community/speechrecognition/Receiver.dex
new file mode 100644
index 0000000..5d21223
Binary files /dev/null and b/node_modules/@capacitor-community/speech-recognition/android/build/.transforms/3b668565d422d3defdbffac23d2ae7a9/transformed/bundleLibRuntimeToDirDebug/bundleLibRuntimeToDirDebug_dex/com/getcapacitor/community/speechrecognition/Receiver.dex differ
diff --git a/node_modules/@capacitor-community/speech-recognition/android/build/.transforms/3b668565d422d3defdbffac23d2ae7a9/transformed/bundleLibRuntimeToDirDebug/bundleLibRuntimeToDirDebug_dex/com/getcapacitor/community/speechrecognition/SpeechRecognition$SpeechRecognitionListener.dex b/node_modules/@capacitor-community/speech-recognition/android/build/.transforms/3b668565d422d3defdbffac23d2ae7a9/transformed/bundleLibRuntimeToDirDebug/bundleLibRuntimeToDirDebug_dex/com/getcapacitor/community/speechrecognition/SpeechRecognition$SpeechRecognitionListener.dex
new file mode 100644
index 0000000..668e631
Binary files /dev/null and b/node_modules/@capacitor-community/speech-recognition/android/build/.transforms/3b668565d422d3defdbffac23d2ae7a9/transformed/bundleLibRuntimeToDirDebug/bundleLibRuntimeToDirDebug_dex/com/getcapacitor/community/speechrecognition/SpeechRecognition$SpeechRecognitionListener.dex differ
diff --git a/node_modules/@capacitor-community/speech-recognition/android/build/.transforms/3b668565d422d3defdbffac23d2ae7a9/transformed/bundleLibRuntimeToDirDebug/bundleLibRuntimeToDirDebug_dex/com/getcapacitor/community/speechrecognition/SpeechRecognition.dex b/node_modules/@capacitor-community/speech-recognition/android/build/.transforms/3b668565d422d3defdbffac23d2ae7a9/transformed/bundleLibRuntimeToDirDebug/bundleLibRuntimeToDirDebug_dex/com/getcapacitor/community/speechrecognition/SpeechRecognition.dex
new file mode 100644
index 0000000..59b8c5b
Binary files /dev/null and b/node_modules/@capacitor-community/speech-recognition/android/build/.transforms/3b668565d422d3defdbffac23d2ae7a9/transformed/bundleLibRuntimeToDirDebug/bundleLibRuntimeToDirDebug_dex/com/getcapacitor/community/speechrecognition/SpeechRecognition.dex differ
diff --git a/node_modules/@capacitor-community/speech-recognition/android/build/.transforms/3b668565d422d3defdbffac23d2ae7a9/transformed/bundleLibRuntimeToDirDebug/desugar_graph.bin b/node_modules/@capacitor-community/speech-recognition/android/build/.transforms/3b668565d422d3defdbffac23d2ae7a9/transformed/bundleLibRuntimeToDirDebug/desugar_graph.bin
new file mode 100644
index 0000000..d60cb42
Binary files /dev/null and b/node_modules/@capacitor-community/speech-recognition/android/build/.transforms/3b668565d422d3defdbffac23d2ae7a9/transformed/bundleLibRuntimeToDirDebug/desugar_graph.bin differ
diff --git a/node_modules/@capacitor-community/speech-recognition/android/build/.transforms/a9f31cd11f0b4f2759dee65fdc12b529/results.bin b/node_modules/@capacitor-community/speech-recognition/android/build/.transforms/a9f31cd11f0b4f2759dee65fdc12b529/results.bin
new file mode 100644
index 0000000..0d259dd
--- /dev/null
+++ b/node_modules/@capacitor-community/speech-recognition/android/build/.transforms/a9f31cd11f0b4f2759dee65fdc12b529/results.bin
@@ -0,0 +1 @@
+o/classes
diff --git a/node_modules/@capacitor-community/speech-recognition/android/build/.transforms/a9f31cd11f0b4f2759dee65fdc12b529/transformed/classes/classes_dex/classes.dex b/node_modules/@capacitor-community/speech-recognition/android/build/.transforms/a9f31cd11f0b4f2759dee65fdc12b529/transformed/classes/classes_dex/classes.dex
new file mode 100644
index 0000000..d4d71e5
Binary files /dev/null and b/node_modules/@capacitor-community/speech-recognition/android/build/.transforms/a9f31cd11f0b4f2759dee65fdc12b529/transformed/classes/classes_dex/classes.dex differ
diff --git a/node_modules/@capacitor-community/speech-recognition/android/build/intermediates/aapt_friendly_merged_manifests/debug/processDebugManifest/aapt/AndroidManifest.xml b/node_modules/@capacitor-community/speech-recognition/android/build/intermediates/aapt_friendly_merged_manifests/debug/processDebugManifest/aapt/AndroidManifest.xml
new file mode 100644
index 0000000..b81ecea
--- /dev/null
+++ b/node_modules/@capacitor-community/speech-recognition/android/build/intermediates/aapt_friendly_merged_manifests/debug/processDebugManifest/aapt/AndroidManifest.xml
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.getcapacitor.community.speechrecognition.speechrecognition" >
+
+ <uses-sdk android:minSdkVersion="28" />
+
+ <uses-permission android:name="android.permission.RECORD_AUDIO" />
+
+ <queries>
+ <intent>
+ <action android:name="android.speech.RecognitionService" />
+ </intent>
+ </queries>
+
+</manifest>
\ No newline at end of file
diff --git a/node_modules/@capacitor-community/speech-recognition/android/build/intermediates/aapt_friendly_merged_manifests/debug/processDebugManifest/aapt/output-metadata.json b/node_modules/@capacitor-community/speech-recognition/android/build/intermediates/aapt_friendly_merged_manifests/debug/processDebugManifest/aapt/output-metadata.json
new file mode 100644
index 0000000..b3c0af9
--- /dev/null
+++ b/node_modules/@capacitor-community/speech-recognition/android/build/intermediates/aapt_friendly_merged_manifests/debug/processDebugManifest/aapt/output-metadata.json
@@ -0,0 +1,18 @@
+{
+ "version": 3,
+ "artifactType": {
+ "type": "AAPT_FRIENDLY_MERGED_MANIFESTS",
+ "kind": "Directory"
+ },
+ "applicationId": "com.getcapacitor.community.speechrecognition.speechrecognition",
+ "variantName": "debug",
+ "elements": [
+ {
+ "type": "SINGLE",
+ "filters": [],
+ "attributes": [],
+ "outputFile": "AndroidManifest.xml"
+ }
+ ],
+ "elementType": "File"
+}
\ No newline at end of file
diff --git a/node_modules/@capacitor-community/speech-recognition/android/build/intermediates/aar_metadata/debug/writeDebugAarMetadata/aar-metadata.properties b/node_modules/@capacitor-community/speech-recognition/android/build/intermediates/aar_metadata/debug/writeDebugAarMetadata/aar-metadata.properties
new file mode 100644
index 0000000..1211b1e
--- /dev/null
+++ b/node_modules/@capacitor-community/speech-recognition/android/build/intermediates/aar_metadata/debug/writeDebugAarMetadata/aar-metadata.properties
@@ -0,0 +1,6 @@
+aarFormatVersion=1.0
+aarMetadataVersion=1.0
+minCompileSdk=1
+minCompileSdkExtension=0
+minAndroidGradlePluginVersion=1.0.0
+coreLibraryDesugaringEnabled=false
diff --git a/node_modules/@capacitor-community/speech-recognition/android/build/intermediates/annotation_processor_list/debug/javaPreCompileDebug/annotationProcessors.json b/node_modules/@capacitor-community/speech-recognition/android/build/intermediates/annotation_processor_list/debug/javaPreCompileDebug/annotationProcessors.json
new file mode 100644
index 0000000..9e26dfe
--- /dev/null
+++ b/node_modules/@capacitor-community/speech-recognition/android/build/intermediates/annotation_processor_list/debug/javaPreCompileDebug/annotationProcessors.json
@@ -0,0 +1 @@
+{}
\ No newline at end of file
diff --git a/node_modules/@capacitor-community/speech-recognition/android/build/intermediates/compile_library_classes_jar/debug/bundleLibCompileToJarDebug/classes.jar b/node_modules/@capacitor-community/speech-recognition/android/build/intermediates/compile_library_classes_jar/debug/bundleLibCompileToJarDebug/classes.jar
new file mode 100644
index 0000000..ff6ad96
Binary files /dev/null and b/node_modules/@capacitor-community/speech-recognition/android/build/intermediates/compile_library_classes_jar/debug/bundleLibCompileToJarDebug/classes.jar differ
diff --git a/node_modules/@capacitor-community/speech-recognition/android/build/intermediates/compile_r_class_jar/debug/generateDebugRFile/R.jar b/node_modules/@capacitor-community/speech-recognition/android/build/intermediates/compile_r_class_jar/debug/generateDebugRFile/R.jar
new file mode 100644
index 0000000..eb6f866
Binary files /dev/null and b/node_modules/@capacitor-community/speech-recognition/android/build/intermediates/compile_r_class_jar/debug/generateDebugRFile/R.jar differ
diff --git a/node_modules/@capacitor-community/speech-recognition/android/build/intermediates/compile_symbol_list/debug/generateDebugRFile/R.txt b/node_modules/@capacitor-community/speech-recognition/android/build/intermediates/compile_symbol_list/debug/generateDebugRFile/R.txt
new file mode 100644
index 0000000..f7ac549
--- /dev/null
+++ b/node_modules/@capacitor-community/speech-recognition/android/build/intermediates/compile_symbol_list/debug/generateDebugRFile/R.txt
@@ -0,0 +1,3 @@
+int id webview 0x0
+int layout bridge_layout_main 0x0
+int string my_string 0x0
diff --git a/node_modules/@capacitor-community/speech-recognition/android/build/intermediates/compiled_local_resources/debug/compileDebugLibraryResources/out/layout_bridge_layout_main.xml.flat b/node_modules/@capacitor-community/speech-recognition/android/build/intermediates/compiled_local_resources/debug/compileDebugLibraryResources/out/layout_bridge_layout_main.xml.flat
new file mode 100644
index 0000000..183eadd
Binary files /dev/null and b/node_modules/@capacitor-community/speech-recognition/android/build/intermediates/compiled_local_resources/debug/compileDebugLibraryResources/out/layout_bridge_layout_main.xml.flat differ
diff --git a/node_modules/@capacitor-community/speech-recognition/android/build/intermediates/incremental/debug/packageDebugResources/compile-file-map.properties b/node_modules/@capacitor-community/speech-recognition/android/build/intermediates/incremental/debug/packageDebugResources/compile-file-map.properties
new file mode 100644
index 0000000..4e5a864
--- /dev/null
+++ b/node_modules/@capacitor-community/speech-recognition/android/build/intermediates/incremental/debug/packageDebugResources/compile-file-map.properties
@@ -0,0 +1,2 @@
+#Mon Jul 20 15:42:26 EDT 2026
+com.getcapacitor.community.speechrecognition.speechrecognition.capacitor-community-speech-recognition-main-6\:/layout/bridge_layout_main.xml=/Users/mohamad-macbook-air/workspace/temp/dt-frontend-worktrees/support-audio-input/node_modules/@capacitor-community/speech-recognition/android/build/intermediates/packaged_res/debug/packageDebugResources/layout/bridge_layout_main.xml
diff --git a/node_modules/@capacitor-community/speech-recognition/android/build/intermediates/incremental/debug/packageDebugResources/merged.dir/values/values.xml b/node_modules/@capacitor-community/speech-recognition/android/build/intermediates/incremental/debug/packageDebugResources/merged.dir/values/values.xml
new file mode 100644
index 0000000..e95bf66
--- /dev/null
+++ b/node_modules/@capacitor-community/speech-recognition/android/build/intermediates/incremental/debug/packageDebugResources/merged.dir/values/values.xml
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="my_string">Just a simple string</string>
+</resources>
\ No newline at end of file
diff --git a/node_modules/@capacitor-community/speech-recognition/android/build/intermediates/incremental/debug/packageDebugResources/merger.xml b/node_modules/@capacitor-community/speech-recognition/android/build/intermediates/incremental/debug/packageDebugResources/merger.xml
new file mode 100644
index 0000000..3058471
--- /dev/null
+++ b/node_modules/@capacitor-community/speech-recognition/android/build/intermediates/incremental/debug/packageDebugResources/merger.xml
@@ -0,0 +1,2 @@
+<?xml version="1.0" encoding="utf-8"?>
+<merger version="3"><dataSet aapt-namespace="http://schemas.android.com/apk/res-auto" config="main$Generated" generated="true" ignore_pattern="!.svn:!.git:!.ds_store:!*.scc:.*:&lt;dir>_*:!CVS:!thumbs.db:!picasa.ini:!*~"><source path="/Users/mohamad-macbook-air/workspace/temp/dt-frontend-worktrees/support-audio-input/node_modules/@capacitor-community/speech-recognition/android/src/main/res"/></dataSet><dataSet aapt-namespace="http://schemas.android.com/apk/res-auto" config="main" generated-set="main$Generated" ignore_pattern="!.svn:!.git:!.ds_store:!*.scc:.*:&lt;dir>_*:!CVS:!thumbs.db:!picasa.ini:!*~"><source path="/Users/mohamad-macbook-air/workspace/temp/dt-frontend-worktrees/support-audio-input/node_modules/@capacitor-community/speech-recognition/android/src/main/res"><file name="bridge_layout_main" path="/Users/mohamad-macbook-air/workspace/temp/dt-frontend-worktrees/support-audio-input/node_modules/@capacitor-community/speech-recognition/android/src/main/res/layout/bridge_layout_main.xml" qualifiers="" type="layout"/><file path="/Users/mohamad-macbook-air/workspace/temp/dt-frontend-worktrees/support-audio-input/node_modules/@capacitor-community/speech-recognition/android/src/main/res/values/colors.xml" qualifiers=""/><file path="/Users/mohamad-macbook-air/workspace/temp/dt-frontend-worktrees/support-audio-input/node_modules/@capacitor-community/speech-recognition/android/src/main/res/values/styles.xml" qualifiers=""/><file path="/Users/mohamad-macbook-air/workspace/temp/dt-frontend-worktrees/support-audio-input/node_modules/@capacitor-community/speech-recognition/android/src/main/res/values/strings.xml" qualifiers=""><string name="my_string">Just a simple string</string></file></source></dataSet><dataSet aapt-namespace="http://schemas.android.com/apk/res-auto" config="debug$Generated" generated="true" ignore_pattern="!.svn:!.git:!.ds_store:!*.scc:.*:&lt;dir>_*:!CVS:!thumbs.db:!picasa.ini:!*~"><source path="/Users/mohamad-macbook-air/workspace/temp/dt-frontend-worktrees/support-audio-input/node_modules/@capacitor-community/speech-recognition/android/src/debug/res"/></dataSet><dataSet aapt-namespace="http://schemas.android.com/apk/res-auto" config="debug" generated-set="debug$Generated" ignore_pattern="!.svn:!.git:!.ds_store:!*.scc:.*:&lt;dir>_*:!CVS:!thumbs.db:!picasa.ini:!*~"><source path="/Users/mohamad-macbook-air/workspace/temp/dt-frontend-worktrees/support-audio-input/node_modules/@capacitor-community/speech-recognition/android/src/debug/res"/></dataSet><dataSet aapt-namespace="http://schemas.android.com/apk/res-auto" config="generated$Generated" generated="true" ignore_pattern="!.svn:!.git:!.ds_store:!*.scc:.*:&lt;dir>_*:!CVS:!thumbs.db:!picasa.ini:!*~"><source path="/Users/mohamad-macbook-air/workspace/temp/dt-frontend-worktrees/support-audio-input/node_modules/@capacitor-community/speech-recognition/android/build/generated/res/resValues/debug"/></dataSet><dataSet aapt-namespace="http://schemas.android.com/apk/res-auto" config="generated" generated-set="generated$Generated" ignore_pattern="!.svn:!.git:!.ds_store:!*.scc:.*:&lt;dir>_*:!CVS:!thumbs.db:!picasa.ini:!*~"><source path="/Users/mohamad-macbook-air/workspace/temp/dt-frontend-worktrees/support-audio-input/node_modules/@capacitor-community/speech-recognition/android/build/generated/res/resValues/debug"/></dataSet><mergedItems/></merger>
\ No newline at end of file
diff --git a/node_modules/@capacitor-community/speech-recognition/android/build/intermediates/incremental/mergeDebugAssets/merger.xml b/node_modules/@capacitor-community/speech-recognition/android/build/intermediates/incremental/mergeDebugAssets/merger.xml
new file mode 100644
index 0000000..8c51637
--- /dev/null
+++ b/node_modules/@capacitor-community/speech-recognition/android/build/intermediates/incremental/mergeDebugAssets/merger.xml
@@ -0,0 +1,2 @@
+<?xml version="1.0" encoding="utf-8"?>
+<merger version="3"><dataSet config="main" ignore_pattern="!.svn:!.git:!.ds_store:!*.scc:.*:&lt;dir>_*:!CVS:!thumbs.db:!picasa.ini:!*~"><source path="/Users/mohamad-macbook-air/workspace/temp/dt-frontend-worktrees/support-audio-input/node_modules/@capacitor-community/speech-recognition/android/src/main/assets"/></dataSet><dataSet config="debug" ignore_pattern="!.svn:!.git:!.ds_store:!*.scc:.*:&lt;dir>_*:!CVS:!thumbs.db:!picasa.ini:!*~"><source path="/Users/mohamad-macbook-air/workspace/temp/dt-frontend-worktrees/support-audio-input/node_modules/@capacitor-community/speech-recognition/android/src/debug/assets"/></dataSet><dataSet config="generated" ignore_pattern="!.svn:!.git:!.ds_store:!*.scc:.*:&lt;dir>_*:!CVS:!thumbs.db:!picasa.ini:!*~"><source path="/Users/mohamad-macbook-air/workspace/temp/dt-frontend-worktrees/support-audio-input/node_modules/@capacitor-community/speech-recognition/android/build/intermediates/shader_assets/debug/compileDebugShaders/out"/></dataSet></merger>
\ No newline at end of file
diff --git a/node_modules/@capacitor-community/speech-recognition/android/build/intermediates/incremental/mergeDebugJniLibFolders/merger.xml b/node_modules/@capacitor-community/speech-recognition/android/build/intermediates/incremental/mergeDebugJniLibFolders/merger.xml
new file mode 100644
index 0000000..472210d
--- /dev/null
+++ b/node_modules/@capacitor-community/speech-recognition/android/build/intermediates/incremental/mergeDebugJniLibFolders/merger.xml
@@ -0,0 +1,2 @@
+<?xml version="1.0" encoding="utf-8"?>
+<merger version="3"><dataSet config="main" ignore_pattern="!.svn:!.git:!.ds_store:!*.scc:.*:&lt;dir>_*:!CVS:!thumbs.db:!picasa.ini:!*~"><source path="/Users/mohamad-macbook-air/workspace/temp/dt-frontend-worktrees/support-audio-input/node_modules/@capacitor-community/speech-recognition/android/src/main/jniLibs"/></dataSet><dataSet config="debug" ignore_pattern="!.svn:!.git:!.ds_store:!*.scc:.*:&lt;dir>_*:!CVS:!thumbs.db:!picasa.ini:!*~"><source path="/Users/mohamad-macbook-air/workspace/temp/dt-frontend-worktrees/support-audio-input/node_modules/@capacitor-community/speech-recognition/android/src/debug/jniLibs"/></dataSet></merger>
\ No newline at end of file
diff --git a/node_modules/@capacitor-community/speech-recognition/android/build/intermediates/incremental/mergeDebugShaders/merger.xml b/node_modules/@capacitor-community/speech-recognition/android/build/intermediates/incremental/mergeDebugShaders/merger.xml
new file mode 100644
index 0000000..6bebd8c
--- /dev/null
+++ b/node_modules/@capacitor-community/speech-recognition/android/build/intermediates/incremental/mergeDebugShaders/merger.xml
@@ -0,0 +1,2 @@
+<?xml version="1.0" encoding="utf-8"?>
+<merger version="3"><dataSet config="main" ignore_pattern="!.svn:!.git:!.ds_store:!*.scc:.*:&lt;dir>_*:!CVS:!thumbs.db:!picasa.ini:!*~"><source path="/Users/mohamad-macbook-air/workspace/temp/dt-frontend-worktrees/support-audio-input/node_modules/@capacitor-community/speech-recognition/android/src/main/shaders"/></dataSet><dataSet config="debug" ignore_pattern="!.svn:!.git:!.ds_store:!*.scc:.*:&lt;dir>_*:!CVS:!thumbs.db:!picasa.ini:!*~"><source path="/Users/mohamad-macbook-air/workspace/temp/dt-frontend-worktrees/support-audio-input/node_modules/@capacitor-community/speech-recognition/android/src/debug/shaders"/></dataSet></merger>
\ No newline at end of file
diff --git a/node_modules/@capacitor-community/speech-recognition/android/build/intermediates/javac/debug/compileDebugJavaWithJavac/classes/com/getcapacitor/community/speechrecognition/Constants.class b/node_modules/@capacitor-community/speech-recognition/android/build/intermediates/javac/debug/compileDebugJavaWithJavac/classes/com/getcapacitor/community/speechrecognition/Constants.class
new file mode 100644
index 0000000..3094bd9
Binary files /dev/null and b/node_modules/@capacitor-community/speech-recognition/android/build/intermediates/javac/debug/compileDebugJavaWithJavac/classes/com/getcapacitor/community/speechrecognition/Constants.class differ
diff --git a/node_modules/@capacitor-community/speech-recognition/android/build/intermediates/javac/debug/compileDebugJavaWithJavac/classes/com/getcapacitor/community/speechrecognition/Receiver.class b/node_modules/@capacitor-community/speech-recognition/android/build/intermediates/javac/debug/compileDebugJavaWithJavac/classes/com/getcapacitor/community/speechrecognition/Receiver.class
new file mode 100644
index 0000000..af9be6c
Binary files /dev/null and b/node_modules/@capacitor-community/speech-recognition/android/build/intermediates/javac/debug/compileDebugJavaWithJavac/classes/com/getcapacitor/community/speechrecognition/Receiver.class differ
diff --git a/node_modules/@capacitor-community/speech-recognition/android/build/intermediates/javac/debug/compileDebugJavaWithJavac/classes/com/getcapacitor/community/speechrecognition/SpeechRecognition$SpeechRecognitionListener.class b/node_modules/@capacitor-community/speech-recognition/android/build/intermediates/javac/debug/compileDebugJavaWithJavac/classes/com/getcapacitor/community/speechrecognition/SpeechRecognition$SpeechRecognitionListener.class
new file mode 100644
index 0000000..a056329
Binary files /dev/null and b/node_modules/@capacitor-community/speech-recognition/android/build/intermediates/javac/debug/compileDebugJavaWithJavac/classes/com/getcapacitor/community/speechrecognition/SpeechRecognition$SpeechRecognitionListener.class differ
diff --git a/node_modules/@capacitor-community/speech-recognition/android/build/intermediates/javac/debug/compileDebugJavaWithJavac/classes/com/getcapacitor/community/speechrecognition/SpeechRecognition.class b/node_modules/@capacitor-community/speech-recognition/android/build/intermediates/javac/debug/compileDebugJavaWithJavac/classes/com/getcapacitor/community/speechrecognition/SpeechRecognition.class
new file mode 100644
index 0000000..57a50fe
Binary files /dev/null and b/node_modules/@capacitor-community/speech-recognition/android/build/intermediates/javac/debug/compileDebugJavaWithJavac/classes/com/getcapacitor/community/speechrecognition/SpeechRecognition.class differ
diff --git a/node_modules/@capacitor-community/speech-recognition/android/build/intermediates/local_only_symbol_list/debug/parseDebugLocalResources/R-def.txt b/node_modules/@capacitor-community/speech-recognition/android/build/intermediates/local_only_symbol_list/debug/parseDebugLocalResources/R-def.txt
new file mode 100644
index 0000000..80ec5cb
--- /dev/null
+++ b/node_modules/@capacitor-community/speech-recognition/android/build/intermediates/local_only_symbol_list/debug/parseDebugLocalResources/R-def.txt
@@ -0,0 +1,5 @@
+R_DEF: Internal format may change without notice
+local
+id webview
+layout bridge_layout_main
+string my_string
diff --git a/node_modules/@capacitor-community/speech-recognition/android/build/intermediates/manifest_merge_blame_file/debug/processDebugManifest/manifest-merger-blame-debug-report.txt b/node_modules/@capacitor-community/speech-recognition/android/build/intermediates/manifest_merge_blame_file/debug/processDebugManifest/manifest-merger-blame-debug-report.txt
new file mode 100644
index 0000000..6010dd3
--- /dev/null
+++ b/node_modules/@capacitor-community/speech-recognition/android/build/intermediates/manifest_merge_blame_file/debug/processDebugManifest/manifest-merger-blame-debug-report.txt
@@ -0,0 +1,21 @@
+1<?xml version="1.0" encoding="utf-8"?>
+2<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+3 package="com.getcapacitor.community.speechrecognition.speechrecognition" >
+4
+5 <uses-sdk android:minSdkVersion="28" />
+6
+7 <uses-permission android:name="android.permission.RECORD_AUDIO" />
+7-->/Users/mohamad-macbook-air/workspace/temp/dt-frontend-worktrees/support-audio-input/node_modules/@capacitor-community/speech-recognition/android/src/main/AndroidManifest.xml:4:5-71
+7-->/Users/mohamad-macbook-air/workspace/temp/dt-frontend-worktrees/support-audio-input/node_modules/@capacitor-community/speech-recognition/android/src/main/AndroidManifest.xml:4:22-68
+8
+9 <queries>
+9-->/Users/mohamad-macbook-air/workspace/temp/dt-frontend-worktrees/support-audio-input/node_modules/@capacitor-community/speech-recognition/android/src/main/AndroidManifest.xml:5:5-9:15
+10 <intent>
+10-->/Users/mohamad-macbook-air/workspace/temp/dt-frontend-worktrees/support-audio-input/node_modules/@capacitor-community/speech-recognition/android/src/main/AndroidManifest.xml:6:7-8:16
+11 <action android:name="android.speech.RecognitionService" />
+11-->/Users/mohamad-macbook-air/workspace/temp/dt-frontend-worktrees/support-audio-input/node_modules/@capacitor-community/speech-recognition/android/src/main/AndroidManifest.xml:7:9-68
+11-->/Users/mohamad-macbook-air/workspace/temp/dt-frontend-worktrees/support-audio-input/node_modules/@capacitor-community/speech-recognition/android/src/main/AndroidManifest.xml:7:17-65
+12 </intent>
+13 </queries>
+14
+15</manifest>
diff --git a/node_modules/@capacitor-community/speech-recognition/android/build/intermediates/merged_manifest/debug/processDebugManifest/AndroidManifest.xml b/node_modules/@capacitor-community/speech-recognition/android/build/intermediates/merged_manifest/debug/processDebugManifest/AndroidManifest.xml
new file mode 100644
index 0000000..b81ecea
--- /dev/null
+++ b/node_modules/@capacitor-community/speech-recognition/android/build/intermediates/merged_manifest/debug/processDebugManifest/AndroidManifest.xml
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.getcapacitor.community.speechrecognition.speechrecognition" >
+
+ <uses-sdk android:minSdkVersion="28" />
+
+ <uses-permission android:name="android.permission.RECORD_AUDIO" />
+
+ <queries>
+ <intent>
+ <action android:name="android.speech.RecognitionService" />
+ </intent>
+ </queries>
+
+</manifest>
\ No newline at end of file
diff --git a/node_modules/@capacitor-community/speech-recognition/android/build/intermediates/navigation_json/debug/extractDeepLinksDebug/navigation.json b/node_modules/@capacitor-community/speech-recognition/android/build/intermediates/navigation_json/debug/extractDeepLinksDebug/navigation.json
new file mode 100644
index 0000000..0637a08
--- /dev/null
+++ b/node_modules/@capacitor-community/speech-recognition/android/build/intermediates/navigation_json/debug/extractDeepLinksDebug/navigation.json
@@ -0,0 +1 @@
+[]
\ No newline at end of file
diff --git a/node_modules/@capacitor-community/speech-recognition/android/build/intermediates/nested_resources_validation_report/debug/generateDebugResources/nestedResourcesValidationReport.txt b/node_modules/@capacitor-community/speech-recognition/android/build/intermediates/nested_resources_validation_report/debug/generateDebugResources/nestedResourcesValidationReport.txt
new file mode 100644
index 0000000..08f4ebe
--- /dev/null
+++ b/node_modules/@capacitor-community/speech-recognition/android/build/intermediates/nested_resources_validation_report/debug/generateDebugResources/nestedResourcesValidationReport.txt
@@ -0,0 +1 @@
+0 Warning/Error
\ No newline at end of file
diff --git a/node_modules/@capacitor-community/speech-recognition/android/build/intermediates/packaged_res/debug/packageDebugResources/layout/bridge_layout_main.xml b/node_modules/@capacitor-community/speech-recognition/android/build/intermediates/packaged_res/debug/packageDebugResources/layout/bridge_layout_main.xml
new file mode 100644
index 0000000..56fec15
--- /dev/null
+++ b/node_modules/@capacitor-community/speech-recognition/android/build/intermediates/packaged_res/debug/packageDebugResources/layout/bridge_layout_main.xml
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="utf-8"?>
+<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ tools:context="com.getcapacitor.BridgeActivity"
+ >
+
+ <WebView
+ android:id="@+id/webview"
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent" />
+
+</androidx.coordinatorlayout.widget.CoordinatorLayout>
diff --git a/node_modules/@capacitor-community/speech-recognition/android/build/intermediates/packaged_res/debug/packageDebugResources/values/values.xml b/node_modules/@capacitor-community/speech-recognition/android/build/intermediates/packaged_res/debug/packageDebugResources/values/values.xml
new file mode 100644
index 0000000..e95bf66
--- /dev/null
+++ b/node_modules/@capacitor-community/speech-recognition/android/build/intermediates/packaged_res/debug/packageDebugResources/values/values.xml
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="my_string">Just a simple string</string>
+</resources>
\ No newline at end of file
diff --git a/node_modules/@capacitor-community/speech-recognition/android/build/intermediates/runtime_library_classes_dir/debug/bundleLibRuntimeToDirDebug/com/getcapacitor/community/speechrecognition/Constants.class b/node_modules/@capacitor-community/speech-recognition/android/build/intermediates/runtime_library_classes_dir/debug/bundleLibRuntimeToDirDebug/com/getcapacitor/community/speechrecognition/Constants.class
new file mode 100644
index 0000000..3094bd9
Binary files /dev/null and b/node_modules/@capacitor-community/speech-recognition/android/build/intermediates/runtime_library_classes_dir/debug/bundleLibRuntimeToDirDebug/com/getcapacitor/community/speechrecognition/Constants.class differ
diff --git a/node_modules/@capacitor-community/speech-recognition/android/build/intermediates/runtime_library_classes_dir/debug/bundleLibRuntimeToDirDebug/com/getcapacitor/community/speechrecognition/Receiver.class b/node_modules/@capacitor-community/speech-recognition/android/build/intermediates/runtime_library_classes_dir/debug/bundleLibRuntimeToDirDebug/com/getcapacitor/community/speechrecognition/Receiver.class
new file mode 100644
index 0000000..af9be6c
Binary files /dev/null and b/node_modules/@capacitor-community/speech-recognition/android/build/intermediates/runtime_library_classes_dir/debug/bundleLibRuntimeToDirDebug/com/getcapacitor/community/speechrecognition/Receiver.class differ
diff --git a/node_modules/@capacitor-community/speech-recognition/android/build/intermediates/runtime_library_classes_dir/debug/bundleLibRuntimeToDirDebug/com/getcapacitor/community/speechrecognition/SpeechRecognition$SpeechRecognitionListener.class b/node_modules/@capacitor-community/speech-recognition/android/build/intermediates/runtime_library_classes_dir/debug/bundleLibRuntimeToDirDebug/com/getcapacitor/community/speechrecognition/SpeechRecognition$SpeechRecognitionListener.class
new file mode 100644
index 0000000..a056329
Binary files /dev/null and b/node_modules/@capacitor-community/speech-recognition/android/build/intermediates/runtime_library_classes_dir/debug/bundleLibRuntimeToDirDebug/com/getcapacitor/community/speechrecognition/SpeechRecognition$SpeechRecognitionListener.class differ
diff --git a/node_modules/@capacitor-community/speech-recognition/android/build/intermediates/runtime_library_classes_dir/debug/bundleLibRuntimeToDirDebug/com/getcapacitor/community/speechrecognition/SpeechRecognition.class b/node_modules/@capacitor-community/speech-recognition/android/build/intermediates/runtime_library_classes_dir/debug/bundleLibRuntimeToDirDebug/com/getcapacitor/community/speechrecognition/SpeechRecognition.class
new file mode 100644
index 0000000..57a50fe
Binary files /dev/null and b/node_modules/@capacitor-community/speech-recognition/android/build/intermediates/runtime_library_classes_dir/debug/bundleLibRuntimeToDirDebug/com/getcapacitor/community/speechrecognition/SpeechRecognition.class differ
diff --git a/node_modules/@capacitor-community/speech-recognition/android/build/intermediates/runtime_library_classes_jar/debug/bundleLibRuntimeToJarDebug/classes.jar b/node_modules/@capacitor-community/speech-recognition/android/build/intermediates/runtime_library_classes_jar/debug/bundleLibRuntimeToJarDebug/classes.jar
new file mode 100644
index 0000000..c701569
Binary files /dev/null and b/node_modules/@capacitor-community/speech-recognition/android/build/intermediates/runtime_library_classes_jar/debug/bundleLibRuntimeToJarDebug/classes.jar differ
diff --git a/node_modules/@capacitor-community/speech-recognition/android/build/intermediates/symbol_list_with_package_name/debug/generateDebugRFile/package-aware-r.txt b/node_modules/@capacitor-community/speech-recognition/android/build/intermediates/symbol_list_with_package_name/debug/generateDebugRFile/package-aware-r.txt
new file mode 100644
index 0000000..f0ee474
--- /dev/null
+++ b/node_modules/@capacitor-community/speech-recognition/android/build/intermediates/symbol_list_with_package_name/debug/generateDebugRFile/package-aware-r.txt
@@ -0,0 +1,4 @@
+com.getcapacitor.community.speechrecognition.speechrecognition
+id webview
+layout bridge_layout_main
+string my_string
diff --git a/node_modules/@capacitor-community/speech-recognition/android/build/outputs/logs/manifest-merger-debug-report.txt b/node_modules/@capacitor-community/speech-recognition/android/build/outputs/logs/manifest-merger-debug-report.txt
new file mode 100644
index 0000000..102bc47
--- /dev/null
+++ b/node_modules/@capacitor-community/speech-recognition/android/build/outputs/logs/manifest-merger-debug-report.txt
@@ -0,0 +1,28 @@
+-- Merging decision tree log ---
+manifest
+ADDED from /Users/mohamad-macbook-air/workspace/temp/dt-frontend-worktrees/support-audio-input/node_modules/@capacitor-community/speech-recognition/android/src/main/AndroidManifest.xml:2:3-10:14
+INJECTED from /Users/mohamad-macbook-air/workspace/temp/dt-frontend-worktrees/support-audio-input/node_modules/@capacitor-community/speech-recognition/android/src/main/AndroidManifest.xml:2:3-10:14
+ package
+ INJECTED from /Users/mohamad-macbook-air/workspace/temp/dt-frontend-worktrees/support-audio-input/node_modules/@capacitor-community/speech-recognition/android/src/main/AndroidManifest.xml
+ xmlns:android
+ ADDED from /Users/mohamad-macbook-air/workspace/temp/dt-frontend-worktrees/support-audio-input/node_modules/@capacitor-community/speech-recognition/android/src/main/AndroidManifest.xml:2:13-71
+uses-permission#android.permission.RECORD_AUDIO
+ADDED from /Users/mohamad-macbook-air/workspace/temp/dt-frontend-worktrees/support-audio-input/node_modules/@capacitor-community/speech-recognition/android/src/main/AndroidManifest.xml:4:5-71
+ android:name
+ ADDED from /Users/mohamad-macbook-air/workspace/temp/dt-frontend-worktrees/support-audio-input/node_modules/@capacitor-community/speech-recognition/android/src/main/AndroidManifest.xml:4:22-68
+queries
+ADDED from /Users/mohamad-macbook-air/workspace/temp/dt-frontend-worktrees/support-audio-input/node_modules/@capacitor-community/speech-recognition/android/src/main/AndroidManifest.xml:5:5-9:15
+intent#action:name:android.speech.RecognitionService
+ADDED from /Users/mohamad-macbook-air/workspace/temp/dt-frontend-worktrees/support-audio-input/node_modules/@capacitor-community/speech-recognition/android/src/main/AndroidManifest.xml:6:7-8:16
+action#android.speech.RecognitionService
+ADDED from /Users/mohamad-macbook-air/workspace/temp/dt-frontend-worktrees/support-audio-input/node_modules/@capacitor-community/speech-recognition/android/src/main/AndroidManifest.xml:7:9-68
+ android:name
+ ADDED from /Users/mohamad-macbook-air/workspace/temp/dt-frontend-worktrees/support-audio-input/node_modules/@capacitor-community/speech-recognition/android/src/main/AndroidManifest.xml:7:17-65
+uses-sdk
+INJECTED from /Users/mohamad-macbook-air/workspace/temp/dt-frontend-worktrees/support-audio-input/node_modules/@capacitor-community/speech-recognition/android/src/main/AndroidManifest.xml reason: use-sdk injection requested
+INJECTED from /Users/mohamad-macbook-air/workspace/temp/dt-frontend-worktrees/support-audio-input/node_modules/@capacitor-community/speech-recognition/android/src/main/AndroidManifest.xml
+INJECTED from /Users/mohamad-macbook-air/workspace/temp/dt-frontend-worktrees/support-audio-input/node_modules/@capacitor-community/speech-recognition/android/src/main/AndroidManifest.xml
+ android:targetSdkVersion
+ INJECTED from /Users/mohamad-macbook-air/workspace/temp/dt-frontend-worktrees/support-audio-input/node_modules/@capacitor-community/speech-recognition/android/src/main/AndroidManifest.xml
+ android:minSdkVersion
+ INJECTED from /Users/mohamad-macbook-air/workspace/temp/dt-frontend-worktrees/support-audio-input/node_modules/@capacitor-community/speech-recognition/android/src/main/AndroidManifest.xml
diff --git a/node_modules/@capacitor-community/speech-recognition/android/build/tmp/compileDebugJavaWithJavac/compileTransaction/stash-dir/SpeechRecognition$SpeechRecognitionListener.class.uniqueId1 b/node_modules/@capacitor-community/speech-recognition/android/build/tmp/compileDebugJavaWithJavac/compileTransaction/stash-dir/SpeechRecognition$SpeechRecognitionListener.class.uniqueId1
new file mode 100644
index 0000000..dac1e83
Binary files /dev/null and b/node_modules/@capacitor-community/speech-recognition/android/build/tmp/compileDebugJavaWithJavac/compileTransaction/stash-dir/SpeechRecognition$SpeechRecognitionListener.class.uniqueId1 differ
diff --git a/node_modules/@capacitor-community/speech-recognition/android/build/tmp/compileDebugJavaWithJavac/compileTransaction/stash-dir/SpeechRecognition.class.uniqueId0 b/node_modules/@capacitor-community/speech-recognition/android/build/tmp/compileDebugJavaWithJavac/compileTransaction/stash-dir/SpeechRecognition.class.uniqueId0
new file mode 100644
index 0000000..836f0ad
Binary files /dev/null and b/node_modules/@capacitor-community/speech-recognition/android/build/tmp/compileDebugJavaWithJavac/compileTransaction/stash-dir/SpeechRecognition.class.uniqueId0 differ
diff --git a/node_modules/@capacitor-community/speech-recognition/android/build/tmp/compileDebugJavaWithJavac/previous-compilation-data.bin b/node_modules/@capacitor-community/speech-recognition/android/build/tmp/compileDebugJavaWithJavac/previous-compilation-data.bin
new file mode 100644
index 0000000..c57ddd3
Binary files /dev/null and b/node_modules/@capacitor-community/speech-recognition/android/build/tmp/compileDebugJavaWithJavac/previous-compilation-data.bin differ
diff --git a/node_modules/@capacitor-community/speech-recognition/android/src/main/java/com/getcapacitor/community/speechrecognition/SpeechRecognition.java b/node_modules/@capacitor-community/speech-recognition/android/src/main/java/com/getcapacitor/community/speechrecognition/SpeechRecognition.java
index a99ea69..5cabbd7 100644
--- a/node_modules/@capacitor-community/speech-recognition/android/src/main/java/com/getcapacitor/community/speechrecognition/SpeechRecognition.java
+++ b/node_modules/@capacitor-community/speech-recognition/android/src/main/java/com/getcapacitor/community/speechrecognition/SpeechRecognition.java
@@ -81,13 +81,18 @@ public class SpeechRecognition extends Plugin implements Constants {
String prompt = call.getString("prompt", null);
boolean partialResults = call.getBoolean("partialResults", false);
boolean popup = call.getBoolean("popup", false);
- beginListening(language, maxResults, prompt, partialResults, popup, call);
+ JSArray contextualStrings = call.getArray("contextualStrings", new JSArray());
+ beginListening(language, maxResults, prompt, partialResults, popup, contextualStrings, call);
}
@PluginMethod
public void stop(final PluginCall call) {
try {
stopListening();
+ // This never resolved the call on success, only rejected on
+ // exception — any caller doing `await stop()` (e.g. a JS-side
+ // restart loop) would hang forever waiting on this promise.
+ call.resolve();
} catch (Exception ex) {
call.reject(ex.getLocalizedMessage());
}
@@ -157,6 +162,7 @@ public class SpeechRecognition extends Plugin implements Constants {
String prompt,
final boolean partialResults,
boolean showPopup,
+ JSArray contextualStrings,
PluginCall call
) {
Logger.info(getLogTag(), "Beginning to listen for audible speech");
@@ -168,6 +174,19 @@ public class SpeechRecognition extends Plugin implements Constants {
intent.putExtra(RecognizerIntent.EXTRA_CALLING_PACKAGE, bridge.getActivity().getPackageName());
intent.putExtra(RecognizerIntent.EXTRA_PARTIAL_RESULTS, partialResults);
intent.putExtra("android.speech.extra.DICTATION_MODE", partialResults);
+ intent.putExtra(RecognizerIntent.EXTRA_PREFER_OFFLINE, true);
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && contextualStrings != null && contextualStrings.length() > 0) {
+ ArrayList<String> biasingStrings = new ArrayList<>();
+ for (int i = 0; i < contextualStrings.length(); i++) {
+ String value = contextualStrings.optString(i, null);
+ if (value != null && !value.trim().isEmpty()) {
+ biasingStrings.add(value);
+ }
+ }
+ if (!biasingStrings.isEmpty()) {
+ intent.putStringArrayListExtra(RecognizerIntent.EXTRA_BIASING_STRINGS, biasingStrings);
+ }
+ }
if (prompt != null) {
intent.putExtra(RecognizerIntent.EXTRA_PROMPT, prompt);
@@ -283,6 +302,25 @@ public class SpeechRecognition extends Plugin implements Constants {
SpeechRecognition.this.stopListening();
String errorMssg = getErrorText(error);
+ // Unlike onEndOfSpeech, this previously never told the JS side the
+ // session ended — silence commonly ends a session via ERROR_SPEECH_TIMEOUT
+ // / ERROR_NO_MATCH rather than onEndOfSpeech, so without this the caller's
+ // restart loop never fires and listening state gets stuck forever.
+ bridge
+ .getWebView()
+ .post(() -> {
+ try {
+ SpeechRecognition.this.lock.lock();
+ SpeechRecognition.this.listening(false);
+
+ JSObject ret = new JSObject();
+ ret.put("status", "stopped");
+ SpeechRecognition.this.notifyListeners(LISTENING_EVENT, ret);
+ } finally {
+ SpeechRecognition.this.lock.unlock();
+ }
+ });
+
if (this.call != null) {
call.reject(errorMssg);
}
diff --git a/node_modules/@capacitor-community/speech-recognition/ios/Plugin/Plugin.swift b/node_modules/@capacitor-community/speech-recognition/ios/Plugin/Plugin.swift
index 5d1b35b..899c7a6 100644
--- a/node_modules/@capacitor-community/speech-recognition/ios/Plugin/Plugin.swift
+++ b/node_modules/@capacitor-community/speech-recognition/ios/Plugin/Plugin.swift
@@ -54,6 +54,7 @@ public class SpeechRecognition: CAPPlugin {
let language: String = call.getString("language") ?? "en-US"
let maxResults: Int = call.getInt("maxResults") ?? self.defaultMatches
let partialResults: Bool = call.getBool("partialResults") ?? false
+ let contextualStrings: [String] = call.getArray("contextualStrings", String.self) ?? []
if self.recognitionTask != nil {
self.recognitionTask?.cancel()
@@ -79,6 +80,12 @@ public class SpeechRecognition: CAPPlugin {
self.recognitionRequest = SFSpeechAudioBufferRecognitionRequest()
self.recognitionRequest?.shouldReportPartialResults = partialResults
+ if !contextualStrings.isEmpty {
+ self.recognitionRequest?.contextualStrings = contextualStrings
+ }
+ if #available(iOS 13, *), self.speechRecognizer?.supportsOnDeviceRecognition == true {
+ self.recognitionRequest?.requiresOnDeviceRecognition = true
+ }
let inputNode: AVAudioInputNode = self.audioEngine!.inputNode
let format: AVAudioFormat = inputNode.outputFormat(forBus: 0)

View File

@@ -1,6 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
tailwindcss: {},
},
}

13
prettier.config.mjs Normal file
View File

@@ -0,0 +1,13 @@
export default {
arrowParens: 'avoid',
bracketSpacing: true,
endOfLine: 'auto',
jsxBracketSameLine: false,
jsxSingleQuote: true,
plugins: ['prettier-plugin-tailwindcss'],
printWidth: 80,
semi: false,
singleQuote: true,
tabWidth: 2,
trailingComma: 'all',
}

View File

@@ -0,0 +1,11 @@
{
"applinks": {
"apps": [],
"details": [
{
"appID": "6UJJ78R3BS.com.donetick.app",
"paths": ["/circle/join*"]
}
]
}
}

View File

@@ -0,0 +1,11 @@
{
"applinks": {
"apps": [],
"details": [
{
"appID": "6UJJ78R3BS.com.donetick.app",
"paths": ["/circle/join*"]
}
]
}
}

View File

@@ -0,0 +1,13 @@
[
{
"relation": ["delegate_permission/common.handle_all_urls"],
"target": {
"namespace": "android_app",
"package_name": "com.donetick.app",
"sha256_cert_fingerprints": [
"EF:45:27:40:A2:D2:11:E4:27:AB:9A:7A:C6:E1:3B:CA:D4:DE:6A:0A:C3:81:05:58:D8:89:F1:FA:4E:CB:44:F3",
"67:0B:E5:30:FB:8A:7F:E6:9A:54:51:7F:06:AA:B0:1D:1A:26:61:5B:2A:60:53:4A:31:75:72:DA:F6:FA:EC:5A"
]
}
}
]

8
public/_headers Normal file
View File

@@ -0,0 +1,8 @@
/.well-known/apple-app-site-association
Content-Type: application/json
/.well-known/apple-app-site-association.json
Content-Type: application/json
/.well-known/assetlinks.json
Content-Type: application/json

1
public/_redirects Normal file
View File

@@ -0,0 +1 @@
/.well-known/apple-app-site-association /.well-known/apple-app-site-association.json 200

View File

@@ -0,0 +1,75 @@
{
"title": "المهام",
"myChores": "مهامي",
"allChores": "جميع المهام",
"addChore": "إضافة مهمة",
"editChore": "تعديل مهمة",
"deleteChore": "حذف مهمة",
"completeChore": "إكمال مهمة",
"dueDate": "تاريخ الاستحقاق",
"assignedTo": "مُسند إلى",
"priority": "الأولوية",
"status": "الحالة",
"description": "الوصف",
"choreView": {
"assignment": "التعيين",
"assigned": "المعين",
"last": "الأخير",
"schedule": "الجدول",
"due": "الاستحقاق",
"statistics": "الإحصائيات",
"completed": "مكتمل",
"times": "مرات",
"details": "التفاصيل",
"createdBy": "أنشئ بواسطة",
"na": "غير متوفر",
"taskCompleted": "المهمة مكتملة",
"taskCompletedMessage": "تم وضع علامة مكتملة على مهمتك",
"taskCompletionUndone": "تم التراجع عن إكمال المهمة.",
"taskSkipUndone": "تم التراجع عن تخطي المهمة.",
"undoSuccessful": "التراجع ناجح",
"undoFailed": "فشل التراجع",
"undoFailedMessage": "تعذر التراجع عن الإجراء. يرجى المحاولة مرة أخرى.",
"resetTimer": "إعادة تعيين المؤقت",
"resetTimerConfirmation": "هل أنت متأكد من أنك تريد إعادة تعيين المؤقت؟ سيؤدي هذا إلى مسح جميع سجلات الوقت منذ بدء المهمة.",
"clearAllTimeRecords": "مسح جميع سجلات الوقت",
"clearAllTimeConfirmation": "سيؤدي هذا إلى حذف جميع المؤقتات لهذه المهمة بشكل دائم وإعادتها إلى حالة \"لم تبدأ\".",
"descriptionTitle": "الوصف",
"description": "الوصف :",
"previousNote": "الملاحظة السابقة",
"previousNoteLabel": "الملاحظة السابقة:",
"subtasksLabel": "المهام الفرعية :",
"taskActions": "إجراءات المهمة",
"addNote": "أضف ملاحظة",
"additionalNotes": "ملاحظات إضافية:",
"notePlaceholder": "أضف ملاحظة حول الإكمال...",
"setCustomCompletionTime": "تعيين وقت إكمال مخصص",
"skipTask": "تخطي المهمة",
"skipTaskConfirmation": "هل أنت متأكد من أنك تريد تخطي هذه المهمة؟",
"markComplete": "وضع علامة مكتمل",
"markAsDone": "وضع علامة كمنجز",
"edit": "تعديل",
"archive": "أرشفة",
"unarchive": "إلغاء الأرشفة",
"viewHistory": "عرض السجل",
"history": "السجل",
"startTimer": "بدء المؤقت",
"start": "بدء",
"pauseTimer": "إيقاف المؤقت مؤقتاً",
"approve": "موافقة",
"reject": "رفض",
"pendingApproval": "في انتظار الموافقة",
"undo": "تراجع",
"skip": "تخطي",
"cancel": "إلغاء",
"noPriority": "بدون أولوية",
"subtasks": "المهام الفرعية",
"noDescription": "لا يوجد وصف متاح",
"timer": {
"active": "المؤقت نشط",
"paused": "المؤقت متوقف مؤقتاً",
"reset": "إعادة تعيين المؤقت",
"delete": "حذف الجلسة"
}
}
}

Some files were not shown because too many files have changed in this diff Show More