57 Commits

Author SHA1 Message Date
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
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
250 changed files with 22345 additions and 8770 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:
@@ -19,3 +18,4 @@ jobs:
- 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

2
.prettierignore Normal file
View File

@@ -0,0 +1,2 @@
LICENSE.md
dist/

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

@@ -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 59
versionName "1.2.38"
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,10 +9,13 @@ 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')

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"
@@ -37,6 +38,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 +125,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')

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,

187
eslint.config.mjs Normal file
View File

@@ -0,0 +1,187 @@
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'],
},
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,ts}'],
languageOptions: {
globals: globals.node,
},
},
{
files: ['*.config.{mjs,ts}'],
languageOptions: {
ecmaVersion: 'latest',
sourceType: 'module',
},
rules: {
eqeqeq: 'error',
indent: ['error', 2],
'space-infix-ops': ['error', { int32Hint: false }],
},
},
{
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

@@ -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 = 59;
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.38;
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 = 59;
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.38;
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 = 59;
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.38;
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 = 59;
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.38;
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,10 @@
<dict>
<key>aps-environment</key>
<string>development</string>
<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,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,10 +11,13 @@ 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'

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,18 @@ PODS:
- Capacitor
- CapacitorPreferences (8.0.1):
- Capacitor
- CapacitorPushNotifications (8.1.1):
- CapacitorPushNotifications (8.1.2):
- Capacitor
- CapacitorStatusBar (8.0.2):
- CapacitorStatusBar (8.0.3):
- Capacitor
- CapgoCapacitorDocumentScanner (8.4.0):
- 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,9 +145,12 @@ 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`)"
@@ -172,10 +171,6 @@ SPEC REPOS:
- Alamofire
- AppAuth
- AppCheckCore
- FBAEMKit
- FBSDKCoreKit
- FBSDKCoreKit_Basics
- FBSDKLoginKit
- FirebaseCore
- FirebaseCoreInternal
- FirebaseInstallations
@@ -203,12 +198,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:
@@ -240,26 +241,25 @@ 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
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 +283,6 @@ SPEC CHECKSUMS:
SQLCipher: eb79c64049cb002b4e9fcb30edb7979bf4706dfc
ZIPFoundation: dfd3d681c4053ff7e2f7350bc4e53b5dba3f5351
PODFILE CHECKSUM: 21b805bdbbb6ac4b8a3527ee8ef346ba0a55ef65
PODFILE CHECKSUM: 028fdeb50d56158db0a97459bd577ed700f03c0c
COCOAPODS: 1.16.2

4245
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.38",
"type": "module",
"engines": {
"node": ">=20.0.0",
@@ -20,7 +20,9 @@
"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 .",
"lint:ci": "true # TODO: eslint -f gha && prettier -c .",
"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,15 +37,19 @@
"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",
@@ -53,7 +59,7 @@
"@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",
@@ -68,8 +74,11 @@
"@openreplay/tracker": "^14.0.4",
"@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",
@@ -113,19 +122,20 @@
"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",
"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,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": "حذف الجلسة"
}
}
}

View File

@@ -0,0 +1,33 @@
{
"save": "حفظ",
"cancel": "إلغاء",
"delete": "حذف",
"edit": "تعديل",
"close": "إغلاق",
"confirm": "تأكيد",
"loading": "جارٍ التحميل...",
"error": "خطأ",
"success": "نجح",
"warning": "تحذير",
"refresh": "تحديث",
"copy": "نسخ",
"copied": "تم النسخ!",
"settings": "الإعدادات",
"yes": "نعم",
"no": "لا",
"back": "رجوع",
"backToCalendar": "العودة إلى التقويم",
"logout": "تسجيل الخروج",
"version": "النسخة",
"navigation": {
"allTasks": "جميع المهام",
"archived": "المؤرشفة",
"things": "الأشياء",
"labels": "التسميات",
"projects": "المشاريع",
"filters": "الفلاتر",
"activities": "الأنشطة",
"points": "النقاط",
"settings": "الإعدادات"
}
}

View File

@@ -0,0 +1,174 @@
{
"title": "الإعدادات",
"circleSettings": {
"title": "إعدادات الدائرة",
"description": "يتم ربط حسابك تلقائيًا بدائرة عند إنشاء واحدة أو الانضمام إليها. ادعُ الأصدقاء بسهولة من خلال مشاركة رمز الدائرة الفريد أو الرابط أدناه. ستتلقى إشعارًا أدناه عندما يطلب شخص ما الانضمام إلى دائرتك. إذا كنت ترغب في المغادرة، فما عليك سوى الضغط على زر 'مغادرة الدائرة'.",
"circleCode": "رمز الدائرة",
"copyCode": "نسخ الرمز",
"copyLink": "نسخ الرابط",
"codeCopied": "تم نسخ رمز الدائرة!",
"linkCopied": "تم نسخ الرابط!",
"joinCircle": "الانضمام إلى دائرة",
"joinCirclePlaceholder": "أدخل رمز الدائرة",
"join": "انضمام",
"leave": "مغادرة الدائرة",
"leaveConfirmTitle": "مغادرة الدائرة",
"leaveConfirmMessage": "هل أنت متأكد من أنك تريد مغادرة هذه الدائرة؟",
"circleMembers": "أعضاء الدائرة",
"circleMemberRequests": "طلبات انضمام الأعضاء",
"admin": "مشرف",
"member": "عضو",
"pending": "قيد الانتظار",
"accept": "قبول",
"reject": "رفض",
"makeAdmin": "جعله مشرف",
"makeMember": "جعله عضو",
"remove": "إزالة",
"webhookURL": "رابط Webhook",
"webhookDescription": "أدخل رابط webhook لتلقي إشعارات أحداث الدائرة",
"webhookPlaceholder": "https://your-webhook-url.com"
},
"accountSettings": {
"title": "إعدادات الحساب",
"subscription": "الاشتراك",
"subscriptionStatus": "الخطة الحالية",
"free": "مجاني",
"plus": "بلس",
"upgrade": "ترقية",
"cancel": "إلغاء",
"changePassword": "تغيير كلمة المرور",
"password": "كلمة المرور",
"dangerZone": "منطقة الخطر",
"dangerZoneDescription": "بمجرد حذف حسابك، لا يمكن التراجع. يرجى التأكد.",
"deleteAccount": "حذف الحساب"
},
"localization": {
"title": "التوطين",
"description": "تخصيص اللغة وتنسيق التاريخ والتفضيلات الإقليمية لحسابك.",
"language": "اللغة",
"languageDescription": "اختر لغتك المفضلة",
"dateFormat": "تنسيق التاريخ",
"dateFormatDescription": "اختر كيفية عرض التواريخ في التطبيق",
"timeFormat": "تنسيق الوقت",
"timeFormatDescription": "اختر تنسيق 12 أو 24 ساعة",
"12hour": "12 ساعة (ص/م)",
"24hour": "24 ساعة",
"firstDayOfWeek": "أول يوم في الأسبوع",
"firstDayOfWeekDescription": "اختر اليوم الذي يبدأ به أسبوعك",
"sunday": "الأحد",
"monday": "الاثنين",
"saturday": "السبت",
"formats": {
"mdy": "MM/DD/YYYY (الولايات المتحدة)",
"dmy": "DD/MM/YYYY (أوروبا)",
"ymd": "YYYY-MM-DD (ISO)",
"long": "تنسيق طويل (مثل 1 يناير 2024)",
"short": "تنسيق قصير (مثل 1 يناير 2024)"
}
},
"sidepanel": {
"title": "تخصيص اللوحة الجانبية",
"description": "قم بتخصيص تخطيط ورؤية البطاقات في اللوحة الجانبية. هذا القسم متاح فقط على أجهزة الشاشة الكبيرة مثل الأجهزة اللوحية وأجهزة سطح المكتب."
},
"theme": {
"title": "تفضيلات المظهر",
"description": "اختر كيف يبدو الموقع لك. حدد مظهرًا واحدًا أو قم بالمزامنة مع نظامك والتبديل تلقائيًا بين مظاهر النهار والليل.",
"themeMode": "وضع المظهر",
"light": "فاتح",
"dark": "داكن",
"system": "النظام"
},
"notifications": {
"settingsSaved": "تم حفظ الإعدادات بنجاح",
"settingsSaveFailed": "فشل حفظ الإعدادات",
"invalidWebhook": "رابط webhook غير صالح"
},
"profile": {
"title": "إعدادات الملف الشخصي",
"description": "تحديث اسم العرض وصورة الملف الشخصي.",
"photoUpdated": "تم تحديث الصورة",
"photoUpdatedMessage": "تم تحديث صورة ملفك الشخصي بنجاح!",
"uploadFailed": "فشل التحميل",
"uploadFailedMessage": "فشل تحميل صورتك. يرجى المحاولة مرة أخرى.",
"profileUpdated": "تم تحديث الملف الشخصي",
"profileUpdatedMessage": "تم حفظ معلومات ملفك الشخصي بنجاح!",
"updateFailed": "فشل التحديث",
"updateFailedMessage": "تعذر تحديث ملفك الشخصي. يرجى التحقق من اتصالك والمحاولة مرة أخرى.",
"changePhoto": "تغيير الصورة",
"displayName": "اسم العرض",
"displayNamePlaceholder": "أدخل اسم العرض الخاص بك",
"timezone": "المنطقة الزمنية",
"timezonePlaceholder": "اختر منطقتك الزمنية",
"save": "حفظ",
"cancel": "إلغاء"
},
"overview": {
"title": "الإعدادات",
"subtitle": "قم بتخصيص تجربتك وإدارة تفضيلات حسابك",
"upgrade": {
"title": "الترقية إلى بلس",
"description": "افتح ميزات قوية لتعزيز إنتاجيتك",
"button": "الترقية الآن",
"features": {
"richText": "أوصاف نصية منسقة",
"notifications": "إشعارات المهام",
"apiIntegrations": "تكاملات API",
"advancedAutomation": "أتمتة متقدمة"
}
},
"sections": {
"profile": {
"title": "إعدادات الملف الشخصي",
"description": "تحديث معلومات ملفك الشخصي والصورة واسم العرض وتفضيلات المنطقة الزمنية."
},
"circle": {
"title": "إعدادات الدائرة",
"description": "إدارة دائرتك ودعوة الأعضاء والتعامل مع طلبات الانضمام."
},
"account": {
"title": "إعدادات الحساب",
"description": "إدارة اشتراكك وتغيير كلمة المرور وخيارات حذف الحساب."
},
"subaccounts": {
"title": "الحسابات المُدارة",
"description": "إنشاء وإدارة حسابات فرعية لتسجيل الدخول وإكمال المهام المعينة."
},
"notifications": {
"title": "الإشعارات",
"description": "تكوين الإشعارات الفورية وتنبيهات البريد الإلكتروني ووجهات الإشعارات للمهام."
},
"mfa": {
"title": "المصادقة متعددة العوامل",
"description": "إضافة طبقة إضافية من الأمان باستخدام MFA مع تطبيقات المصادقة."
},
"apitokens": {
"title": "رموز API",
"description": "إنشاء وإدارة رموز الوصول لتكاملات الطرف الثالث والوصول إلى API."
},
"storage": {
"title": "إعدادات التخزين",
"description": "نسخ احتياطي واستعادة بياناتك وإدارة التخزين المحلي وتفضيلات المزامنة."
},
"sidepanel": {
"title": "تخصيص اللوحة الجانبية",
"description": "قم بتخصيص تخطيط ورؤية البطاقات في واجهة اللوحة الجانبية."
},
"theme": {
"title": "تفضيلات المظهر",
"description": "اختر مظهرك المفضل وقم بتكوين إعدادات الوضع الداكن/الفاتح."
},
"localization": {
"title": "التوطين",
"description": "تخصيص اللغة وتنسيق التاريخ وتنسيق الوقت والتفضيلات الإقليمية."
},
"advanced": {
"title": "الإعدادات المتقدمة",
"description": "تكوين webhooks والتحديثات في الوقت الفعلي وميزات متقدمة أخرى لتعزيز الإنتاجية."
},
"developer": {
"title": "إعدادات المطور",
"description": "عرض المعلومات الفنية حول رموز المصادقة واتصالات SSE وبيانات التصحيح."
}
}
}
}

View File

@@ -29,5 +29,51 @@
"activities": "الأنشطة",
"points": "النقاط",
"settings": "الإعدادات"
},
"feedback": {
"later": "Maybe later",
"sentiment": {
"title": "How's Donetick working for you?",
"subtitle": "Your answer helps us decide what to build next.",
"options": {
"love": "Love it",
"okay": "It's okay",
"issues": "Having issues"
}
},
"categories": {
"bugs": "Bugs",
"missingFeature": "Missing feature",
"tooComplicated": "Too complicated",
"slow": "Slow",
"notifications": "Notifications",
"ai": "AI",
"other": "Other"
},
"details": {
"title": "What could we improve?",
"messageLabel": "Tell us more",
"messagePlaceholder": "What happened, or what would make this better?",
"contextNote": "We'll include your app version, device and platform so we can reproduce issues.",
"submit": "Send feedback",
"contextNoteSelfHosted": "You're on a self-hosted instance, so nothing is sent from your server — we'll open a pre-filled GitHub issue you can review and edit first.",
"submitSelfHosted": "Continue to GitHub"
},
"review": {
"title": "Glad you're enjoying it!",
"subtitle": "A rating or a star helps other people find Donetick.",
"github": "Star on GitHub",
"appStore": "Rate on the App Store",
"playStore": "Rate on Google Play"
},
"thanks": {
"title": "Thanks for the feedback",
"subtitle": "We read every response and it shapes what we work on next."
},
"github": {
"title": "Report it on GitHub",
"subtitle": "We've filled in an issue with your notes and version details. Nothing has been sent yet — review it and post when you're ready.",
"open": "Open the issue"
}
}
}

View File

@@ -168,6 +168,10 @@
"developer": {
"title": "إعدادات المطور",
"description": "عرض المعلومات الفنية حول رموز المصادقة واتصالات SSE وبيانات التصحيح."
},
"feedback": {
"title": "Send Feedback",
"description": "Tell us how Donetick is working for you, report a bug, or request a feature."
}
}
}

View File

@@ -33,7 +33,7 @@
"resetTimer": "Timer zurücksetzen",
"resetTimerConfirmation": "Bist du sicher, dass du den Timer zurücksetzen möchtest? Dies löscht alle Zeitaufzeichnungen seit du die Aufgabe gestartet hast.",
"clearAllTimeRecords": "Alle Zeitaufzeichnungen löschen",
"clearAllTimeConfirmation": "Dies löscht dauerhaft alle Timer für diese Aufgabe und setzt sie zurück auf \"nicht gestartet\".",
"clearAllTimeConfirmation": "Dies löscht dauerhaft alle Timer für diese Aufgabe und setzt sie zurück auf nicht gestartet.",
"descriptionTitle": "Beschreibung",
"description": "Beschreibung:",
"previousNote": "Vorherige Notiz",

View File

@@ -22,7 +22,7 @@
"navigation": {
"allTasks": "Alle Aufgaben",
"archived": "Archiviert",
"things": "Things",
"things": "Dinge",
"labels": "Beschriftungen",
"projects": "Projekte",
"filters": "Filter",

View File

@@ -168,7 +168,11 @@
"developer": {
"title": "Entwickler-Einstellungen",
"description": "Zeige technische Informationen über Authentifizierungs-Token, SSE-Verbindungen und Debug-Daten an."
},
"feedback": {
"title": "Send Feedback",
"description": "Tell us how Donetick is working for you, report a bug, or request a feature."
}
}
}
}
}

View File

@@ -29,5 +29,51 @@
"activities": "Activities",
"points": "Points",
"settings": "Settings"
},
"feedback": {
"later": "Maybe later",
"sentiment": {
"title": "How's Donetick working for you?",
"subtitle": "Your answer helps us decide what to build next.",
"options": {
"love": "Love it",
"okay": "It's okay",
"issues": "Having issues"
}
},
"categories": {
"bugs": "Bugs",
"missingFeature": "Missing feature",
"tooComplicated": "Too complicated",
"slow": "Slow",
"notifications": "Notifications",
"ai": "AI",
"other": "Other"
},
"details": {
"title": "What could we improve?",
"messageLabel": "Tell us more",
"messagePlaceholder": "What happened, or what would make this better?",
"contextNote": "We'll include your app version, device and platform so we can reproduce issues.",
"submit": "Send feedback",
"contextNoteSelfHosted": "You're on a self-hosted instance, so nothing is sent from your server — we'll open a pre-filled GitHub issue you can review and edit first.",
"submitSelfHosted": "Continue to GitHub"
},
"review": {
"title": "Glad you're enjoying it!",
"subtitle": "A rating or a star helps other people find Donetick.",
"github": "Star on GitHub",
"appStore": "Rate on the App Store",
"playStore": "Rate on Google Play"
},
"thanks": {
"title": "Thanks for the feedback",
"subtitle": "We read every response and it shapes what we work on next."
},
"github": {
"title": "Report it on GitHub",
"subtitle": "We've filled in an issue with your notes and version details. Nothing has been sent yet — review it and post when you're ready.",
"open": "Open the issue"
}
}
}

View File

@@ -168,6 +168,10 @@
"developer": {
"title": "Developer Settings",
"description": "View technical information about authentication tokens, SSE connections, and debug data."
},
"feedback": {
"title": "Send Feedback",
"description": "Tell us how Donetick is working for you, report a bug, or request a feature."
}
}
}

View File

@@ -0,0 +1,75 @@
{
"title": "Tareas",
"myChores": "Mis Tareas",
"allChores": "Todas las Tareas",
"addChore": "Agregar Tarea",
"editChore": "Editar Tarea",
"deleteChore": "Eliminar Tarea",
"completeChore": "Completar Tarea",
"dueDate": "Fecha de Vencimiento",
"assignedTo": "Asignado a",
"priority": "Prioridad",
"status": "Estado",
"description": "Descripción",
"choreView": {
"assignment": "Asignación",
"assigned": "Asignado",
"last": "Último",
"schedule": "Horario",
"due": "Vencimiento",
"statistics": "Estadísticas",
"completed": "Completado",
"times": "veces",
"details": "Detalles",
"createdBy": "Creado por",
"na": "N/D",
"taskCompleted": "Tarea Completada",
"taskCompletedMessage": "Tu tarea ha sido marcada como completada",
"taskCompletionUndone": "La finalización de la tarea ha sido deshecha.",
"taskSkipUndone": "El salto de tarea ha sido deshecho.",
"undoSuccessful": "Deshacer Exitoso",
"undoFailed": "Deshacer Fallido",
"undoFailedMessage": "No se pudo deshacer la acción. Por favor, inténtalo de nuevo.",
"resetTimer": "Reiniciar Temporizador",
"resetTimerConfirmation": "¿Estás seguro de que quieres reiniciar el temporizador? Esto borrará todos los registros de tiempo desde que iniciaste la tarea.",
"clearAllTimeRecords": "Borrar Todos los Registros de Tiempo",
"clearAllTimeConfirmation": "Esto eliminará permanentemente todos los temporizadores de esta tarea y la volverá al estado \"no iniciada\".",
"descriptionTitle": "Descripción",
"description": "Descripción :",
"previousNote": "Nota Anterior",
"previousNoteLabel": "Nota anterior:",
"subtasksLabel": "Subtareas :",
"taskActions": "Acciones de Tarea",
"addNote": "Agregar una nota",
"additionalNotes": "Notas Adicionales:",
"notePlaceholder": "Agregar una nota sobre la finalización...",
"setCustomCompletionTime": "Establecer hora de finalización personalizada",
"skipTask": "Saltar Tarea",
"skipTaskConfirmation": "¿Estás seguro de que quieres saltar esta tarea?",
"markComplete": "Marcar como Completada",
"markAsDone": "Marcar como hecha",
"edit": "Editar",
"archive": "Archivar",
"unarchive": "Desarchivar",
"viewHistory": "Ver Historial",
"history": "Historial",
"startTimer": "Iniciar Temporizador",
"start": "Iniciar",
"pauseTimer": "Pausar Temporizador",
"approve": "Aprobar",
"reject": "Rechazar",
"pendingApproval": "Pendiente de Aprobación",
"undo": "Deshacer",
"skip": "Saltar",
"cancel": "Cancelar",
"noPriority": "Sin Prioridad",
"subtasks": "Subtareas",
"noDescription": "No hay descripción disponible",
"timer": {
"active": "Temporizador Activo",
"paused": "Temporizador Pausado",
"reset": "Reiniciar Temporizador",
"delete": "Eliminar Sesión"
}
}
}

View File

@@ -0,0 +1,33 @@
{
"save": "Guardar",
"cancel": "Cancelar",
"delete": "Eliminar",
"edit": "Editar",
"close": "Cerrar",
"confirm": "Confirmar",
"loading": "Cargando...",
"error": "Error",
"success": "Éxito",
"warning": "Advertencia",
"refresh": "Actualizar",
"copy": "Copiar",
"copied": "¡Copiado!",
"settings": "Configuración",
"yes": "Sí",
"no": "No",
"back": "Atrás",
"backToCalendar": "Volver al Calendario",
"logout": "Cerrar Sesión",
"version": "Versión",
"navigation": {
"allTasks": "Todas las Tareas",
"archived": "Archivadas",
"things": "Cosas",
"labels": "Etiquetas",
"projects": "Proyectos",
"filters": "Filtros",
"activities": "Actividades",
"points": "Puntos",
"settings": "Configuración"
}
}

View File

@@ -0,0 +1,174 @@
{
"title": "Configuración",
"circleSettings": {
"title": "Configuración del círculo",
"description": "Tu cuenta se conecta automáticamente a un Círculo cuando creas o te unes a uno. Invita fácilmente a amigos compartiendo el código único del Círculo o el enlace a continuación.",
"circleCode": "Código del Círculo",
"copyCode": "Copiar Código",
"copyLink": "Copiar Enlace",
"codeCopied": "¡Código del círculo copiado!",
"linkCopied": "¡Enlace copiado!",
"joinCircle": "Unirse a un Círculo",
"joinCirclePlaceholder": "Ingresa el Código del Círculo",
"join": "Unirse",
"leave": "Salir del Círculo",
"leaveConfirmTitle": "Salir del Círculo",
"leaveConfirmMessage": "¿Estás seguro de que quieres salir de este círculo?",
"circleMembers": "Miembros del Círculo",
"circleMemberRequests": "Solicitudes de Miembros del Círculo",
"admin": "Administrador",
"member": "Miembro",
"pending": "Pendiente",
"accept": "Aceptar",
"reject": "Rechazar",
"makeAdmin": "Hacer Administrador",
"makeMember": "Hacer Miembro",
"remove": "Eliminar",
"webhookURL": "URL del Webhook",
"webhookDescription": "Ingresa una URL de webhook para recibir notificaciones de eventos del círculo",
"webhookPlaceholder": "https://tu-url-webhook.com"
},
"accountSettings": {
"title": "Configuración de la Cuenta",
"subscription": "Suscripción",
"subscriptionStatus": "Plan Actual",
"free": "Gratis",
"plus": "Plus",
"upgrade": "Actualizar",
"cancel": "Cancelar",
"changePassword": "Cambiar Contraseña",
"password": "Contraseña",
"dangerZone": "Zona de Peligro",
"dangerZoneDescription": "Una vez que elimines tu cuenta, no hay vuelta atrás. Por favor, está seguro.",
"deleteAccount": "Eliminar Cuenta"
},
"localization": {
"title": "Localización",
"description": "Personaliza el idioma, formato de fecha y preferencias regionales para tu cuenta.",
"language": "Idioma",
"languageDescription": "Selecciona tu idioma preferido",
"dateFormat": "Formato de Fecha",
"dateFormatDescription": "Elige cómo se deben mostrar las fechas en toda la aplicación",
"timeFormat": "Formato de Hora",
"timeFormatDescription": "Selecciona formato de 12 o 24 horas",
"12hour": "12 horas (AM/PM)",
"24hour": "24 horas",
"firstDayOfWeek": "Primer Día de la Semana",
"firstDayOfWeekDescription": "Selecciona qué día comienza tu semana",
"sunday": "Domingo",
"monday": "Lunes",
"saturday": "Sábado",
"formats": {
"mdy": "MM/DD/AAAA (EE.UU.)",
"dmy": "DD/MM/AAAA (Europa)",
"ymd": "AAAA-MM-DD (ISO)",
"long": "Formato largo (ej., 1 de enero de 2024)",
"short": "Formato corto (ej., 1 ene 2024)"
}
},
"sidepanel": {
"title": "Personalización del Panel Lateral",
"description": "Personaliza el diseño y la visibilidad de las tarjetas en el panel lateral. Esta sección solo está disponible en dispositivos de pantalla grande como tabletas y computadoras de escritorio."
},
"theme": {
"title": "Preferencias de tema",
"description": "Elige cómo se ve el sitio para ti. Selecciona un solo tema o sincronízalo con tu sistema y cambia automáticamente entre temas de día y noche.",
"themeMode": "Modo de tema",
"light": "Claro",
"dark": "Oscuro",
"system": "Sistema"
},
"notifications": {
"settingsSaved": "Configuración guardada con éxito",
"settingsSaveFailed": "Error al guardar la configuración",
"invalidWebhook": "URL de webhook no válida"
},
"profile": {
"title": "Configuración del Perfil",
"description": "Actualiza tu nombre para mostrar y foto de perfil.",
"photoUpdated": "Foto Actualizada",
"photoUpdatedMessage": "¡Tu foto de perfil ha sido actualizada con éxito!",
"uploadFailed": "Error al Subir",
"uploadFailedMessage": "Error al subir tu foto. Por favor, inténtalo de nuevo.",
"profileUpdated": "Perfil Actualizado",
"profileUpdatedMessage": "¡Tu información de perfil ha sido guardada con éxito!",
"updateFailed": "Error al Actualizar",
"updateFailedMessage": "No se pudo actualizar tu perfil. Por favor, verifica tu conexión e inténtalo de nuevo.",
"changePhoto": "Cambiar Foto",
"displayName": "Nombre para Mostrar",
"displayNamePlaceholder": "Ingresa tu nombre para mostrar",
"timezone": "Zona Horaria",
"timezonePlaceholder": "Selecciona tu zona horaria",
"save": "Guardar",
"cancel": "Cancelar"
},
"overview": {
"title": "Configuración",
"subtitle": "Personaliza tu experiencia y administra las preferencias de tu cuenta",
"upgrade": {
"title": "Actualizar a Plus",
"description": "Desbloquea funciones potentes para mejorar tu productividad",
"button": "Actualizar Ahora",
"features": {
"richText": "Descripciones en texto enriquecido",
"notifications": "Notificaciones de tareas",
"apiIntegrations": "Integraciones API",
"advancedAutomation": "Automatización avanzada"
}
},
"sections": {
"profile": {
"title": "Configuración del Perfil",
"description": "Actualiza tu información de perfil, foto, nombre para mostrar y preferencias de zona horaria."
},
"circle": {
"title": "Configuración del Círculo",
"description": "Administra tu círculo, invita miembros y gestiona solicitudes de unión."
},
"account": {
"title": "Configuración de la Cuenta",
"description": "Administra tu suscripción, cambia la contraseña y opciones de eliminación de cuenta."
},
"subaccounts": {
"title": "Cuentas Administradas",
"description": "Crea y administra subcuentas para iniciar sesión y completar tareas asignadas."
},
"notifications": {
"title": "Notificaciones",
"description": "Configura notificaciones push, alertas por correo electrónico y destinos de notificación para tareas."
},
"mfa": {
"title": "Autenticación Multifactor",
"description": "Agrega una capa adicional de seguridad con MFA usando aplicaciones de autenticación."
},
"apitokens": {
"title": "Tokens API",
"description": "Genera y administra tokens de acceso para integraciones de terceros y acceso a la API."
},
"storage": {
"title": "Configuración de Almacenamiento",
"description": "Respalda y restaura tus datos, administra el almacenamiento local y las preferencias de sincronización."
},
"sidepanel": {
"title": "Personalización del Panel Lateral",
"description": "Personaliza el diseño y la visibilidad de las tarjetas en la interfaz del panel lateral."
},
"theme": {
"title": "Preferencias de Tema",
"description": "Elige tu tema preferido y configura los ajustes de modo oscuro/claro."
},
"localization": {
"title": "Localización",
"description": "Personaliza el idioma, formato de fecha, formato de hora y preferencias regionales."
},
"advanced": {
"title": "Configuración Avanzada",
"description": "Configura webhooks, actualizaciones en tiempo real y otras funciones avanzadas para mejorar la productividad."
},
"developer": {
"title": "Configuración de Desarrollador",
"description": "Ver información técnica sobre tokens de autenticación, conexiones SSE y datos de depuración."
}
}
}
}

View File

@@ -29,5 +29,51 @@
"activities": "Actividades",
"points": "Puntos",
"settings": "Configuración"
},
"feedback": {
"later": "Maybe later",
"sentiment": {
"title": "How's Donetick working for you?",
"subtitle": "Your answer helps us decide what to build next.",
"options": {
"love": "Love it",
"okay": "It's okay",
"issues": "Having issues"
}
},
"categories": {
"bugs": "Bugs",
"missingFeature": "Missing feature",
"tooComplicated": "Too complicated",
"slow": "Slow",
"notifications": "Notifications",
"ai": "AI",
"other": "Other"
},
"details": {
"title": "What could we improve?",
"messageLabel": "Tell us more",
"messagePlaceholder": "What happened, or what would make this better?",
"contextNote": "We'll include your app version, device and platform so we can reproduce issues.",
"submit": "Send feedback",
"contextNoteSelfHosted": "You're on a self-hosted instance, so nothing is sent from your server — we'll open a pre-filled GitHub issue you can review and edit first.",
"submitSelfHosted": "Continue to GitHub"
},
"review": {
"title": "Glad you're enjoying it!",
"subtitle": "A rating or a star helps other people find Donetick.",
"github": "Star on GitHub",
"appStore": "Rate on the App Store",
"playStore": "Rate on Google Play"
},
"thanks": {
"title": "Thanks for the feedback",
"subtitle": "We read every response and it shapes what we work on next."
},
"github": {
"title": "Report it on GitHub",
"subtitle": "We've filled in an issue with your notes and version details. Nothing has been sent yet — review it and post when you're ready.",
"open": "Open the issue"
}
}
}

View File

@@ -168,6 +168,10 @@
"developer": {
"title": "Configuración de Desarrollador",
"description": "Ver información técnica sobre tokens de autenticación, conexiones SSE y datos de depuración."
},
"feedback": {
"title": "Send Feedback",
"description": "Tell us how Donetick is working for you, report a bug, or request a feature."
}
}
}

View File

@@ -0,0 +1,75 @@
{
"title": "Tâches",
"myChores": "Mes Tâches",
"allChores": "Toutes les Tâches",
"addChore": "Ajouter une Tâche",
"editChore": "Modifier la Tâche",
"deleteChore": "Supprimer la Tâche",
"completeChore": "Terminer la Tâche",
"dueDate": "Date d'Échéance",
"assignedTo": "Assigné à",
"priority": "Priorité",
"status": "Statut",
"description": "Description",
"choreView": {
"assignment": "Attribution",
"assigned": "Assigné",
"last": "Dernier",
"schedule": "Calendrier",
"due": "Échéance",
"statistics": "Statistiques",
"completed": "Terminé",
"times": "fois",
"details": "Détails",
"createdBy": "Créé par",
"na": "N/A",
"taskCompleted": "Tâche Terminée",
"taskCompletedMessage": "Votre tâche a été marquée comme terminée",
"taskCompletionUndone": "La complétion de la tâche a été annulée.",
"taskSkipUndone": "Le saut de tâche a été annulé.",
"undoSuccessful": "Annulation Réussie",
"undoFailed": "Annulation Échouée",
"undoFailedMessage": "Impossible d'annuler l'action. Veuillez réessayer.",
"resetTimer": "Réinitialiser le Minuteur",
"resetTimerConfirmation": "Êtes-vous sûr de vouloir réinitialiser le minuteur ? Cela effacera tous les enregistrements de temps depuis le début de la tâche.",
"clearAllTimeRecords": "Effacer Tous les Enregistrements de Temps",
"clearAllTimeConfirmation": "Cela supprimera définitivement tous les minuteurs de cette tâche et la remettra à l'état \"non démarrée\".",
"descriptionTitle": "Description",
"description": "Description :",
"previousNote": "Note Précédente",
"previousNoteLabel": "Note précédente :",
"subtasksLabel": "Sous-tâches :",
"taskActions": "Actions de Tâche",
"addNote": "Ajouter une note",
"additionalNotes": "Notes Supplémentaires :",
"notePlaceholder": "Ajouter une note sur la complétion...",
"setCustomCompletionTime": "Définir une heure de complétion personnalisée",
"skipTask": "Passer la Tâche",
"skipTaskConfirmation": "Êtes-vous sûr de vouloir passer cette tâche ?",
"markComplete": "Marquer comme Terminée",
"markAsDone": "Marquer comme fait",
"edit": "Modifier",
"archive": "Archiver",
"unarchive": "Désarchiver",
"viewHistory": "Voir l'Historique",
"history": "Historique",
"startTimer": "Démarrer le Minuteur",
"start": "Démarrer",
"pauseTimer": "Mettre en Pause le Minuteur",
"approve": "Approuver",
"reject": "Rejeter",
"pendingApproval": "En Attente d'Approbation",
"undo": "Annuler",
"skip": "Passer",
"cancel": "Annuler",
"noPriority": "Aucune Priorité",
"subtasks": "Sous-tâches",
"noDescription": "Aucune description disponible",
"timer": {
"active": "Minuteur Actif",
"paused": "Minuteur en Pause",
"reset": "Réinitialiser le Minuteur",
"delete": "Supprimer la Session"
}
}
}

View File

@@ -0,0 +1,33 @@
{
"save": "Enregistrer",
"cancel": "Annuler",
"delete": "Supprimer",
"edit": "Modifier",
"close": "Fermer",
"confirm": "Confirmer",
"loading": "Chargement...",
"error": "Erreur",
"success": "Succès",
"warning": "Avertissement",
"refresh": "Actualiser",
"copy": "Copier",
"copied": "Copié !",
"settings": "Paramètres",
"yes": "Oui",
"no": "Non",
"back": "Retour",
"backToCalendar": "Retour au Calendrier",
"logout": "Déconnexion",
"version": "Version",
"navigation": {
"allTasks": "Toutes les Tâches",
"archived": "Archivées",
"things": "Objets",
"labels": "Étiquettes",
"projects": "Projets",
"filters": "Filtres",
"activities": "Activités",
"points": "Points",
"settings": "Paramètres"
}
}

View File

@@ -0,0 +1,174 @@
{
"title": "Paramètres",
"circleSettings": {
"title": "Paramètres du cercle",
"description": "Votre compte est automatiquement connecté à un Cercle lorsque vous en créez un ou en rejoignez un. Invitez facilement des amis en partageant le code unique du Cercle ou le lien ci-dessous.",
"circleCode": "Code du Cercle",
"copyCode": "Copier le Code",
"copyLink": "Copier le Lien",
"codeCopied": "Code du cercle copié !",
"linkCopied": "Lien copié !",
"joinCircle": "Rejoindre un Cercle",
"joinCirclePlaceholder": "Entrer le Code du Cercle",
"join": "Rejoindre",
"leave": "Quitter le Cercle",
"leaveConfirmTitle": "Quitter le Cercle",
"leaveConfirmMessage": "Êtes-vous sûr de vouloir quitter ce cercle ?",
"circleMembers": "Membres du Cercle",
"circleMemberRequests": "Demandes de Membres du Cercle",
"admin": "Administrateur",
"member": "Membre",
"pending": "En attente",
"accept": "Accepter",
"reject": "Rejeter",
"makeAdmin": "Nommer Administrateur",
"makeMember": "Nommer Membre",
"remove": "Supprimer",
"webhookURL": "URL du Webhook",
"webhookDescription": "Entrez une URL de webhook pour recevoir des notifications pour les événements du cercle",
"webhookPlaceholder": "https://votre-url-webhook.com"
},
"accountSettings": {
"title": "Paramètres du Compte",
"subscription": "Abonnement",
"subscriptionStatus": "Plan Actuel",
"free": "Gratuit",
"plus": "Plus",
"upgrade": "Mettre à Niveau",
"cancel": "Annuler",
"changePassword": "Changer le Mot de Passe",
"password": "Mot de passe",
"dangerZone": "Zone Dangereuse",
"dangerZoneDescription": "Une fois votre compte supprimé, il n'y a pas de retour en arrière. Veuillez être certain.",
"deleteAccount": "Supprimer le Compte"
},
"localization": {
"title": "Localisation",
"description": "Personnalisez la langue, le format de date et les préférences régionales pour votre compte.",
"language": "Langue",
"languageDescription": "Sélectionnez votre langue préférée",
"dateFormat": "Format de Date",
"dateFormatDescription": "Choisissez comment les dates doivent être affichées dans l'application",
"timeFormat": "Format de l'Heure",
"timeFormatDescription": "Sélectionnez le format 12 heures ou 24 heures",
"12hour": "12 heures (AM/PM)",
"24hour": "24 heures",
"firstDayOfWeek": "Premier Jour de la Semaine",
"firstDayOfWeekDescription": "Sélectionnez quel jour commence votre semaine",
"sunday": "Dimanche",
"monday": "Lundi",
"saturday": "Samedi",
"formats": {
"mdy": "MM/JJ/AAAA (États-Unis)",
"dmy": "JJ/MM/AAAA (Europe)",
"ymd": "AAAA-MM-JJ (ISO)",
"long": "Format long (ex., 1 janvier 2024)",
"short": "Format court (ex., 1 janv. 2024)"
}
},
"sidepanel": {
"title": "Personnalisation du Panneau Latéral",
"description": "Personnalisez la disposition et la visibilité des cartes dans le panneau latéral. Cette section n'est disponible que sur les grands écrans comme les tablettes et les ordinateurs de bureau."
},
"theme": {
"title": "Préférences de thème",
"description": "Choisissez comment le site vous apparaît. Sélectionnez un thème unique ou synchronisez avec votre système pour basculer automatiquement entre les thèmes jour et nuit.",
"themeMode": "Mode de thème",
"light": "Clair",
"dark": "Sombre",
"system": "Système"
},
"notifications": {
"settingsSaved": "Paramètres enregistrés avec succès",
"settingsSaveFailed": "Échec de l'enregistrement des paramètres",
"invalidWebhook": "URL de webhook invalide"
},
"profile": {
"title": "Paramètres du Profil",
"description": "Mettez à jour votre nom d'affichage et votre photo de profil.",
"photoUpdated": "Photo Mise à Jour",
"photoUpdatedMessage": "Votre photo de profil a été mise à jour avec succès !",
"uploadFailed": "Échec du Téléchargement",
"uploadFailedMessage": "Échec du téléchargement de votre photo. Veuillez réessayer.",
"profileUpdated": "Profil Mis à Jour",
"profileUpdatedMessage": "Vos informations de profil ont été enregistrées avec succès !",
"updateFailed": "Échec de la Mise à Jour",
"updateFailedMessage": "Impossible de mettre à jour votre profil. Veuillez vérifier votre connexion et réessayer.",
"changePhoto": "Changer la Photo",
"displayName": "Nom d'Affichage",
"displayNamePlaceholder": "Entrez votre nom d'affichage",
"timezone": "Fuseau Horaire",
"timezonePlaceholder": "Sélectionnez votre fuseau horaire",
"save": "Enregistrer",
"cancel": "Annuler"
},
"overview": {
"title": "Paramètres",
"subtitle": "Personnalisez votre expérience et gérez les préférences de votre compte",
"upgrade": {
"title": "Passer à Plus",
"description": "Débloquez des fonctionnalités puissantes pour améliorer votre productivité",
"button": "Mettre à Niveau Maintenant",
"features": {
"richText": "Descriptions en texte enrichi",
"notifications": "Notifications de tâches",
"apiIntegrations": "Intégrations API",
"advancedAutomation": "Automatisation avancée"
}
},
"sections": {
"profile": {
"title": "Paramètres du Profil",
"description": "Mettez à jour vos informations de profil, photo, nom d'affichage et préférences de fuseau horaire."
},
"circle": {
"title": "Paramètres du Cercle",
"description": "Gérez votre cercle, invitez des membres et gérez les demandes d'adhésion."
},
"account": {
"title": "Paramètres du Compte",
"description": "Gérez votre abonnement, changez le mot de passe et les options de suppression de compte."
},
"subaccounts": {
"title": "Comptes Gérés",
"description": "Créez et gérez des sous-comptes pour vous connecter et accomplir les tâches assignées."
},
"notifications": {
"title": "Notifications",
"description": "Configurez les notifications push, les alertes par e-mail et les cibles de notification pour les tâches."
},
"mfa": {
"title": "Authentification Multi-Facteurs",
"description": "Ajoutez une couche de sécurité supplémentaire avec MFA en utilisant des applications d'authentification."
},
"apitokens": {
"title": "Jetons API",
"description": "Générez et gérez des jetons d'accès pour les intégrations tierces et l'accès à l'API."
},
"storage": {
"title": "Paramètres de Stockage",
"description": "Sauvegardez et restaurez vos données, gérez le stockage local et les préférences de synchronisation."
},
"sidepanel": {
"title": "Personnalisation du Panneau Latéral",
"description": "Personnalisez la disposition et la visibilité des cartes dans l'interface du panneau latéral."
},
"theme": {
"title": "Préférences de Thème",
"description": "Choisissez votre thème préféré et configurez les paramètres de mode sombre/clair."
},
"localization": {
"title": "Localisation",
"description": "Personnalisez la langue, le format de date, le format de l'heure et les préférences régionales."
},
"advanced": {
"title": "Paramètres Avancés",
"description": "Configurez les webhooks, les mises à jour en temps réel et d'autres fonctionnalités avancées pour améliorer la productivité."
},
"developer": {
"title": "Paramètres Développeur",
"description": "Consultez les informations techniques sur les jetons d'authentification, les connexions SSE et les données de débogage."
}
}
}
}

View File

@@ -29,5 +29,51 @@
"activities": "Activités",
"points": "Points",
"settings": "Paramètres"
},
"feedback": {
"later": "Maybe later",
"sentiment": {
"title": "How's Donetick working for you?",
"subtitle": "Your answer helps us decide what to build next.",
"options": {
"love": "Love it",
"okay": "It's okay",
"issues": "Having issues"
}
},
"categories": {
"bugs": "Bugs",
"missingFeature": "Missing feature",
"tooComplicated": "Too complicated",
"slow": "Slow",
"notifications": "Notifications",
"ai": "AI",
"other": "Other"
},
"details": {
"title": "What could we improve?",
"messageLabel": "Tell us more",
"messagePlaceholder": "What happened, or what would make this better?",
"contextNote": "We'll include your app version, device and platform so we can reproduce issues.",
"submit": "Send feedback",
"contextNoteSelfHosted": "You're on a self-hosted instance, so nothing is sent from your server — we'll open a pre-filled GitHub issue you can review and edit first.",
"submitSelfHosted": "Continue to GitHub"
},
"review": {
"title": "Glad you're enjoying it!",
"subtitle": "A rating or a star helps other people find Donetick.",
"github": "Star on GitHub",
"appStore": "Rate on the App Store",
"playStore": "Rate on Google Play"
},
"thanks": {
"title": "Thanks for the feedback",
"subtitle": "We read every response and it shapes what we work on next."
},
"github": {
"title": "Report it on GitHub",
"subtitle": "We've filled in an issue with your notes and version details. Nothing has been sent yet — review it and post when you're ready.",
"open": "Open the issue"
}
}
}

View File

@@ -25,7 +25,7 @@
"makeMember": "Nommer Membre",
"remove": "Supprimer",
"webhookURL": "URL du Webhook",
"webhookDescription": "Entrez une URL de webhook pour recevoir des notifications pour les événements du cercle",
"webhookDescription": "Entrez une URL de Webhook pour recevoir des notifications pour les événements du cercle",
"webhookPlaceholder": "https://votre-url-webhook.com"
},
"accountSettings": {
@@ -168,6 +168,10 @@
"developer": {
"title": "Paramètres Développeur",
"description": "Consultez les informations techniques sur les jetons d'authentification, les connexions SSE et les données de débogage."
},
"feedback": {
"title": "Send Feedback",
"description": "Tell us how Donetick is working for you, report a bug, or request a feature."
}
}
}

View File

@@ -29,5 +29,51 @@
"activities": "アクティビティ",
"points": "ポイント",
"settings": "設定"
},
"feedback": {
"later": "Maybe later",
"sentiment": {
"title": "How's Donetick working for you?",
"subtitle": "Your answer helps us decide what to build next.",
"options": {
"love": "Love it",
"okay": "It's okay",
"issues": "Having issues"
}
},
"categories": {
"bugs": "Bugs",
"missingFeature": "Missing feature",
"tooComplicated": "Too complicated",
"slow": "Slow",
"notifications": "Notifications",
"ai": "AI",
"other": "Other"
},
"details": {
"title": "What could we improve?",
"messageLabel": "Tell us more",
"messagePlaceholder": "What happened, or what would make this better?",
"contextNote": "We'll include your app version, device and platform so we can reproduce issues.",
"submit": "Send feedback",
"contextNoteSelfHosted": "You're on a self-hosted instance, so nothing is sent from your server — we'll open a pre-filled GitHub issue you can review and edit first.",
"submitSelfHosted": "Continue to GitHub"
},
"review": {
"title": "Glad you're enjoying it!",
"subtitle": "A rating or a star helps other people find Donetick.",
"github": "Star on GitHub",
"appStore": "Rate on the App Store",
"playStore": "Rate on Google Play"
},
"thanks": {
"title": "Thanks for the feedback",
"subtitle": "We read every response and it shapes what we work on next."
},
"github": {
"title": "Report it on GitHub",
"subtitle": "We've filled in an issue with your notes and version details. Nothing has been sent yet — review it and post when you're ready.",
"open": "Open the issue"
}
}
}

View File

@@ -168,6 +168,10 @@
"developer": {
"title": "開発者設定",
"description": "認証トークン、SSE 接続、デバッグデータなどの技術情報を確認できます。"
},
"feedback": {
"title": "Send Feedback",
"description": "Tell us how Donetick is working for you, report a bug, or request a feature."
}
}
}

View File

@@ -29,5 +29,51 @@
"activities": "Activiteiten",
"points": "Punten",
"settings": "Instellingen"
},
"feedback": {
"later": "Maybe later",
"sentiment": {
"title": "How's Donetick working for you?",
"subtitle": "Your answer helps us decide what to build next.",
"options": {
"love": "Love it",
"okay": "It's okay",
"issues": "Having issues"
}
},
"categories": {
"bugs": "Bugs",
"missingFeature": "Missing feature",
"tooComplicated": "Too complicated",
"slow": "Slow",
"notifications": "Notifications",
"ai": "AI",
"other": "Other"
},
"details": {
"title": "What could we improve?",
"messageLabel": "Tell us more",
"messagePlaceholder": "What happened, or what would make this better?",
"contextNote": "We'll include your app version, device and platform so we can reproduce issues.",
"submit": "Send feedback",
"contextNoteSelfHosted": "You're on a self-hosted instance, so nothing is sent from your server — we'll open a pre-filled GitHub issue you can review and edit first.",
"submitSelfHosted": "Continue to GitHub"
},
"review": {
"title": "Glad you're enjoying it!",
"subtitle": "A rating or a star helps other people find Donetick.",
"github": "Star on GitHub",
"appStore": "Rate on the App Store",
"playStore": "Rate on Google Play"
},
"thanks": {
"title": "Thanks for the feedback",
"subtitle": "We read every response and it shapes what we work on next."
},
"github": {
"title": "Report it on GitHub",
"subtitle": "We've filled in an issue with your notes and version details. Nothing has been sent yet — review it and post when you're ready.",
"open": "Open the issue"
}
}
}

View File

@@ -168,6 +168,10 @@
"developer": {
"title": "Ontwikkelaarsinstellingen",
"description": "Bekijk technische informatie over authenticatietokens, SSE-verbindingen en foutopsporingsgegevens."
},
"feedback": {
"title": "Send Feedback",
"description": "Tell us how Donetick is working for you, report a bug, or request a feature."
}
}
}

View File

@@ -29,5 +29,51 @@
"activities": "Atividades",
"points": "Pontos",
"settings": "Configurações"
},
"feedback": {
"later": "Maybe later",
"sentiment": {
"title": "How's Donetick working for you?",
"subtitle": "Your answer helps us decide what to build next.",
"options": {
"love": "Love it",
"okay": "It's okay",
"issues": "Having issues"
}
},
"categories": {
"bugs": "Bugs",
"missingFeature": "Missing feature",
"tooComplicated": "Too complicated",
"slow": "Slow",
"notifications": "Notifications",
"ai": "AI",
"other": "Other"
},
"details": {
"title": "What could we improve?",
"messageLabel": "Tell us more",
"messagePlaceholder": "What happened, or what would make this better?",
"contextNote": "We'll include your app version, device and platform so we can reproduce issues.",
"submit": "Send feedback",
"contextNoteSelfHosted": "You're on a self-hosted instance, so nothing is sent from your server — we'll open a pre-filled GitHub issue you can review and edit first.",
"submitSelfHosted": "Continue to GitHub"
},
"review": {
"title": "Glad you're enjoying it!",
"subtitle": "A rating or a star helps other people find Donetick.",
"github": "Star on GitHub",
"appStore": "Rate on the App Store",
"playStore": "Rate on Google Play"
},
"thanks": {
"title": "Thanks for the feedback",
"subtitle": "We read every response and it shapes what we work on next."
},
"github": {
"title": "Report it on GitHub",
"subtitle": "We've filled in an issue with your notes and version details. Nothing has been sent yet — review it and post when you're ready.",
"open": "Open the issue"
}
}
}

View File

@@ -168,6 +168,10 @@
"developer": {
"title": "Configurações de desenvolvedor",
"description": "Veja informações técnicas sobre tokens de autenticação, conexões SSE e dados de depuração."
},
"feedback": {
"title": "Send Feedback",
"description": "Tell us how Donetick is working for you, report a bug, or request a feature."
}
}
}

View File

@@ -0,0 +1,75 @@
{
"title": "Chores",
"myChores": "My Chores",
"allChores": "All Chores",
"addChore": "Add Chore",
"editChore": "Edit Chore",
"deleteChore": "Delete Chore",
"completeChore": "Complete Task",
"dueDate": "Due Date",
"assignedTo": "Assigned To",
"priority": "Priority",
"status": "Status",
"description": "Description",
"choreView": {
"assignment": "Assignment",
"assigned": "Assigned",
"last": "Last",
"schedule": "Schedule",
"due": "Due",
"statistics": "Statistics",
"completed": "Completed",
"times": "times",
"details": "Details",
"createdBy": "Created By",
"na": "N/A",
"taskCompleted": "Task Completed",
"taskCompletedMessage": "Your task has been marked as complete",
"taskCompletionUndone": "Task completion has been undone.",
"taskSkipUndone": "Task skip has been undone.",
"undoSuccessful": "Undo Successful",
"undoFailed": "Undo Failed",
"undoFailedMessage": "Unable to undo the action. Please try again.",
"resetTimer": "Reset Timer",
"resetTimerConfirmation": "Are you sure you want to reset the timer? This will clear all time records since you started the task.",
"clearAllTimeRecords": "Clear All Time Records",
"clearAllTimeConfirmation": "This will permanently delete all timers for this task and set it back to \"not started\".",
"descriptionTitle": "Description",
"description": "Description :",
"previousNote": "Previous Note",
"previousNoteLabel": "Previous note:",
"subtasksLabel": "Subtasks :",
"taskActions": "Task Actions",
"addNote": "Add a note",
"additionalNotes": "Additional Notes:",
"notePlaceholder": "Add a note about the completion...",
"setCustomCompletionTime": "Set custom completion time",
"skipTask": "Skip Task",
"skipTaskConfirmation": "Are you sure you want to skip this task?",
"markComplete": "Mark Complete",
"markAsDone": "Mark as done",
"edit": "Edit",
"archive": "Archive",
"unarchive": "Unarchive",
"viewHistory": "View History",
"history": "History",
"startTimer": "Start Timer",
"start": "Start",
"pauseTimer": "Pause Timer",
"approve": "Approve",
"reject": "Reject",
"pendingApproval": "Pending Approval",
"undo": "Undo",
"skip": "Skip",
"cancel": "Cancel",
"noPriority": "No Priority",
"subtasks": "Subtasks",
"noDescription": "No description available",
"timer": {
"active": "Timer Active",
"paused": "Timer Paused",
"reset": "Reset Timer",
"delete": "Delete Session"
}
}
}

View File

@@ -0,0 +1,79 @@
{
"save": "Save",
"cancel": "Cancel",
"delete": "Delete",
"edit": "Edit",
"close": "Close",
"confirm": "Confirm",
"loading": "Loading...",
"error": "Error",
"success": "Success",
"warning": "Warning",
"refresh": "Refresh",
"copy": "Copy",
"copied": "Copied!",
"settings": "Settings",
"yes": "Yes",
"no": "No",
"back": "Back",
"backToCalendar": "Back to Calendar",
"logout": "Logout",
"version": "Version",
"navigation": {
"allTasks": "All Tasks",
"archived": "Archived",
"things": "Things",
"labels": "Labels",
"projects": "Projects",
"filters": "Filters",
"activities": "Activities",
"points": "Points",
"settings": "Settings"
},
"feedback": {
"later": "Maybe later",
"sentiment": {
"title": "How's Donetick working for you?",
"subtitle": "Your answer helps us decide what to build next.",
"options": {
"love": "Love it",
"okay": "It's okay",
"issues": "Having issues"
}
},
"categories": {
"bugs": "Bugs",
"missingFeature": "Missing feature",
"tooComplicated": "Too complicated",
"slow": "Slow",
"notifications": "Notifications",
"ai": "AI",
"other": "Other"
},
"details": {
"title": "What could we improve?",
"messageLabel": "Tell us more",
"messagePlaceholder": "What happened, or what would make this better?",
"contextNote": "We'll include your app version, device and platform so we can reproduce issues.",
"submit": "Send feedback",
"contextNoteSelfHosted": "You're on a self-hosted instance, so nothing is sent from your server — we'll open a pre-filled GitHub issue you can review and edit first.",
"submitSelfHosted": "Continue to GitHub"
},
"review": {
"title": "Glad you're enjoying it!",
"subtitle": "A rating or a star helps other people find Donetick.",
"github": "Star on GitHub",
"appStore": "Rate on the App Store",
"playStore": "Rate on Google Play"
},
"thanks": {
"title": "Thanks for the feedback",
"subtitle": "We read every response and it shapes what we work on next."
},
"github": {
"title": "Report it on GitHub",
"subtitle": "We've filled in an issue with your notes and version details. Nothing has been sent yet — review it and post when you're ready.",
"open": "Open the issue"
}
}
}

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