diff --git a/.github/workflows/build-nightly.yml b/.github/workflows/build-nightly.yml new file mode 100644 index 00000000..3050b473 --- /dev/null +++ b/.github/workflows/build-nightly.yml @@ -0,0 +1,85 @@ +name: Nightly Build & Log + +# Builds the signed nightly APK on a schedule and records the result in the +# website build log (website/data/nightly_builds.json). Committing that file to +# main touches website/**, which triggers deploy-website.yml to refresh the +# published site. This complements the push-triggered F-Droid deploy +# (nightlydepolyci.yml); the two can be unified later. + +on: + schedule: + - cron: '0 2 * * *' # 02:00 UTC daily + workflow_dispatch: + +permissions: + contents: write # commit the updated build log back to main + +jobs: + nightly: + name: Build nightly APK and record build log + runs-on: ubuntu-latest + steps: + - name: Free Disk Space (Ubuntu) + uses: jlumbroso/free-disk-space@main + with: + tool-cache: false + android: false # keep Android SDKs for Flutter + dotnet: true + haskell: true + large-packages: true + docker-images: true + swap-storage: true + + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: '17.x' + + - name: Setup Flutter + uses: subosito/flutter-action@v2 + with: + flutter-version: '3.29.2' + + - name: Get dependencies + run: flutter pub get + + - name: Decode signing secrets + env: + NIGHTLY_KEYSTORE_B64: ${{ secrets.NIGHTLY_KEYSTORE_B64 }} + NIGHTLY_PROPERTIES_B64: ${{ secrets.NIGHTLY_PROPERTIES_B64 }} + run: | + echo "$NIGHTLY_KEYSTORE_B64" | base64 --decode > android/nightly.jks + echo "$NIGHTLY_PROPERTIES_B64" | base64 --decode > android/key_nightly.properties + + - name: Build nightly APK + id: build + run: flutter build apk --flavor nightly --build-number=${{ github.run_number }} --release + + - name: Record successful build + if: success() + run: | + python3 scripts/update_build_log.py success \ + "https://github.com/${{ github.repository }}/raw/fdroid-repo/repo/nightly.${{ github.run_number }}.apk" \ + "${{ github.run_number }}" + + - name: Record failed build + if: failure() + run: python3 scripts/update_build_log.py failure "" "${{ github.run_number }}" + + - name: Commit build log + if: always() + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + if git diff --quiet -- website/data/nightly_builds.json; then + echo "No build-log changes to commit." + else + git add website/data/nightly_builds.json + git commit -m "chore(nightly): record build ${{ github.run_number }}" + git push + fi diff --git a/.github/workflows/build-tc-helper.yml b/.github/workflows/build-tc-helper.yml new file mode 100644 index 00000000..f8f7ced2 --- /dev/null +++ b/.github/workflows/build-tc-helper.yml @@ -0,0 +1,72 @@ +# Compiles the tc_helper Rust native library from source (instead of relying on +# the pre-built .so committed under android/app/src/main/jniLibs/) and then +# builds the production APK against the freshly-compiled library. +# +# This is the "proper" fix for stale committed binaries: the .so is regenerated +# from rust/ on every run, so it can never drift from the Rust source. Once this +# is green, the committed jniLibs/*.so can be git-ignored and purged from history. +# +# NOTE: This workflow has not yet been run on CI — action versions, the NDK +# version, and the cargo-ndk invocation are transcribed from a verified LOCAL +# build (macOS) and may need small adjustments on the first CI run. +# +# Both arm64-v8a and armeabi-v7a are built. taskchampion is configured with only +# the server-sync backend (see rust/Cargo.toml), so there's no aws-lc-sys — hence +# no cmake/ninja/bindgen/libclang toolchain needed; ring builds with just the +# Rust + NDK toolchain. +name: Build tc_helper (compile from source) + +on: + workflow_dispatch: + pull_request: + branches: [main, reports] + paths: + - "rust/**" + - "android/**" + - ".github/workflows/build-tc-helper.yml" + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-java@v4 + with: + distribution: "temurin" + java-version: "17" + + # Android SDK + NDK. cargo-ndk reads ANDROID_NDK_HOME to find the toolchain. + - uses: android-actions/setup-android@v3 + - name: Install NDK + run: | + sdkmanager "ndk;26.1.10909125" + echo "ANDROID_NDK_HOME=$ANDROID_HOME/ndk/26.1.10909125" >> "$GITHUB_ENV" + + - name: Install Rust + Android targets + cargo-ndk + run: | + rustup toolchain install stable --profile minimal + rustup target add aarch64-linux-android armv7-linux-androideabi + cargo install cargo-ndk --version ^4 + + - name: Compile tc_helper (arm64-v8a + armeabi-v7a) from source + working-directory: rust + run: | + cargo ndk -t arm64-v8a -t armeabi-v7a \ + -o ../android/app/src/main/jniLibs \ + build --release + + - name: Show freshly-built libs + run: ls -lhR android/app/src/main/jniLibs/ + + - uses: subosito/flutter-action@v2 + with: + flutter-version: "3.44.5" + + - run: flutter pub get + - run: flutter build apk --release --flavor production + + - uses: actions/upload-artifact@v4 + with: + name: production-apk-from-source + path: build/app/outputs/flutter-apk/app-production-release.apk diff --git a/.github/workflows/deploy-website.yml b/.github/workflows/deploy-website.yml new file mode 100644 index 00000000..b5fa4d79 --- /dev/null +++ b/.github/workflows/deploy-website.yml @@ -0,0 +1,64 @@ +name: Deploy Website + +# Builds the Hugo site in website/ and publishes it to GitHub Pages. +# Triggered when the site content or this workflow changes (the nightly +# build-log commit made by build-nightly.yml touches website/data/, so a new +# nightly automatically refreshes the published site). + +on: + push: + branches: [main] + paths: + - 'website/**' + - '.github/workflows/deploy-website.yml' + workflow_dispatch: + +permissions: + contents: read + pages: write + id-token: write + +# Allow one concurrent deployment; don't cancel an in-progress production deploy. +concurrency: + group: pages + cancel-in-progress: false + +jobs: + build: + runs-on: ubuntu-latest + env: + HUGO_VERSION: 0.164.0 + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 # hugo.toml enableGitInfo needs full history + + - name: Setup Hugo + uses: peaceiris/actions-hugo@v3 + with: + hugo-version: ${{ env.HUGO_VERSION }} + extended: true + + - name: Configure Pages + id: pages + uses: actions/configure-pages@v5 + + - name: Build site + run: hugo --source website --minify --baseURL "${{ steps.pages.outputs.base_url }}/" + + - name: Upload artifact + uses: actions/upload-pages-artifact@v3 + with: + path: website/public + + deploy: + needs: build + runs-on: ubuntu-latest + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 diff --git a/.github/workflows/nightlydepolyci.yml b/.github/workflows/nightlydepolyci.yml index e47359b8..c8e92ad4 100644 --- a/.github/workflows/nightlydepolyci.yml +++ b/.github/workflows/nightlydepolyci.yml @@ -4,6 +4,13 @@ on: push: branches: - main + # Website content and the nightly build-log commit must not trigger a full + # APK rebuild + F-Droid redeploy (see build-nightly.yml / deploy-website.yml). + paths-ignore: + - 'website/**' + - 'scripts/**' + - '.github/workflows/deploy-website.yml' + - '.github/workflows/build-nightly.yml' jobs: build-and-deploy: diff --git a/android/app/src/main/jniLibs/arm64-v8a/libcrc_fast-b3182f249ae653b7.so b/android/app/src/main/jniLibs/arm64-v8a/libcrc_fast-b3182f249ae653b7.so deleted file mode 100755 index 3a8b6827..00000000 Binary files a/android/app/src/main/jniLibs/arm64-v8a/libcrc_fast-b3182f249ae653b7.so and /dev/null differ diff --git a/android/app/src/main/jniLibs/arm64-v8a/libtc_helper.so b/android/app/src/main/jniLibs/arm64-v8a/libtc_helper.so index 2ed5d305..6eef8ac4 100755 Binary files a/android/app/src/main/jniLibs/arm64-v8a/libtc_helper.so and b/android/app/src/main/jniLibs/arm64-v8a/libtc_helper.so differ diff --git a/android/app/src/main/jniLibs/armeabi-v7a/libcrc_fast-c8bd19aec5c73f3a.so b/android/app/src/main/jniLibs/armeabi-v7a/libcrc_fast-c8bd19aec5c73f3a.so deleted file mode 100755 index df205bcd..00000000 Binary files a/android/app/src/main/jniLibs/armeabi-v7a/libcrc_fast-c8bd19aec5c73f3a.so and /dev/null differ diff --git a/android/app/src/main/jniLibs/armeabi-v7a/libtc_helper.so b/android/app/src/main/jniLibs/armeabi-v7a/libtc_helper.so index 4fa2b74e..d89a4f66 100755 Binary files a/android/app/src/main/jniLibs/armeabi-v7a/libtc_helper.so and b/android/app/src/main/jniLibs/armeabi-v7a/libtc_helper.so differ diff --git a/ios/tc_helper.xcframework/Info.plist b/ios/tc_helper.xcframework/Info.plist index cf3e2802..560aabd5 100644 --- a/ios/tc_helper.xcframework/Info.plist +++ b/ios/tc_helper.xcframework/Info.plist @@ -8,32 +8,32 @@ BinaryPath tc_helper.framework/tc_helper LibraryIdentifier - ios-arm64 + ios-arm64_x86_64-simulator LibraryPath tc_helper.framework SupportedArchitectures arm64 + x86_64 SupportedPlatform ios + SupportedPlatformVariant + simulator BinaryPath tc_helper.framework/tc_helper LibraryIdentifier - ios-arm64_x86_64-simulator + ios-arm64 LibraryPath tc_helper.framework SupportedArchitectures arm64 - x86_64 SupportedPlatform ios - SupportedPlatformVariant - simulator CFBundlePackageType diff --git a/ios/tc_helper.xcframework/ios-arm64/tc_helper.framework/tc_helper b/ios/tc_helper.xcframework/ios-arm64/tc_helper.framework/tc_helper index 458cab50..a19b4587 100755 Binary files a/ios/tc_helper.xcframework/ios-arm64/tc_helper.framework/tc_helper and b/ios/tc_helper.xcframework/ios-arm64/tc_helper.framework/tc_helper differ diff --git a/ios/tc_helper.xcframework/ios-arm64_x86_64-simulator/tc_helper.framework/tc_helper b/ios/tc_helper.xcframework/ios-arm64_x86_64-simulator/tc_helper.framework/tc_helper index cec5b500..a9f3dfc8 100755 Binary files a/ios/tc_helper.xcframework/ios-arm64_x86_64-simulator/tc_helper.framework/tc_helper and b/ios/tc_helper.xcframework/ios-arm64_x86_64-simulator/tc_helper.framework/tc_helper differ diff --git a/lib/app/modules/home/controllers/home_controller.dart b/lib/app/modules/home/controllers/home_controller.dart index 3cf342f8..e556b4c6 100644 --- a/lib/app/modules/home/controllers/home_controller.dart +++ b/lib/app/modules/home/controllers/home_controller.dart @@ -33,9 +33,7 @@ import 'package:taskwarrior/app/utils/app_settings/app_settings.dart'; import 'package:taskwarrior/app/v3/champion/replica.dart'; import 'package:taskwarrior/app/v3/champion/models/task_for_replica.dart'; import 'package:taskwarrior/app/v3/db/task_database.dart'; -import 'package:taskwarrior/app/v3/db/update.dart'; import 'package:taskwarrior/app/v3/models/task.dart'; -import 'package:taskwarrior/app/v3/net/fetch.dart'; import 'package:textfield_tags/textfield_tags.dart'; import 'package:taskwarrior/app/utils/themes/theme_extension.dart'; import 'package:tutorial_coach_mark/tutorial_coach_mark.dart'; @@ -51,6 +49,11 @@ class HomeController extends GetxController { final RxSet selectedTags = {}.obs; final RxList queriedTasks = [].obs; final RxList searchedTasks = [].obs; + // Reactive mirror of the search box text. `searchedTasks` only drives the + // local-taskc list (TasksBuilder); the TaskChampion replica list reads + // `tasksFromReplica` directly, so it needs an observable query to rebuild and + // filter on each keystroke. Kept in sync by search()/toggleSearch(). + final RxString searchQuery = ''.obs; final RxList selectedDates = List.filled(4, null).obs; final RxMap pendingTags = {}.obs; final RxMap projects = {}.obs; @@ -86,6 +89,12 @@ class HomeController extends GetxController { taskdb = TaskDatabase(); taskdb.open(); getUniqueProjects(); + // Initialize the pending/waiting filters from their persisted values. + // Without this the RxBool defaults to false, and the replica list view + // (which filters `status == pending` only when pendingFilter is true) + // shows completed tasks only — hiding every pending task on first load. + pendingFilter.value = Query(storage.tabs.tab()).getPendingFilter(); + waitingFilter.value = Query(storage.tabs.tab()).getWaitingFilter(); _loadTaskChampion(); fetchTasksFromDB(); @@ -168,16 +177,6 @@ class HomeController extends GetxController { debugPrint("Tasks from Replica: ${tasks.length}"); } - Future refreshTasks(String clientId, String encryptionSecret) async { - TaskDatabase taskDatabase = TaskDatabase(); - await taskDatabase.open(); - List tasksFromServer = - await fetchTasks(clientId, encryptionSecret); - await updateTasksInDatabase(tasksFromServer); - List fetchedTasks = await taskDatabase.fetchTasksFromDatabase(); - tasks.value = fetchedTasks; - } - Future fetchTasksFromDB() async { debugPrint("Fetching tasks from DB ${taskReplica.value}"); await _loadTaskChampion(); @@ -499,10 +498,12 @@ class HomeController extends GetxController { if (!searchVisible.value) { searchedTasks.assignAll(queriedTasks); searchController.text = ''; + searchQuery.value = ''; } } void search(String term) { + searchQuery.value = term; searchedTasks.assignAll( queriedTasks .where( @@ -580,9 +581,10 @@ class HomeController extends GetxController { await refreshReplicaTasks(); } } else if (taskchampion.value) { - if (clientId != null && encryptionSecret != null) { - await refreshTasks(clientId, encryptionSecret); - } + // CCSync HTTP sync has been retired; the local TaskChampion database is + // the source of truth for this mode, so reload it without a remote + // round-trip. + await fetchTasksFromDB(); } else { await synchronize(context, false); } diff --git a/lib/app/modules/home/views/filter_drawer_home_page.dart b/lib/app/modules/home/views/filter_drawer_home_page.dart index 085a969d..c83bad37 100644 --- a/lib/app/modules/home/views/filter_drawer_home_page.dart +++ b/lib/app/modules/home/views/filter_drawer_home_page.dart @@ -331,76 +331,105 @@ class FilterDrawer extends StatelessWidget { spacing: 8, runSpacing: 4, children: [ - for (var sort in [ - SentenceManager( - currentLanguage: - homeController.selectedLanguage.value) - .sentences - .filterDrawerCreated, - SentenceManager( - currentLanguage: - homeController.selectedLanguage.value) - .sentences - .filterDrawerModified, - SentenceManager( - currentLanguage: - homeController.selectedLanguage.value) - .sentences - .filterDrawerStartTime, - SentenceManager( - currentLanguage: - homeController.selectedLanguage.value) - .sentences - .filterDrawerDueTill, - SentenceManager( - currentLanguage: - homeController.selectedLanguage.value) - .sentences - .filterDrawerPriority, - SentenceManager( - currentLanguage: - homeController.selectedLanguage.value) - .sentences - .filterDrawerProject, - SentenceManager( - currentLanguage: - homeController.selectedLanguage.value) - .sentences - .filterDrawerTags, - SentenceManager( - currentLanguage: - homeController.selectedLanguage.value) - .sentences - .filterDrawerUrgency, + // Each sort option pairs a STABLE key (always English, + // e.g. 'Created') with a localized display label. The + // key is what gets stored in `selectedSort` and matched + // by the sort switches in show_tasks*.dart. Previously + // the localized label doubled as the sort value, so in + // any non-English locale the stored value (e.g. + // "Creado+") never matched case 'Created+' and sorting + // silently did nothing. + for (final option in >[ + { + 'key': 'Created', + 'label': SentenceManager( + currentLanguage: + homeController.selectedLanguage.value) + .sentences + .filterDrawerCreated + }, + { + 'key': 'Modified', + 'label': SentenceManager( + currentLanguage: + homeController.selectedLanguage.value) + .sentences + .filterDrawerModified + }, + { + 'key': 'Start Time', + 'label': SentenceManager( + currentLanguage: + homeController.selectedLanguage.value) + .sentences + .filterDrawerStartTime + }, + { + 'key': 'Due till', + 'label': SentenceManager( + currentLanguage: + homeController.selectedLanguage.value) + .sentences + .filterDrawerDueTill + }, + { + 'key': 'Priority', + 'label': SentenceManager( + currentLanguage: + homeController.selectedLanguage.value) + .sentences + .filterDrawerPriority + }, + { + 'key': 'Project', + 'label': SentenceManager( + currentLanguage: + homeController.selectedLanguage.value) + .sentences + .filterDrawerProject + }, + { + 'key': 'Tags', + 'label': SentenceManager( + currentLanguage: + homeController.selectedLanguage.value) + .sentences + .filterDrawerTags + }, + { + 'key': 'Urgency', + 'label': SentenceManager( + currentLanguage: + homeController.selectedLanguage.value) + .sentences + .filterDrawerUrgency + }, ]) Obx( - () => ChoiceChip( - label: (homeController.selectedSort.value - .startsWith(sort)) - ? Text( - homeController.selectedSort.value, - ) - : Text(sort), - selected: false, - onSelected: (_) { - if (homeController.selectedSort == '$sort+') { - homeController.selectSort('$sort-'); - } else if (homeController.selectedSort == - '$sort-') { - homeController.selectSort(sort); - } else { - homeController.selectSort('$sort+'); - } - }, - // labelStyle: GoogleFonts.poppins( - // color: AppSettings.isDarkMode - // ? TaskWarriorColors.black - // : TaskWarriorColors.white), - // backgroundColor: AppSettings.isDarkMode - // ? TaskWarriorColors - // .kLightSecondaryBackgroundColor - // : TaskWarriorColors.ksecondaryBackgroundColor, - ), + () { + final String key = option['key']!; + final String label = option['label']!; + final String value = + homeController.selectedSort.value; + final String display = value == '$key+' + ? '$label+' + : value == '$key-' + ? '$label-' + : label; + return ChoiceChip( + label: Text(display), + selected: false, + onSelected: (_) { + if (value == '$key+') { + homeController.selectSort('$key-'); + } else if (value == '$key-') { + homeController.selectSort(key); + } else { + homeController.selectSort('$key+'); + } + }, + ); + }, ) ], ), diff --git a/lib/app/modules/home/views/home_page_app_bar.dart b/lib/app/modules/home/views/home_page_app_bar.dart index 72643e2d..52a053be 100644 --- a/lib/app/modules/home/views/home_page_app_bar.dart +++ b/lib/app/modules/home/views/home_page_app_bar.dart @@ -186,7 +186,7 @@ class HomePageAppBar extends StatelessWidget implements PreferredSizeWidget { .sentences .homePageFetchingTasks); - await controller.refreshTasks(c, e); + await controller.fetchTasksFromDB(); ScaffoldMessenger.of(context) .hideCurrentSnackBar(); diff --git a/lib/app/modules/home/views/home_page_body.dart b/lib/app/modules/home/views/home_page_body.dart index 0e139462..179e7182 100644 --- a/lib/app/modules/home/views/home_page_body.dart +++ b/lib/app/modules/home/views/home_page_body.dart @@ -30,7 +30,25 @@ class HomePageBody extends StatelessWidget { child: Padding( padding: const EdgeInsets.only(left: 8.0, right: 8.0), child: Obx( - () => Column( + () { + // Read the replica list reactively here so the whole view rebuilds + // when tasks arrive. On launch, taskReplica flips true (triggering a + // rebuild) *before* the async FFI fetch populates tasksFromReplica; + // without a reactive read of the list itself, the populated tasks + // would never appear (the view stays on its initial empty build). + var replicaTasks = controller.tasksFromReplica.toList(); + // Apply the search text to the replica list. Unlike the local-taskc + // path (which reads the pre-filtered `searchedTasks`), this builder + // gets the raw list, so filter here. Reading `searchQuery`/ + // `searchVisible` inside the Obx makes it rebuild on each keystroke. + final query = controller.searchQuery.value.trim().toLowerCase(); + if (controller.searchVisible.value && query.isNotEmpty) { + replicaTasks = replicaTasks + .where((task) => + (task.description ?? '').toLowerCase().contains(query)) + .toList(); + } + return Column( children: [ if (controller.searchVisible.value) Container( @@ -134,14 +152,15 @@ class HomePageBody extends StatelessWidget { child: Expanded( child: Scrollbar( child: TaskReplicaViewBuilder( - replicaTasks: controller.tasksFromReplica, + replicaTasks: replicaTasks, pendingFilter: controller.pendingFilter.value, selectedSort: controller.selectedSort.value, project: controller.projectFilter.value, ), ))) ], - ), + ); + }, ), ), ), diff --git a/lib/app/modules/home/views/show_tasks.dart b/lib/app/modules/home/views/show_tasks.dart index 008b30fb..8421e18d 100644 --- a/lib/app/modules/home/views/show_tasks.dart +++ b/lib/app/modules/home/views/show_tasks.dart @@ -11,8 +11,6 @@ import 'package:taskwarrior/app/utils/themes/theme_extension.dart'; import 'package:taskwarrior/app/utils/language/sentence_manager.dart'; import 'package:taskwarrior/app/v3/db/task_database.dart'; import 'package:taskwarrior/app/v3/models/task.dart'; -import 'package:taskwarrior/app/v3/net/complete.dart'; -import 'package:taskwarrior/app/v3/net/delete.dart'; class TaskViewBuilder extends StatelessWidget { const TaskViewBuilder({ @@ -230,14 +228,12 @@ class TaskViewBuilder extends StatelessWidget { TaskDatabase taskDatabase = TaskDatabase(); await taskDatabase.open(); taskDatabase.markTaskAsCompleted(uuid); - completeTask('email', uuid); } void _markTaskAsDeleted(String uuid) async { TaskDatabase taskDatabase = TaskDatabase(); await taskDatabase.open(); taskDatabase.markTaskAsDeleted(uuid); - deleteTask('email', uuid); } Color _getPriorityColor(String priority) { diff --git a/lib/app/modules/home/views/show_tasks_replica.dart b/lib/app/modules/home/views/show_tasks_replica.dart index 642fbdff..90421e08 100644 --- a/lib/app/modules/home/views/show_tasks_replica.dart +++ b/lib/app/modules/home/views/show_tasks_replica.dart @@ -31,16 +31,13 @@ class TaskReplicaViewBuilder extends StatelessWidget { TaskwarriorColorTheme tColors = Theme.of(context).extension()!; - return Obx(() { - List tasks = List.from(replicaTasks); - if (project != null && project != 'All Projects') { + // Reactivity is handled by the parent Obx in home_page_body (which reads + // tasksFromReplica); this widget just renders the snapshot it's given, so + // it must NOT be an Obx (an Obx with no observable read throws ObxError). + List tasks = List.from(replicaTasks); + if (project != null && project!.isNotEmpty && project != 'All Projects') { tasks = tasks.where((task) => task.project == project).toList(); } - tasks.sort((a, b) { - final am = a.modified ?? 0; - final bm = b.modified ?? 0; - return bm.compareTo(am); - }); tasks = tasks.where((task) { if (pendingFilter) { return task.status == 'pending'; @@ -49,22 +46,59 @@ class TaskReplicaViewBuilder extends StatelessWidget { } }).toList(); + // Urgency is computed (TaskChampion doesn't store it) — precompute once + // per task against a single "now" so every comparison is consistent and + // we don't recompute inside the O(n log n) sort. + final bool sortingByUrgency = + selectedSort == 'Urgency+' || selectedSort == 'Urgency-'; + final DateTime now = DateTime.now().toUtc(); + final Map urgencyByUuid = sortingByUrgency + ? {for (final t in tasks) t.uuid: t.computeUrgency(clock: now)} + : const {}; + + // Sort by the selected column. All eight columns are backed: + // Created (entry), Modified, Start Time, Due till, Priority (by severity), + // Project, Tags (grouped by sorted tag list), and Urgency (computed). tasks.sort((a, b) { switch (selectedSort) { + case 'Created+': + return (a.entry ?? 0).compareTo(b.entry ?? 0); + case 'Created-': + return (b.entry ?? 0).compareTo(a.entry ?? 0); case 'Modified+': return (a.modified ?? 0).compareTo(b.modified ?? 0); case 'Modified-': return (b.modified ?? 0).compareTo(a.modified ?? 0); + case 'Start Time+': + return (a.start ?? '').compareTo(b.start ?? ''); + case 'Start Time-': + return (b.start ?? '').compareTo(a.start ?? ''); case 'Due till+': return (a.due ?? '').compareTo(b.due ?? ''); case 'Due till-': return (b.due ?? '').compareTo(a.due ?? ''); case 'Priority+': - return (a.priority ?? '').compareTo(b.priority ?? ''); + return _priorityRank(a.priority) + .compareTo(_priorityRank(b.priority)); case 'Priority-': - return (b.priority ?? '').compareTo(a.priority ?? ''); + return _priorityRank(b.priority) + .compareTo(_priorityRank(a.priority)); + case 'Project+': + return (a.project ?? '').compareTo(b.project ?? ''); + case 'Project-': + return (b.project ?? '').compareTo(a.project ?? ''); + case 'Tags+': + return _tagKey(a).compareTo(_tagKey(b)); + case 'Tags-': + return _tagKey(b).compareTo(_tagKey(a)); + case 'Urgency+': + return (urgencyByUuid[a.uuid] ?? 0.0) + .compareTo(urgencyByUuid[b.uuid] ?? 0.0); + case 'Urgency-': + return (urgencyByUuid[b.uuid] ?? 0.0) + .compareTo(urgencyByUuid[a.uuid] ?? 0.0); default: - return 0; + return (b.modified ?? 0).compareTo(a.modified ?? 0); } }); @@ -203,7 +237,29 @@ class TaskReplicaViewBuilder extends StatelessWidget { }, ), ); - }); + } + + // A stable key for Tags sort: the task's tags sorted alphabetically and + // joined, so tasks that share the same tags group together and untagged + // tasks (empty key) sort to one end. + String _tagKey(TaskForReplica task) { + final tags = [...(task.tags ?? const [])]..sort(); + return tags.join(' '); + } + + // Rank priorities by severity (H > M > L > none) so Priority sort is + // meaningful rather than alphabetical (which would order H < L < M). + int _priorityRank(String? priority) { + switch (priority) { + case 'H': + return 3; + case 'M': + return 2; + case 'L': + return 1; + default: + return 0; + } } Color _getPriorityColor(String priority) { diff --git a/lib/app/modules/manageTaskServer/controllers/manage_task_server_controller.dart b/lib/app/modules/manageTaskServer/controllers/manage_task_server_controller.dart index 2fe75484..42d194a0 100644 --- a/lib/app/modules/manageTaskServer/controllers/manage_task_server_controller.dart +++ b/lib/app/modules/manageTaskServer/controllers/manage_task_server_controller.dart @@ -9,6 +9,7 @@ import 'package:taskwarrior/app/models/storage/set_config.dart'; import 'package:taskwarrior/app/modules/home/controllers/home_controller.dart'; import 'package:taskwarrior/app/modules/splash/controllers/splash_controller.dart'; import 'package:taskwarrior/app/tour/manage_task_server_page_tour.dart'; +import 'package:taskwarrior/app/tour/safe_tour.dart'; import 'package:taskwarrior/app/utils/constants/taskwarrior_colors.dart'; import 'package:taskwarrior/app/utils/home_path/home_path.dart' as rc; import 'package:taskwarrior/app/utils/taskserver/taskserver.dart'; @@ -221,18 +222,20 @@ class ManageTaskServerController extends GetxController { void showManageTaskServerPageTour(BuildContext context) { Future.delayed( const Duration(milliseconds: 500), - () { - SaveTourStatus.getManageTaskServerTourStatus().then((value) => { - if (value == false) - { - tutorialCoachMark.show(context: context), - } - else - { - // ignore: avoid_print - print('User has seen this page'), - } - }); + () async { + final seen = await SaveTourStatus.getManageTaskServerTourStatus(); + if (seen) return; + await safeShowTour( + tutorialCoachMark: tutorialCoachMark, + context: context, + targetKeys: [ + configureTaskRC, + configureServerCertificate, + configureTaskServerKey, + configureYourCertificate, + ], + markSeen: () => SaveTourStatus.saveManageTaskServerTourStatus(true), + ); }, ); } diff --git a/lib/app/modules/manage_task_champion_creds/controllers/manage_task_champion_creds_controller.dart b/lib/app/modules/manage_task_champion_creds/controllers/manage_task_champion_creds_controller.dart index 8bf68577..e77a6ce7 100644 --- a/lib/app/modules/manage_task_champion_creds/controllers/manage_task_champion_creds_controller.dart +++ b/lib/app/modules/manage_task_champion_creds/controllers/manage_task_champion_creds_controller.dart @@ -1,17 +1,16 @@ -import 'dart:convert'; - import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:shared_preferences/shared_preferences.dart'; +import 'package:taskwarrior/app/modules/home/controllers/home_controller.dart'; import 'package:taskwarrior/app/modules/splash/controllers/splash_controller.dart'; import 'package:taskwarrior/app/utils/taskchampion/credentials_storage.dart'; -import 'package:taskwarrior/app/v3/net/origin.dart'; -import 'package:http/http.dart' as http; +import 'package:taskwarrior/app/v3/champion/replica.dart'; +import 'package:taskwarrior/rust_bridge/api.dart'; class ManageTaskChampionCredsController extends GetxController { final encryptionSecretController = TextEditingController(); final clientIdController = TextEditingController(); - final ccsyncBackendUrlController = TextEditingController(); + final syncServerUrlController = TextEditingController(); var profilesWidget = Get.find(); RxBool isCheckingCreds = false.obs; RxBool taskReplica = false.obs; @@ -25,50 +24,57 @@ class ManageTaskChampionCredsController extends GetxController { encryptionSecretController.text = await CredentialsStorage.getEncryptionSecret() ?? ''; clientIdController.text = await CredentialsStorage.getClientId() ?? ''; - ccsyncBackendUrlController.text = + syncServerUrlController.text = await CredentialsStorage.getApiUrl() ?? ''; final SharedPreferences prefs = await SharedPreferences.getInstance(); taskReplica.value = prefs.getBool('settings_taskr_repl') ?? false; } + /// Validates and persists sync credentials through a single path: the native + /// TaskChampion sync via the Rust FFI bridge. + /// + /// The entered credentials are validated with a real [sync_] *before* they + /// are persisted — a successful sync confirms they are valid, while invalid + /// credentials raise a Rust-level exception that surfaces here as a thrown + /// error. This ordering guarantees we never write unverified credentials into + /// the active profile. (Because validation is a live sync, saving requires + /// connectivity to the sync server.) Future saveCredentials() async { - if (taskReplica.value) { - profilesWidget.setTaskcCreds( - profilesWidget.currentProfile.value, - clientIdController.text, - encryptionSecretController.text, - ccsyncBackendUrlController.text); - return 0; - } isCheckingCreds.value = true; - String baseUrl = ccsyncBackendUrlController.text; - String uuid = clientIdController.text; - String encryptionSecret = encryptionSecretController.text; try { - String url = - '$baseUrl/tasks?email=email&origin=$origin&UUID=$uuid&encryptionSecret=$encryptionSecret'; - - var response = await http.get(Uri.parse(url), headers: { - "Content-Type": "application/json", - }).timeout(const Duration(seconds: 10000)); - debugPrint("Fetch tasks response: ${response.statusCode}"); - debugPrint("Fetch tasks body: ${response.body}"); - if (response.statusCode == 200) { - List allTasks = jsonDecode(response.body); - debugPrint(allTasks.toString()); - profilesWidget.setTaskcCreds( - profilesWidget.currentProfile.value, - clientIdController.text, - encryptionSecretController.text, - ccsyncBackendUrlController.text); - - isCheckingCreds.value = false; - return 0; - } else { - throw Exception('Failed to load tasks'); + final String replicaPath = await Replica.getReplicaPath(); + await sync_( + taskdbDirPath: replicaPath, + url: syncServerUrlController.text, + clientId: clientIdController.text, + encryptionSecret: encryptionSecretController.text, + ); + // Only persist after the sync has confirmed the credentials are valid. + profilesWidget.setTaskcCreds( + profilesWidget.currentProfile.value, + clientIdController.text, + encryptionSecretController.text, + syncServerUrlController.text, + ); + // Populate the task list immediately. The home list is otherwise only + // filled by an explicit refresh, so right after configuring credentials + // the user would see an empty list (until a manual refresh or restart) + // even though the sync above already succeeded. Reload the sync-mode flag + // and sync so the freshly-synced tasks appear at once. Guarded so a + // transient refresh failure never flips an already-successful save. + try { + if (Get.isRegistered()) { + final homeController = Get.find(); + await homeController.fetchTasksFromDB(); + await homeController.refreshReplicaTasks(); + } + } catch (refreshErr) { + debugPrint('Post-save replica refresh failed: $refreshErr'); } - } catch (e, s) { - debugPrint('Error fetching tasks: $e\n $s'); + isCheckingCreds.value = false; + return 0; + } catch (err) { + debugPrint('Credential check failed: $err'); isCheckingCreds.value = false; return 1; } @@ -78,7 +84,7 @@ class ManageTaskChampionCredsController extends GetxController { void onClose() { encryptionSecretController.dispose(); clientIdController.dispose(); - ccsyncBackendUrlController.dispose(); + syncServerUrlController.dispose(); super.onClose(); } } diff --git a/lib/app/modules/manage_task_champion_creds/views/manage_task_champion_creds_view.dart b/lib/app/modules/manage_task_champion_creds/views/manage_task_champion_creds_view.dart index e9b98351..fe9a6da1 100644 --- a/lib/app/modules/manage_task_champion_creds/views/manage_task_champion_creds_view.dart +++ b/lib/app/modules/manage_task_champion_creds/views/manage_task_champion_creds_view.dart @@ -36,22 +36,7 @@ class ManageTaskChampionCredsView ), ], ), - actions: [ - // IconButton( - // icon: Icon( - // Icons.info, - // color: TaskWarriorColors.white, - // ), - // onPressed: () async { - // String url = !controller.taskReplica.value - // ? "https://github.com/its-me-abhishek/ccsync" - // : "https://github.com/GothenburgBitFactory/taskchampion"; - // if (!await launchUrl(Uri.parse(url))) { - // throw Exception('Could not launch $url'); - // } - // }, - // ), - ], + actions: const [], leading: IconButton( icon: Icon(Icons.arrow_back, color: TaskWarriorColors.white), onPressed: () => Get.back(), @@ -74,7 +59,7 @@ class ManageTaskChampionCredsView labelText: SentenceManager( currentLanguage: AppSettings.selectedLanguage) .sentences - .ccsyncClientId, + .syncServerClientId, labelStyle: TextStyle(color: tColors.primaryTextColor), border: const OutlineInputBorder(), ), @@ -93,26 +78,18 @@ class ManageTaskChampionCredsView ), ), const SizedBox(height: 10), - Obx(() => TextField( - style: TextStyle(color: tColors.primaryTextColor), - controller: controller.ccsyncBackendUrlController, - decoration: InputDecoration( - labelText: controller.taskReplica.value - ? SentenceManager( - currentLanguage: - AppSettings.selectedLanguage) - .sentences - .taskchampionBackendUrl - : SentenceManager( - currentLanguage: - AppSettings.selectedLanguage) - .sentences - .ccsyncBackendUrl, - labelStyle: - TextStyle(color: tColors.primaryTextColor), - border: const OutlineInputBorder(), - ), - )), + TextField( + style: TextStyle(color: tColors.primaryTextColor), + controller: controller.syncServerUrlController, + decoration: InputDecoration( + labelText: SentenceManager( + currentLanguage: AppSettings.selectedLanguage) + .sentences + .syncServerBackendUrl, + labelStyle: TextStyle(color: tColors.primaryTextColor), + border: const OutlineInputBorder(), + ), + ), const SizedBox(height: 20), Obx(() => SizedBox( width: double.infinity, @@ -196,7 +173,7 @@ class ManageTaskChampionCredsView SentenceManager( currentLanguage: AppSettings.selectedLanguage) .sentences - .ccsyncEasySyncTitle, + .syncServerEasySyncTitle, style: GoogleFonts.poppins( fontSize: 18, fontWeight: FontWeight.w600, @@ -208,7 +185,7 @@ class ManageTaskChampionCredsView SentenceManager( currentLanguage: AppSettings.selectedLanguage) .sentences - .ccsyncIntro, + .syncServerIntro, style: TextStyle( fontSize: 14, color: tColors.primaryTextColor?.withValues(alpha: 0.8), @@ -220,7 +197,7 @@ class ManageTaskChampionCredsView SentenceManager( currentLanguage: AppSettings.selectedLanguage) .sentences - .ccsyncLoginInstruction, + .syncServerLoginInstruction, style: TextStyle( fontSize: 14, color: tColors.primaryTextColor?.withValues(alpha: 0.8), @@ -235,7 +212,7 @@ class ManageTaskChampionCredsView SentenceManager( currentLanguage: AppSettings.selectedLanguage) .sentences - .ccsyncOpenButton, + .syncServerOpenButton, style: TextStyle(color: tColors.primaryTextColor), ), style: OutlinedButton.styleFrom( @@ -261,7 +238,7 @@ class ManageTaskChampionCredsView SentenceManager( currentLanguage: AppSettings.selectedLanguage) .sentences - .ccsyncSelfHosted, + .syncServerSelfHosted, style: TextStyle( fontSize: 13, color: tColors.primaryTextColor?.withValues(alpha: 0.6), diff --git a/lib/app/modules/profile/controllers/profile_controller.dart b/lib/app/modules/profile/controllers/profile_controller.dart index 50e6e5cd..0c0303a5 100644 --- a/lib/app/modules/profile/controllers/profile_controller.dart +++ b/lib/app/modules/profile/controllers/profile_controller.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:taskwarrior/app/modules/splash/controllers/splash_controller.dart'; import 'package:taskwarrior/app/tour/profile_page_tour.dart'; +import 'package:taskwarrior/app/tour/safe_tour.dart'; import 'package:taskwarrior/app/utils/constants/taskwarrior_colors.dart'; import 'package:taskwarrior/app/utils/app_settings/app_settings.dart'; import 'package:tutorial_coach_mark/tutorial_coach_mark.dart'; @@ -45,18 +46,19 @@ class ProfileController extends GetxController { void showProfilePageTour(BuildContext context) { Future.delayed( const Duration(milliseconds: 500), - () { - SaveTourStatus.getProfileTourStatus().then((value) => { - if (value == false) - { - tutorialCoachMark.show(context: context), - } - else - { - // ignore: avoid_print - print('User has seen this page'), - } - }); + () async { + final seen = await SaveTourStatus.getProfileTourStatus(); + if (seen) return; + await safeShowTour( + tutorialCoachMark: tutorialCoachMark, + context: context, + targetKeys: [ + currentProfileKey, + addNewProfileKey, + manageSelectedProfileKey, + ], + markSeen: () => SaveTourStatus.saveProfileTourStatus(true), + ); }, ); } diff --git a/lib/app/modules/profile/views/profile_view.dart b/lib/app/modules/profile/views/profile_view.dart index a50eec66..7f9c97d6 100644 --- a/lib/app/modules/profile/views/profile_view.dart +++ b/lib/app/modules/profile/views/profile_view.dart @@ -264,17 +264,6 @@ class ProfileView extends GetView { }); }, ), - // CCSync v3 is deprecated, so hiding it for now - // RadioListTile( - // title: const Text('CCSync (v3)'), - // value: 'TW3', - // groupValue: selectedMode, - // onChanged: (String? value) { - // setState(() { - // selectedMode = value; - // }); - // }, - // ), RadioListTile( title: const Text('TaskServer'), value: 'TW2', @@ -327,8 +316,9 @@ class ProfileView extends GetView { AppSettings.selectedLanguage) .sentences .profilePageSuccessfullyChangedProfileModeTo + - ((selectedMode ?? "") == "TW3" - ? "CCSync" + " " + + ((selectedMode ?? "") == "TW3C" + ? "Taskchampion" : "Taskserver"), style: TextStyle( color: tColors.primaryTextColor, diff --git a/lib/app/modules/profile/views/profiles_list.dart b/lib/app/modules/profile/views/profiles_list.dart index 93d867a6..20ebbcd5 100644 --- a/lib/app/modules/profile/views/profiles_list.dart +++ b/lib/app/modules/profile/views/profiles_list.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; +import 'package:taskwarrior/app/modules/splash/controllers/splash_controller.dart'; import 'package:taskwarrior/app/utils/constants/taskwarrior_colors.dart'; import 'package:taskwarrior/app/utils/app_settings/app_settings.dart'; import 'package:taskwarrior/app/utils/language/sentence_manager.dart'; @@ -133,10 +134,20 @@ class ProfilesList extends StatelessWidget { ? TaskWarriorColors.kprimaryTextColor : TaskWarriorColors.kLightPrimaryTextColor), title: Text( - SentenceManager( - currentLanguage: AppSettings.selectedLanguage) - .sentences - .profilePageConfigureTaskserver, // New descriptive text + // Label the config option for the profile's actual sync + // mode: a Taskchampion (v3) profile configures + // Taskchampion, not the Taskserver. + Get.find().getMode(profileId) == 'TW3C' + ? SentenceManager( + currentLanguage: + AppSettings.selectedLanguage) + .sentences + .configureTaskchampion + : SentenceManager( + currentLanguage: + AppSettings.selectedLanguage) + .sentences + .profilePageConfigureTaskserver, style: TextStyle( color: AppSettings.isDarkMode ? TaskWarriorColors.kprimaryTextColor diff --git a/lib/app/modules/taskc_details/controllers/taskc_details_controller.dart b/lib/app/modules/taskc_details/controllers/taskc_details_controller.dart index 6116cf7e..f925dba3 100644 --- a/lib/app/modules/taskc_details/controllers/taskc_details_controller.dart +++ b/lib/app/modules/taskc_details/controllers/taskc_details_controller.dart @@ -13,7 +13,6 @@ import 'package:taskwarrior/app/v3/models/annotation.dart'; import 'package:taskwarrior/app/v3/models/task.dart'; import 'package:taskwarrior/app/v3/champion/replica.dart'; import 'package:taskwarrior/app/v3/champion/models/task_for_replica.dart'; -import 'package:taskwarrior/app/v3/net/modify.dart'; enum UnsavedChangesAction { save, discard, cancel } @@ -35,6 +34,9 @@ class TaskcDetailsController extends GetxController { late RxString rtype; late RxString recur; late RxList annotations; + // Blocking state surfaced by the Rust serializer (replica tasks only). + late RxBool isBlocked; + late RxBool isBlocking; late RxList previousTags = [].obs; @override @@ -64,6 +66,8 @@ class TaskcDetailsController extends GetxController { rtype = "".obs; recur = "".obs; annotations = [].obs; + isBlocked = false.obs; + isBlocking = false.obs; } else if (task is TaskForReplica) { description = (task.description ?? '').obs; project = (task.project ?? 'None').obs; @@ -81,10 +85,13 @@ class TaskcDetailsController extends GetxController { ? task.tags!.map((e) => e.toString()).toList().obs : [].obs; previousTags = tags.toList().obs; - depends = "".split(",").obs; + // Attributes now surfaced by the Rust serializer. + depends = (task.depends ?? []).obs; rtype = "".obs; - recur = "".obs; - annotations = [].obs; + recur = (task.recur ?? "").obs; + annotations = (task.annotations ?? []).obs; + isBlocked = (task.isBlocked ?? false).obs; + isBlocking = (task.isBlocking ?? false).obs; } else { // Fallback description = ''.obs; @@ -100,6 +107,8 @@ class TaskcDetailsController extends GetxController { rtype = "".obs; recur = "".obs; annotations = [].obs; + isBlocked = false.obs; + isBlocking = false.obs; } } @@ -249,16 +258,6 @@ class TaskcDetailsController extends GetxController { hasChanges.value = false; debugPrint('Task saved in local DB ${description.string}'); processTagsLists(); - await modifyTaskOnTaskwarrior( - description.string, - project.string, - DateTime.parse(due.string).toIso8601String(), - priority.string, - status.string, - initialTask.uuid!, - initialTask.id.toString(), - tags.toList(), - ); } else if (initialTask is TaskForReplica) { debugPrint( 'Saving replica task changes... status ${status.string} ${tags.join(", ")}'); diff --git a/lib/app/modules/taskc_details/views/taskc_details_view.dart b/lib/app/modules/taskc_details/views/taskc_details_view.dart index 2203274f..ba2a12f3 100644 --- a/lib/app/modules/taskc_details/views/taskc_details_view.dart +++ b/lib/app/modules/taskc_details/views/taskc_details_view.dart @@ -111,6 +111,45 @@ class TaskcDetailsView extends GetView { controller.tags.join(', '), (value) => controller.updateListField(controller.tags, value), ), + // Attributes surfaced by the enriched Rust serializer (D2). + // Replica tasks only; read-only (the mobile UI cannot edit + // dependencies/annotations yet). + if (controller.isReplicaTask) ...[ + _buildDetail( + context, + 'Blocked:', + controller.isBlocked.value ? 'Yes' : 'No', + ), + _buildDetail( + context, + 'Blocking:', + controller.isBlocking.value ? 'Yes' : 'No', + ), + _buildDetail( + context, + 'Depends:', + controller.depends.isEmpty + ? 'None' + : controller.depends.join(', '), + ), + _buildDetail( + context, + 'Recur:', + controller.recur.value.isEmpty + ? 'None' + : controller.recur.value, + ), + _buildDetail( + context, + 'Annotations:', + controller.annotations.isEmpty + ? 'None' + : controller.annotations + .map((a) => + '• ${a.description ?? ''}${(a.entry != null && a.entry!.isNotEmpty) ? ' (${a.entry})' : ''}') + .join('\n'), + ), + ], if (controller.isLocalTask) ...[ _buildDetail( context, diff --git a/lib/app/tour/safe_tour.dart b/lib/app/tour/safe_tour.dart new file mode 100644 index 00000000..fa13c457 --- /dev/null +++ b/lib/app/tour/safe_tour.dart @@ -0,0 +1,36 @@ +import 'package:flutter/widgets.dart'; +import 'package:tutorial_coach_mark/tutorial_coach_mark.dart'; + +/// Safely shows a coach-mark tour. +/// +/// `tutorial_coach_mark` throws +/// `FormatException: It was not possible to obtain target position (null)` +/// when any target's [GlobalKey] is not currently laid out. This happens when +/// the screen changes during the pre-show delay (e.g. the user navigates +/// deeper before the tour fires): the target widgets are unmounted, so their +/// render boxes are null. +/// +/// This helper guards against that: it only calls `show()` when the context is +/// still mounted and every target key has a live element. Otherwise — or if +/// `show()` throws anyway — it marks the tour as seen via [markSeen] so a +/// failed attempt never surfaces an exception or loops forever (the tour's own +/// `onFinish` would never run to persist the flag). +Future safeShowTour({ + required TutorialCoachMark tutorialCoachMark, + required BuildContext context, + required List targetKeys, + Future Function()? markSeen, +}) async { + final bool allMounted = + targetKeys.every((k) => k.currentContext != null); + if (!context.mounted || !allMounted) { + await markSeen?.call(); + return; + } + try { + tutorialCoachMark.show(context: context); + } catch (_) { + // Defensive: never let a tour failure bubble up or repeat. + await markSeen?.call(); + } +} diff --git a/lib/app/utils/language/bengali_sentences.dart b/lib/app/utils/language/bengali_sentences.dart index 4761031c..46d9f3fb 100644 --- a/lib/app/utils/language/bengali_sentences.dart +++ b/lib/app/utils/language/bengali_sentences.dart @@ -2,17 +2,17 @@ import 'package:taskwarrior/app/utils/language/sentences.dart'; class BengaliSentences extends Sentences { @override - String get ccsyncLoginInstruction => - 'CCSync-এ লগইন করুন, আপনার শংসাপত্র কপি করুন এবং উপরে পেস্ট করুন।'; + String get syncServerLoginInstruction => + 'TaskChampion-এ লগইন করুন, আপনার শংসাপত্র কপি করুন এবং উপরে পেস্ট করুন।'; @override - String get ccsyncEasySyncTitle => 'সহজ সিঙ্কের জন্য CCSync ব্যবহার করুন'; + String get syncServerEasySyncTitle => 'সহজ সিঙ্কের জন্য TaskChampion ব্যবহার করুন'; @override - String get ccsyncOpenButton => 'CCSync খুলুন'; + String get syncServerOpenButton => 'TaskChampion খুলুন'; @override - String get ccsyncIntro => - 'CCSync TaskChampion ব্যবহার করে আপনার কাজগুলি একাধিক ডিভাইসে নির্বিঘ্নে সিঙ্ক করে। আপনি যেকোনো ব্রাউজার থেকে আপনার কাজগুলি পরিচালনা করার জন্য একটি ওয়েব ড্যাশবোর্ডও পান।'; + String get syncServerIntro => + 'TaskChampion ব্যবহার করে আপনার কাজগুলি একাধিক ডিভাইসে নির্বিঘ্নে সিঙ্ক করে। আপনি যেকোনো ব্রাউজার থেকে আপনার কাজগুলি পরিচালনা করার জন্য একটি ওয়েব ড্যাশবোর্ডও পান।'; @override - String get ccsyncSelfHosted => + String get syncServerSelfHosted => 'অথবা একটি স্ব-হোস্টেড TaskChampion সিঙ্ক সার্ভার থেকে আপনার নিজস্ব শংসাপত্র আনুন।'; @override String get helloWorld => 'হ্যালো বিশ্ব!'; @@ -204,13 +204,13 @@ class BengaliSentences extends Sentences { @override String get taskchampionTileDescription => - 'Taskwarrior সিঙ্কিং CCSync বা Taskchampion সিঙ্ক সার্ভারে পরিবর্তন করুন'; + 'Taskwarrior সিঙ্কিং TaskChampion সিঙ্ক সার্ভারে পরিবর্তন করুন'; @override String get taskchampionTileTitle => 'Taskchampion সিঙ্ক'; @override - String get ccsyncCredentials => 'CCSync ক্রেডেনশিয়াল'; + String get syncServerCredentials => 'TaskChampion ক্রেডেনশিয়াল'; @override String get deleteTaskConfirmation => 'টাস্ক মুছুন'; @@ -661,9 +661,9 @@ class BengaliSentences extends Sentences { @override String get encryptionSecret => 'এনক্রিপশন সিক্রেট'; @override - String get ccsyncBackendUrl => 'CCSync ব্যাকএন্ড URL'; + String get syncServerBackendUrl => 'TaskChampion ব্যাকএন্ড URL'; @override - String get ccsyncClientId => 'ক্লায়েন্ট আইডি'; + String get syncServerClientId => 'ক্লায়েন্ট আইডি'; @override String get success => 'সফল হয়েছে'; @override @@ -686,6 +686,4 @@ class BengaliSentences extends Sentences { String get storageAndData => 'স্টোরেজ এবং ডাটা'; @override String get advanced => 'উন্নত'; - @override - String get taskchampionBackendUrl => 'Taskchampion ব্যাকএন্ড URL'; } diff --git a/lib/app/utils/language/english_sentences.dart b/lib/app/utils/language/english_sentences.dart index a6b0fb00..716a3e96 100644 --- a/lib/app/utils/language/english_sentences.dart +++ b/lib/app/utils/language/english_sentences.dart @@ -2,17 +2,17 @@ import 'package:taskwarrior/app/utils/language/sentences.dart'; class EnglishSentences extends Sentences { @override - String get ccsyncLoginInstruction => - 'Login to CCSync, copy your credentials, and paste them above.'; + String get syncServerLoginInstruction => + 'Login to TaskChampion, copy your credentials, and paste them above.'; @override - String get ccsyncEasySyncTitle => 'Use CCSync for Easy Sync'; + String get syncServerEasySyncTitle => 'Use TaskChampion for Easy Sync'; @override - String get ccsyncOpenButton => 'Open CCSync'; + String get syncServerOpenButton => 'Open TaskChampion'; @override - String get ccsyncIntro => - 'CCSync uses TaskChampion to sync your tasks across multiple devices seamlessly. You also get a web dashboard to manage your tasks from any browser.'; + String get syncServerIntro => + 'TaskChampion syncs your tasks across multiple devices seamlessly. You also get a web dashboard to manage your tasks from any browser.'; @override - String get ccsyncSelfHosted => + String get syncServerSelfHosted => 'Or bring your own credentials from a self-hosted TaskChampion sync server.'; @override String get helloWorld => 'Hello, World!'; @@ -219,12 +219,12 @@ class EnglishSentences extends Sentences { @override String get taskchampionTileDescription => - 'Switch to Taskwarrior sync with CCSync or Taskchampion Sync Server'; + 'Switch to Taskwarrior sync with a TaskChampion sync server'; @override String get taskchampionTileTitle => 'Taskchampion sync'; @override - String get ccsyncCredentials => 'CCync credentials'; + String get syncServerCredentials => 'TaskChampion credentials'; @override String get deleteTaskConfirmation => 'Delete Tasks'; @@ -650,9 +650,9 @@ class EnglishSentences extends Sentences { @override String get encryptionSecret => 'Encryption Secret'; @override - String get ccsyncBackendUrl => 'CCSync Backend URL'; + String get syncServerBackendUrl => 'TaskChampion Backend URL'; @override - String get ccsyncClientId => 'Client ID'; + String get syncServerClientId => 'Client ID'; @override String get success => 'Success'; @override @@ -675,6 +675,4 @@ class EnglishSentences extends Sentences { String get storageAndData => 'Storage and Data'; @override String get advanced => 'Advanced'; - @override - String get taskchampionBackendUrl => 'Taskchampion URL'; } diff --git a/lib/app/utils/language/french_sentences.dart b/lib/app/utils/language/french_sentences.dart index 788a0cb6..d1f15411 100644 --- a/lib/app/utils/language/french_sentences.dart +++ b/lib/app/utils/language/french_sentences.dart @@ -2,18 +2,18 @@ import 'package:taskwarrior/app/utils/language/sentences.dart'; class FrenchSentences extends Sentences { @override - String get ccsyncLoginInstruction => - 'Connectez-vous à CCSync, copiez vos identifiants et collez-les ci-dessus.'; + String get syncServerLoginInstruction => + 'Connectez-vous à TaskChampion, copiez vos identifiants et collez-les ci-dessus.'; @override - String get ccsyncEasySyncTitle => - 'Utilisez CCSync pour une synchronisation facile'; + String get syncServerEasySyncTitle => + 'Utilisez TaskChampion pour une synchronisation facile'; @override - String get ccsyncOpenButton => 'Ouvrir CCSync'; + String get syncServerOpenButton => 'Ouvrir TaskChampion'; @override - String get ccsyncIntro => - 'CCSync utilise TaskChampion pour synchroniser vos tâches sur plusieurs appareils sans effort. Vous bénéficiez également d’un tableau de bord web pour gérer vos tâches depuis n’importe quel navigateur.'; + String get syncServerIntro => + 'TaskChampion synchronise vos tâches sur plusieurs appareils sans effort. Vous bénéficiez également d’un tableau de bord web pour gérer vos tâches depuis n’importe quel navigateur.'; @override - String get ccsyncSelfHosted => + String get syncServerSelfHosted => 'Ou utilisez vos propres identifiants depuis un serveur TaskChampion auto-hébergé.'; @override String get helloWorld => 'Bonjour, le monde!'; @@ -208,13 +208,13 @@ class FrenchSentences extends Sentences { @override String get taskchampionTileDescription => - 'Basculez la synchronisation de Taskwarrior vers le serveur de synchronisation CCSync ou Taskchampion'; + 'Basculez la synchronisation de Taskwarrior vers le serveur de synchronisation TaskChampion'; @override String get taskchampionTileTitle => 'Synchronisation Taskchampion'; @override - String get ccsyncCredentials => 'Identifiants CCSync'; + String get syncServerCredentials => 'Identifiants TaskChampion'; @override String get deleteTaskConfirmation => 'Supprimer la tâche'; @@ -677,9 +677,9 @@ class FrenchSentences extends Sentences { @override String get encryptionSecret => 'Secret de chiffrement'; @override - String get ccsyncBackendUrl => 'URL du backend CCSync'; + String get syncServerBackendUrl => 'URL du backend TaskChampion'; @override - String get ccsyncClientId => 'ID client'; + String get syncServerClientId => 'ID client'; @override String get success => 'Succès'; @override @@ -705,6 +705,4 @@ class FrenchSentences extends Sentences { String get storageAndData => 'Stockage et données'; @override String get advanced => 'Avancé'; - @override - String get taskchampionBackendUrl => 'URL de Taskchampion'; } diff --git a/lib/app/utils/language/german_sentences.dart b/lib/app/utils/language/german_sentences.dart index c38ad4cb..ed2ef0f2 100644 --- a/lib/app/utils/language/german_sentences.dart +++ b/lib/app/utils/language/german_sentences.dart @@ -2,17 +2,17 @@ import 'package:taskwarrior/app/utils/language/sentences.dart'; class GermanSentences extends Sentences { @override - String get ccsyncLoginInstruction => - 'Melde dich bei CCSync an, kopiere deine Anmeldedaten und füge sie oben ein.'; + String get syncServerLoginInstruction => + 'Melde dich bei TaskChampion an, kopiere deine Anmeldedaten und füge sie oben ein.'; @override - String get ccsyncEasySyncTitle => 'CCSync nutzen für einfachen Sync'; + String get syncServerEasySyncTitle => 'TaskChampion nutzen für einfachen Sync'; @override - String get ccsyncOpenButton => 'CCSync öffnen'; + String get syncServerOpenButton => 'TaskChampion öffnen'; @override - String get ccsyncIntro => - 'CCSync nutzt TaskChampion, um Aufgaben nahtlos über mehrere Geräte hinweg zu synchronisieren. Außerdem erhälst du ein Web-Dashboard, über das du deine Aufgaben von jedem Browser aus verwalten kannst.'; + String get syncServerIntro => + 'TaskChampion synchronisiert deine Aufgaben nahtlos über mehrere Geräte hinweg. Außerdem erhälst du ein Web-Dashboard, über das du deine Aufgaben von jedem Browser aus verwalten kannst.'; @override - String get ccsyncSelfHosted => + String get syncServerSelfHosted => 'Oder bringe deine eigenen Anmeldedaten von einem selbst gehosteten TaskChampion-Synchronisierungsserver mit.'; @override String get helloWorld => 'Hallo Welt!'; @@ -219,12 +219,12 @@ class GermanSentences extends Sentences { @override String get taskchampionTileDescription => - 'Wechsel zu Taskwarrior Sync mit CCSync oder Taskchampion Sync Server'; + 'Wechsel zu Taskwarrior Sync mit einem TaskChampion Sync Server'; @override String get taskchampionTileTitle => 'Taskchampion Sync'; @override - String get ccsyncCredentials => 'CCync Anmeldedaten'; + String get syncServerCredentials => 'TaskChampion Anmeldedaten'; @override String get deleteTaskConfirmation => 'Aufgaben löschen'; @@ -650,9 +650,9 @@ class GermanSentences extends Sentences { @override String get encryptionSecret => 'Encryption Secret'; @override - String get ccsyncBackendUrl => 'CCSync Backend URL'; + String get syncServerBackendUrl => 'TaskChampion Backend URL'; @override - String get ccsyncClientId => 'Client ID'; + String get syncServerClientId => 'Client ID'; @override String get success => 'Erfolg'; @override @@ -675,6 +675,4 @@ class GermanSentences extends Sentences { String get storageAndData => 'Speicher und Daten'; @override String get advanced => 'Fortgeschritten'; - @override - String get taskchampionBackendUrl => 'Taskchampion URL'; } diff --git a/lib/app/utils/language/hindi_sentences.dart b/lib/app/utils/language/hindi_sentences.dart index 6b5c428b..2252005a 100644 --- a/lib/app/utils/language/hindi_sentences.dart +++ b/lib/app/utils/language/hindi_sentences.dart @@ -2,17 +2,17 @@ import 'package:taskwarrior/app/utils/language/sentences.dart'; class HindiSentences extends Sentences { @override - String get ccsyncLoginInstruction => - 'CCSync में लॉगिन करें, अपनी क्रेडेंशियल्स कॉपी करें, और उन्हें ऊपर पेस्ट करें।'; + String get syncServerLoginInstruction => + 'TaskChampion में लॉगिन करें, अपनी क्रेडेंशियल्स कॉपी करें, और उन्हें ऊपर पेस्ट करें।'; @override - String get ccsyncEasySyncTitle => 'आसान सिंक के लिए CCSync का उपयोग करें'; + String get syncServerEasySyncTitle => 'आसान सिंक के लिए TaskChampion का उपयोग करें'; @override - String get ccsyncOpenButton => 'CCSync खोलें'; + String get syncServerOpenButton => 'TaskChampion खोलें'; @override - String get ccsyncIntro => - 'CCSync आपके कार्यों को कई डिवाइसों पर TaskChampion के माध्यम से निर्बाध रूप से सिंक करता है। आपको किसी भी ब्राउज़र से अपने कार्यों को प्रबंधित करने के लिए एक वेब डैशबोर्ड भी मिलता है।'; + String get syncServerIntro => + 'TaskChampion आपके कार्यों को कई डिवाइसों पर निर्बाध रूप से सिंक करता है। आपको किसी भी ब्राउज़र से अपने कार्यों को प्रबंधित करने के लिए एक वेब डैशबोर्ड भी मिलता है।'; @override - String get ccsyncSelfHosted => + String get syncServerSelfHosted => 'या अपने स्वयं के TaskChampion सिंक सर्वर से क्रेडेंशियल्स लाएँ।'; @override String get helloWorld => 'नमस्ते दुनिया!'; @@ -220,13 +220,13 @@ class HindiSentences extends Sentences { @override String get taskchampionTileDescription => - 'CCSync या Taskchampion सिंक सर्वर के साथ Taskwarrior सिंक पर स्विच करें'; + 'TaskChampion सिंक सर्वर के साथ Taskwarrior सिंक पर स्विच करें'; @override String get taskchampionTileTitle => 'Taskchampion सिंक'; @override - String get ccsyncCredentials => 'CCync क्रेडेन्शियल'; + String get syncServerCredentials => 'TaskChampion क्रेडेन्शियल'; @override String get deleteTaskConfirmation => 'कार्य हटाएं'; @@ -638,9 +638,9 @@ class HindiSentences extends Sentences { @override String get encryptionSecret => 'एन्क्रिप्शन सीक्रेट'; @override - String get ccsyncBackendUrl => 'CCSync बैकएंड URL'; + String get syncServerBackendUrl => 'TaskChampion बैकएंड URL'; @override - String get ccsyncClientId => 'क्लाइंट आईडी'; + String get syncServerClientId => 'क्लाइंट आईडी'; @override String get success => 'सफलता'; @override @@ -664,6 +664,4 @@ class HindiSentences extends Sentences { String get storageAndData => 'स्टोरेज और डेटा'; @override String get advanced => 'अड्वांस्ड'; - @override - String get taskchampionBackendUrl => 'Taskchampion URL'; } diff --git a/lib/app/utils/language/marathi_sentences.dart b/lib/app/utils/language/marathi_sentences.dart index b7045742..a96e4959 100644 --- a/lib/app/utils/language/marathi_sentences.dart +++ b/lib/app/utils/language/marathi_sentences.dart @@ -2,18 +2,18 @@ import 'package:taskwarrior/app/utils/language/sentences.dart'; class MarathiSentences extends Sentences { @override - String get ccsyncLoginInstruction => - 'CCSync मध्ये लॉगिन करा, तुमची क्रेडेन्शियल्स कॉपी करा आणि वर पेस्ट करा.'; + String get syncServerLoginInstruction => + 'TaskChampion मध्ये लॉगिन करा, तुमची क्रेडेन्शियल्स कॉपी करा आणि वर पेस्ट करा.'; @override - String get ccsyncEasySyncTitle => 'सोप्या सिंकसाठी CCSync वापरा'; + String get syncServerEasySyncTitle => 'सोप्या सिंकसाठी TaskChampion वापरा'; @override - String get ccsyncOpenButton => 'CCSync उघडा'; + String get syncServerOpenButton => 'TaskChampion उघडा'; @override - String get ccsyncIntro => - 'CCSync TaskChampion वापरून तुमची कामे अनेक उपकरणांवर सहजपणे सिंक करते. तुम्हाला कोणत्याही ब्राउझरमधून तुमची कामे व्यवस्थापित करण्यासाठी वेब डॅशबोर्ड देखील मिळतो.'; + String get syncServerIntro => + 'TaskChampion वापरून तुमची कामे अनेक उपकरणांवर सहजपणे सिंक करते. तुम्हाला कोणत्याही ब्राउझरमधून तुमची कामे व्यवस्थापित करण्यासाठी वेब डॅशबोर्ड देखील मिळतो.'; @override - String get ccsyncSelfHosted => + String get syncServerSelfHosted => 'किंवा स्वतःच्या TaskChampion सिंक सर्व्हरमधून तुमची क्रेडेन्शियल्स वापरा.'; @override String get helloWorld => 'नमस्कार, जग!'; @@ -206,13 +206,13 @@ class MarathiSentences extends Sentences { @override String get taskchampionTileDescription => - 'CCSync किंवा Taskchampion Sync Server सह Taskwarrior सिंक वर स्विच करा'; + 'TaskChampion Sync Server सह Taskwarrior सिंक वर स्विच करा'; @override String get taskchampionTileTitle => 'Taskchampion सिंक'; @override - String get ccsyncCredentials => 'CCync क्रेडेन्शियल'; + String get syncServerCredentials => 'TaskChampion क्रेडेन्शियल'; @override String get deleteTaskConfirmation => 'कार्य हटवा'; @@ -661,9 +661,9 @@ class MarathiSentences extends Sentences { @override String get encryptionSecret => 'एन्क्रिप्शन गुपित'; @override - String get ccsyncBackendUrl => 'CCSync बॅकएंड URL'; + String get syncServerBackendUrl => 'TaskChampion बॅकएंड URL'; @override - String get ccsyncClientId => 'क्लायंट आयडी'; + String get syncServerClientId => 'क्लायंट आयडी'; @override String get success => 'यशस्वी'; @override @@ -688,6 +688,4 @@ class MarathiSentences extends Sentences { String get storageAndData => 'स्टोरेज आणि डेटा'; @override String get advanced => 'अड्वांस्ड'; - @override - String get taskchampionBackendUrl => 'Taskchampion URL'; } diff --git a/lib/app/utils/language/sentences.dart b/lib/app/utils/language/sentences.dart index 891f3e30..d2560cab 100644 --- a/lib/app/utils/language/sentences.dart +++ b/lib/app/utils/language/sentences.dart @@ -1,12 +1,12 @@ abstract class Sentences { - /// CCSync UI additional sentences - String get ccsyncLoginInstruction; - String get ccsyncEasySyncTitle; - String get ccsyncOpenButton; - - /// CCSync intro and self-hosted sentences - String get ccsyncIntro; - String get ccsyncSelfHosted; + /// TaskChampion UI additional sentences + String get syncServerLoginInstruction; + String get syncServerEasySyncTitle; + String get syncServerOpenButton; + + /// TaskChampion intro and self-hosted sentences + String get syncServerIntro; + String get syncServerSelfHosted; String get helloWorld; String get homePageTitle; @@ -64,7 +64,7 @@ abstract class Sentences { String get navDrawerReports; String get navDrawerAbout; String get navDrawerSettings; - String get ccsyncCredentials; + String get syncServerCredentials; String get deleteTaskTitle; String get deleteTaskConfirmation; String get deleteTaskWarning; @@ -344,12 +344,11 @@ abstract class Sentences { String get add; String get change; String get dateCanNotBeInPast; - // ccsync credentials page + // sync server credentials page String get configureTaskchampion; String get encryptionSecret; - String get ccsyncBackendUrl; - String get taskchampionBackendUrl; - String get ccsyncClientId; + String get syncServerBackendUrl; + String get syncServerClientId; String get success; String get credentialsSavedSuccessfully; String get tip; diff --git a/lib/app/utils/language/spanish_sentences.dart b/lib/app/utils/language/spanish_sentences.dart index 94d6a460..5ed6b740 100644 --- a/lib/app/utils/language/spanish_sentences.dart +++ b/lib/app/utils/language/spanish_sentences.dart @@ -2,17 +2,17 @@ import 'package:taskwarrior/app/utils/language/sentences.dart'; class SpanishSentences extends Sentences { @override - String get ccsyncLoginInstruction => - 'Inicia sesión en CCSync, copia tus credenciales y pégalas arriba.'; + String get syncServerLoginInstruction => + 'Inicia sesión en TaskChampion, copia tus credenciales y pégalas arriba.'; @override - String get ccsyncEasySyncTitle => 'Usa CCSync para una sincronización fácil'; + String get syncServerEasySyncTitle => 'Usa TaskChampion para una sincronización fácil'; @override - String get ccsyncOpenButton => 'Abrir CCSync'; + String get syncServerOpenButton => 'Abrir TaskChampion'; @override - String get ccsyncIntro => - 'CCSync utiliza TaskChampion para sincronizar tus tareas en múltiples dispositivos sin problemas. También obtienes un panel web para gestionar tus tareas desde cualquier navegador.'; + String get syncServerIntro => + 'TaskChampion sincroniza tus tareas en múltiples dispositivos sin problemas. También obtienes un panel web para gestionar tus tareas desde cualquier navegador.'; @override - String get ccsyncSelfHosted => + String get syncServerSelfHosted => 'O utiliza tus propias credenciales de un servidor de sincronización TaskChampion autohospedado.'; @override String get helloWorld => '¡Hola, mundo!'; @@ -206,13 +206,13 @@ class SpanishSentences extends Sentences { @override String get taskchampionTileDescription => - 'Cambia la sincronización de Taskwarrior al servidor de sincronización CCSync o Taskchampion'; + 'Cambia la sincronización de Taskwarrior al servidor de sincronización TaskChampion'; @override String get taskchampionTileTitle => 'Sincronización Taskchampion'; @override - String get ccsyncCredentials => 'Credenciales de CCSync'; + String get syncServerCredentials => 'Credenciales de TaskChampion'; @override String get deleteTaskConfirmation => 'Eliminar tarea'; @@ -665,9 +665,9 @@ class SpanishSentences extends Sentences { @override String get encryptionSecret => 'Secreto de cifrado'; @override - String get ccsyncBackendUrl => 'URL del backend de CCSync'; + String get syncServerBackendUrl => 'URL del backend de TaskChampion'; @override - String get ccsyncClientId => 'ID de cliente'; + String get syncServerClientId => 'ID de cliente'; @override String get success => 'Éxito'; @override @@ -692,6 +692,4 @@ class SpanishSentences extends Sentences { String get storageAndData => 'Almacenamiento y datos'; @override String get advanced => 'Avanzado'; - @override - String get taskchampionBackendUrl => 'Taskchampion URL'; } diff --git a/lib/app/utils/language/urdu_sentences.dart b/lib/app/utils/language/urdu_sentences.dart index e78c37de..f8cf270c 100644 --- a/lib/app/utils/language/urdu_sentences.dart +++ b/lib/app/utils/language/urdu_sentences.dart @@ -2,17 +2,17 @@ import 'package:taskwarrior/app/utils/language/sentences.dart'; class UrduSentences extends Sentences { @override - String get ccsyncLoginInstruction => - 'CCSync میں لاگ ان کریں، اپنی اسناد کاپی کریں، اور اوپر پیسٹ کریں۔'; + String get syncServerLoginInstruction => + 'TaskChampion میں لاگ ان کریں، اپنی اسناد کاپی کریں، اور اوپر پیسٹ کریں۔'; @override - String get ccsyncEasySyncTitle => 'آسان سینک کے لیے CCSync کا używaj'; + String get syncServerEasySyncTitle => 'آسان سینک کے لیے TaskChampion کا استعمال کریں'; @override - String get ccsyncOpenButton => 'CCSync کھولیں'; + String get syncServerOpenButton => 'TaskChampion کھولیں'; @override - String get ccsyncIntro => - 'CCSync آپ کے کاموں کو کئی آلاتوں میں ہموار طور پر سینک کرنے کے لیے TaskChampion کا gebruikt۔ آپ کو اپنے کاموں کو کسی بھی براؤزر سے manage کرنے کے لیے ویب ڈیش بورد بھی ملتی ہے۔'; + String get syncServerIntro => + 'TaskChampion آپ کے کاموں کو کئی آلاتوں میں ہموار طور پر سینک کرتا ہے۔ آپ کو اپنے کاموں کو کسی بھی براؤزر سے manage کرنے کے لیے ویب ڈیش بورد بھی ملتی ہے۔'; @override - String get ccsyncSelfHosted => + String get syncServerSelfHosted => 'یا اپنے سیلف ہوسٹڈ TaskChampion sync سرور سے اپنی اسناد لائیں۔'; @override String get helloWorld => 'ہیلو، دنیا!'; @@ -221,12 +221,12 @@ class UrduSentences extends Sentences { @override String get taskchampionTileDescription => - 'CCSync یا Taskchampion Sync Server کے ساتھ ٹاسکواریر sync پر سوئچ کریں'; + 'TaskChampion Sync Server کے ساتھ ٹاسکواریر sync پر سوئچ کریں'; @override String get taskchampionTileTitle => 'Taskchampion sync'; @override - String get ccsyncCredentials => 'CCync اسناد'; + String get syncServerCredentials => 'TaskChampion اسناد'; @override String get deleteTaskConfirmation => 'کام حذف کریں'; @@ -653,9 +653,9 @@ class UrduSentences extends Sentences { @override String get encryptionSecret => 'انکرپشن سیکریٹ'; @override - String get ccsyncBackendUrl => 'CCSync بیک اینڈ یو آر ایل'; + String get syncServerBackendUrl => 'TaskChampion بیک اینڈ یو آر ایل'; @override - String get ccsyncClientId => 'کلائنٹ آئی ڈی'; + String get syncServerClientId => 'کلائنٹ آئی ڈی'; @override String get success => 'کامیابی'; @override @@ -679,6 +679,4 @@ class UrduSentences extends Sentences { String get storageAndData => 'اسٹوریج اور ڈیٹا'; @override String get advanced => 'ایڈوانس'; - @override - String get taskchampionBackendUrl => 'Taskchampion یو آر ایل'; } diff --git a/lib/app/v3/champion/models/task_for_replica.dart b/lib/app/v3/champion/models/task_for_replica.dart index c1e966c8..00088ee7 100644 --- a/lib/app/v3/champion/models/task_for_replica.dart +++ b/lib/app/v3/champion/models/task_for_replica.dart @@ -1,7 +1,10 @@ import 'dart:convert'; +import 'package:taskwarrior/app/v3/models/annotation.dart'; + class TaskForReplica { final int? modified; + final int? entry; final String? due; final String? start; final String? wait; @@ -13,8 +16,16 @@ class TaskForReplica { final String? priority; final String? project; + // Attributes surfaced from the TaskChampion Rust serializer. + final bool? isBlocked; + final bool? isBlocking; + final List? depends; + final String? recur; + final List? annotations; + TaskForReplica({ this.modified, + this.entry, this.due, this.start, this.wait, @@ -24,13 +35,26 @@ class TaskForReplica { required this.uuid, this.priority, this.project, + this.isBlocked, + this.isBlocking, + this.depends, + this.recur, + this.annotations, }); + static bool _parseBool(dynamic value) { + if (value is bool) return value; + return value?.toString().toLowerCase() == 'true'; + } + factory TaskForReplica.fromJson(Map json) { return TaskForReplica( modified: json['modified'] is int ? json['modified'] as int : int.tryParse('${json['modified']}'), + entry: json['entry'] is int + ? json['entry'] as int + : int.tryParse('${json['entry']}'), due: json['due'] != null ? DateTime.fromMillisecondsSinceEpoch( (int.tryParse(json['due'].toString()) ?? 0) * 1000, @@ -62,12 +86,28 @@ class TaskForReplica { uuid: json['uuid']?.toString() ?? '', priority: json['priority']?.toString(), project: json['project']?.toString(), + isBlocked: + json['is_blocked'] != null ? _parseBool(json['is_blocked']) : null, + isBlocking: + json['is_blocking'] != null ? _parseBool(json['is_blocking']) : null, + depends: (json['depends'] is List) + ? (json['depends'] as List).map((e) => e.toString()).toList() + : null, + recur: (json['recur'] != null && json['recur'].toString().isNotEmpty) + ? json['recur'].toString() + : null, + annotations: (json['annotations'] is List) + ? (json['annotations'] as List) + .map((e) => Annotation.fromJson(Map.from(e))) + .toList() + : null, ); } Map toJson() { return { if (modified != null) 'modified': modified, + if (entry != null) 'entry': entry, if (due != null) 'due': due, if (start != null) 'start': start, if (wait != null) 'wait': wait, @@ -77,11 +117,18 @@ class TaskForReplica { 'uuid': uuid, if (priority != null) 'priority': priority, if (project != null) 'project': project, + if (isBlocked != null) 'is_blocked': isBlocked, + if (isBlocking != null) 'is_blocking': isBlocking, + if (depends != null) 'depends': depends, + if (recur != null) 'recur': recur, + if (annotations != null) + 'annotations': annotations!.map((a) => a.toJson()).toList(), }; } TaskForReplica copyWith({ int? modified, + int? entry, String? due, String? start, String? wait, @@ -90,9 +137,16 @@ class TaskForReplica { List? tags, String? uuid, String? priority, + String? project, + bool? isBlocked, + bool? isBlocking, + List? depends, + String? recur, + List? annotations, }) { return TaskForReplica( modified: modified ?? this.modified, + entry: entry ?? this.entry, due: due ?? this.due, start: start ?? this.start, wait: wait ?? this.wait, @@ -101,10 +155,125 @@ class TaskForReplica { tags: tags ?? this.tags, uuid: uuid ?? this.uuid, priority: priority ?? this.priority, - project: project ?? project, + project: project ?? this.project, + isBlocked: isBlocked ?? this.isBlocked, + isBlocking: isBlocking ?? this.isBlocking, + depends: depends ?? this.depends, + recur: recur ?? this.recur, + annotations: annotations ?? this.annotations, ); } + /// Computes the task's urgency using Taskwarrior's standard algorithm and its + /// built-in default coefficients. + /// + /// TaskChampion (the storage/sync layer this app embeds) does not compute or + /// store urgency — it is a Taskwarrior-CLI concept — so we reproduce the + /// formula here from the attributes the Rust serializer surfaces. Urgency is a + /// weighted sum of independent terms; the default coefficients match upstream + /// Taskwarrior (Task.cpp `urgency_c`): + /// + /// priority H/M/L = 6.0 / 3.9 / 1.8 due = 12.0 next(tag) = 15.0 + /// active = 4.0 age = 2.0 (over 365d) annotations = 1.0 + /// tags = 1.0 project = 1.0 blocking = 8.0 blocked = -5.0 + /// waiting = -3.0 + /// + /// `scheduled` (+5.0) and user-defined attributes/coefficients are omitted: + /// TaskChampion does not surface a scheduled date, and there are no UDAs here. + /// + /// [clock] overrides "now" (for age/due/waiting) so the result is testable. + double computeUrgency({DateTime? clock}) { + final DateTime now = (clock ?? DateTime.now()).toUtc(); + double urgency = 0.0; + + // Priority. + switch (priority) { + case 'H': + urgency += 6.0; + break; + case 'M': + urgency += 3.9; + break; + case 'L': + urgency += 1.8; + break; + } + + // Belongs to a project. + if (project != null && project!.isNotEmpty) urgency += 1.0; + + // Active (has been started). + if (start != null && start!.isNotEmpty) urgency += 4.0; + + // Tags: 1 -> 0.8, 2 -> 0.9, 3+ -> 1.0. The special "next" tag adds 15.0. + final List tagList = tags ?? const []; + if (tagList.length == 1) { + urgency += 0.8; + } else if (tagList.length == 2) { + urgency += 0.9; + } else if (tagList.length >= 3) { + urgency += 1.0; + } + if (tagList.contains('next')) urgency += 15.0; + + // Annotations: 1 -> 0.8, 2 -> 0.9, 3+ -> 1.0. + final int annCount = annotations?.length ?? 0; + if (annCount == 1) { + urgency += 0.8; + } else if (annCount == 2) { + urgency += 0.9; + } else if (annCount >= 3) { + urgency += 1.0; + } + + // Age: linear ramp from 0 to 1 over 365 days since entry, coefficient 2.0. + if (entry != null) { + final DateTime entryDate = + DateTime.fromMillisecondsSinceEpoch(entry! * 1000, isUtc: true); + final double ageDays = now.difference(entryDate).inSeconds / 86400.0; + const double maxAge = 365.0; + final double ageTerm = + ageDays >= maxAge ? 1.0 : (ageDays <= 0 ? 0.0 : ageDays / maxAge); + urgency += 2.0 * ageTerm; + } + + // Due: ramp mapping ~21 days around the due date to 0.2..1.0, coefficient 12. + final DateTime? dueDate = _parseDate(due); + if (dueDate != null) { + final double daysOverdue = now.difference(dueDate).inSeconds / 86400.0; + double term; + if (daysOverdue >= 7.0) { + term = 1.0; + } else if (daysOverdue >= -14.0) { + term = ((daysOverdue + 14.0) * 0.8 / 21.0) + 0.2; + } else { + term = 0.2; + } + urgency += 12.0 * term; + } + + // Waiting (wait date in the future). + final DateTime? waitDate = _parseDate(wait); + if (waitDate != null && waitDate.isAfter(now)) urgency -= 3.0; + + // Dependency relationships. + if (isBlocking == true) urgency += 8.0; + if (isBlocked == true) urgency -= 5.0; + + return urgency; + } + + static DateTime? _parseDate(String? value) { + if (value == null || value.isEmpty) return null; + final DateTime? parsed = DateTime.tryParse(value); + if (parsed != null) return parsed.toUtc(); + final int? epoch = int.tryParse(value); + if (epoch != null) { + return DateTime.fromMillisecondsSinceEpoch(epoch * 1000, isUtc: true); + } + return null; + } + @override String toString() => 'TaskForReplica(${jsonEncode(toJson())})'; @@ -120,7 +289,11 @@ class TaskForReplica { other.description == description && _listEquals(other.tags, tags) && other.uuid == uuid && - other.priority == priority; + other.priority == priority && + other.isBlocked == isBlocked && + other.isBlocking == isBlocking && + _listEquals(other.depends, depends) && + other.recur == recur; } @override diff --git a/lib/app/v3/db/update.dart b/lib/app/v3/db/update.dart deleted file mode 100644 index 7d0f549f..00000000 --- a/lib/app/v3/db/update.dart +++ /dev/null @@ -1,87 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:taskwarrior/app/v3/db/task_database.dart'; -import 'package:taskwarrior/app/v3/models/task.dart'; -import 'package:taskwarrior/app/v3/net/add_task.dart'; -import 'package:taskwarrior/app/v3/net/complete.dart'; -import 'package:taskwarrior/app/v3/net/delete.dart'; -import 'package:taskwarrior/app/v3/net/modify.dart'; -import 'package:timezone/timezone.dart'; - -Future updateTasksInDatabase(List tasks) async { - debugPrint( - "Updating tasks in database... Total tasks from server: ${tasks.length}"); - var taskDatabase = TaskDatabase(); - await taskDatabase.open(); - // find tasks without UUID - List tasksWithoutUUID = await taskDatabase.findTasksWithoutUUIDs(); - - //add tasks without UUID to the server and delete them from database - for (var task in tasksWithoutUUID) { - try { - await addTaskAndDeleteFromDatabase( - task.description, - task.project != null ? task.project! : '', - task.due!, - task.priority!, - task.tags != null ? task.tags! : []); - } catch (e) { - debugPrint( - 'Failed to add task without UUID to server: $e ${task.tags} ${task.project}'); - } - } - - // update existing tasks in db - for (var task in tasks) { - var existingTask = await taskDatabase.getTaskByUuid(task.uuid!); - if (existingTask != null) { - if (task.modified!.compareTo(existingTask.modified!) > 0) { - await taskDatabase.updateTask(task); - } - } else { - // add new tasks to db - await taskDatabase.insertTask(task); - } - } - - var localTasks = await taskDatabase.fetchTasksFromDatabase(); - var localTasksMap = {for (var task in localTasks) task.uuid: task}; - - for (var serverTask in tasks) { - var localTask = localTasksMap[serverTask.uuid]; - - if (localTask == null) { - // Task doesn't exist in the local database, insert it - debugPrint( - 'Inserting new task from server: ${serverTask.description}, modified: ${serverTask.modified}'); - await taskDatabase.insertTask(serverTask); - } else { - var serverTaskModifiedDate = DateTime.parse(serverTask.modified!); - var localTaskModifiedDate = DateTime.parse(localTask.modified!); - - if (serverTaskModifiedDate.isAfter(localTaskModifiedDate)) { - // Server task is newer, update local database - await taskDatabase.updateTask(serverTask); - } else if (serverTaskModifiedDate.isBefore(localTaskModifiedDate)) { - // local task is newer, update server - debugPrint( - 'Updating task on server: ${localTask.description}, modified: ${localTask.modified}'); - await modifyTaskOnTaskwarrior( - localTask.description, - localTask.project!, - localTask.due!, - localTask.priority!, - localTask.status, - localTask.uuid!, - localTask.id.toString(), - localTask.tags != null - ? localTask.tags!.map((e) => e.toString()).toList() - : []); - if (localTask.status == 'completed') { - completeTask('email', localTask.uuid!); - } else if (localTask.status == 'deleted') { - deleteTask('email', localTask.uuid!); - } - } - } - } -} diff --git a/lib/app/v3/models/task.dart b/lib/app/v3/models/task.dart index 91c64045..68b83a69 100644 --- a/lib/app/v3/models/task.dart +++ b/lib/app/v3/models/task.dart @@ -14,7 +14,7 @@ class TaskForC { final String entry; final String? modified; final List? tags; - // newer feilds in CCSync Model + // newer fields in the TaskChampion model final String? start; final String? wait; final String? rtype; diff --git a/lib/app/v3/net/add_task.dart b/lib/app/v3/net/add_task.dart deleted file mode 100644 index 370a5c27..00000000 --- a/lib/app/v3/net/add_task.dart +++ /dev/null @@ -1,37 +0,0 @@ -import 'dart:convert'; -import 'package:http/http.dart' as http; -import 'package:flutter/material.dart'; -import 'package:taskwarrior/app/utils/taskchampion/credentials_storage.dart'; -import 'package:taskwarrior/app/v3/db/task_database.dart'; - -Future addTaskAndDeleteFromDatabase(String description, String project, - String due, String priority, List tags) async { - var baseUrl = await CredentialsStorage.getApiUrl(); - String apiUrl = '$baseUrl/add-task'; - var c = await CredentialsStorage.getClientId(); - var e = await CredentialsStorage.getEncryptionSecret(); - debugPrint("Database Adding Tags $tags $description"); - debugPrint(c); - debugPrint(e); - var res = await http.post( - Uri.parse(apiUrl), - headers: { - 'Content-Type': 'text/plain', - }, - body: jsonEncode({ - 'email': 'email', - 'encryptionSecret': e, - 'UUID': c, - 'description': description, - 'project': project, - 'due': due, - 'priority': priority, - 'tags': tags - }), - ); - debugPrint('Database res ${res.body}'); - var taskDatabase = TaskDatabase(); - await taskDatabase.open(); - await taskDatabase.deleteTask( - description: description, due: due, project: project, priority: priority); -} diff --git a/lib/app/v3/net/complete.dart b/lib/app/v3/net/complete.dart deleted file mode 100644 index b3718146..00000000 --- a/lib/app/v3/net/complete.dart +++ /dev/null @@ -1,41 +0,0 @@ -import 'dart:convert'; -import 'package:http/http.dart' as http; -import 'package:flutter/material.dart'; -import 'package:taskwarrior/app/utils/taskchampion/credentials_storage.dart'; -import 'package:path/path.dart'; - -Future completeTask(String email, String taskUuid) async { - var c = await CredentialsStorage.getClientId(); - var e = await CredentialsStorage.getEncryptionSecret(); - var baseUrl = await CredentialsStorage.getApiUrl(); - final url = Uri.parse('$baseUrl/complete-task'); - final body = jsonEncode({ - 'email': email, - 'encryptionSecret': e, - 'UUID': c, - 'taskuuid': taskUuid, - }); - - try { - final response = await http.post( - url, - headers: { - 'Content-Type': 'application/json', - }, - body: body, - ); - - if (response.statusCode == 200) { - debugPrint('Task completed successfully on server'); - } else { - debugPrint('Failed to complete task: ${response.statusCode}'); - ScaffoldMessenger.of(context as BuildContext).showSnackBar(const SnackBar( - content: Text( - "Failed to complete task!", - style: TextStyle(color: Colors.red), - ))); - } - } catch (e) { - debugPrint('Error completing task: $e'); - } -} diff --git a/lib/app/v3/net/delete.dart b/lib/app/v3/net/delete.dart deleted file mode 100644 index 8873377b..00000000 --- a/lib/app/v3/net/delete.dart +++ /dev/null @@ -1,35 +0,0 @@ -import 'dart:convert'; -import 'package:http/http.dart' as http; -import 'package:flutter/material.dart'; -import 'package:taskwarrior/app/utils/taskchampion/credentials_storage.dart'; - -Future deleteTask(String email, String taskUuid) async { - var baseUrl = await CredentialsStorage.getApiUrl(); - var c = await CredentialsStorage.getClientId(); - var e = await CredentialsStorage.getEncryptionSecret(); - final url = Uri.parse('$baseUrl/delete-task'); - final body = jsonEncode({ - 'email': email, - 'encryptionSecret': e, - 'UUID': c, - 'taskuuid': taskUuid, - }); - - try { - final response = await http.post( - url, - headers: { - 'Content-Type': 'application/json', - }, - body: body, - ); - - if (response.statusCode == 200) { - debugPrint('Task deleted successfully on server'); - } else { - debugPrint('Failed to delete task: ${response.statusCode}'); - } - } catch (e) { - debugPrint('Error deleting task: $e'); - } -} diff --git a/lib/app/v3/net/fetch.dart b/lib/app/v3/net/fetch.dart deleted file mode 100644 index 54adde77..00000000 --- a/lib/app/v3/net/fetch.dart +++ /dev/null @@ -1,32 +0,0 @@ -import 'dart:convert'; - -import 'package:flutter/material.dart'; -import 'package:taskwarrior/app/utils/taskchampion/credentials_storage.dart'; -import 'package:taskwarrior/app/v3/models/task.dart'; -import 'package:taskwarrior/app/v3/net/origin.dart'; -import 'package:http/http.dart' as http; - -Future> fetchTasks(String uuid, String encryptionSecret) async { - var baseUrl = await CredentialsStorage.getApiUrl(); - try { - String url = - '$baseUrl/tasks?email=email&origin=$origin&UUID=$uuid&encryptionSecret=$encryptionSecret'; - - var response = await http.get(Uri.parse(url), headers: { - "Content-Type": "application/json", - }).timeout(const Duration(milliseconds: 10000)); - debugPrint("Fetch tasks response: ${response.statusCode}"); - debugPrint("Fetch tasks body: ${response.body}"); - if (response.statusCode == 200) { - List allTasks = jsonDecode(response.body); - debugPrint(allTasks.toString()); - return allTasks.map((task) => TaskForC.fromJson(task)).toList(); - } else { - throw Exception('Failed to load tasks'); - } - } catch (e, s) { - debugPrint('Error fetching tasks: $e\n $s'); - - return []; - } -} diff --git a/lib/app/v3/net/modify.dart b/lib/app/v3/net/modify.dart deleted file mode 100644 index 1d32976a..00000000 --- a/lib/app/v3/net/modify.dart +++ /dev/null @@ -1,73 +0,0 @@ -import 'dart:convert'; -import 'package:get/get.dart'; -import 'package:http/http.dart' as http; -import 'package:path/path.dart'; -import 'package:flutter/material.dart'; -import 'package:taskwarrior/app/utils/taskchampion/credentials_storage.dart'; -import 'package:taskwarrior/app/v3/db/task_database.dart'; - -Future modifyTaskOnTaskwarrior( - String description, - String project, - String due, - String priority, - String status, - String taskuuid, - String id, - List newTags) async { - var baseUrl = await CredentialsStorage.getApiUrl(); - var c = await CredentialsStorage.getClientId(); - var e = await CredentialsStorage.getEncryptionSecret(); - String apiUrl = '$baseUrl/modify-task'; - debugPrint(c); - debugPrint(e); - debugPrint("modifyTaskOnTaskwarrior called"); - debugPrint("description: $description project: $project due: $due " - "priority: $priority status: $status taskuuid: $taskuuid id: $id tags: $newTags" - "body: ${jsonEncode({ - "email": "e", - "encryptionSecret": e, - "UUID": c, - "description": description, - "priority": priority, - "project": project, - "due": due, - "status": status, - "taskuuid": taskuuid, - "taskId": id, - "tags": newTags.isNotEmpty ? newTags : null - })}"); - final response = await http.post( - Uri.parse(apiUrl), - headers: { - 'Content-Type': 'text/plain', - }, - body: jsonEncode({ - "email": "e", - "encryptionSecret": e, - "UUID": c, - "description": description, - "priority": priority, - "project": project, - "due": due, - "status": status, - "taskuuid": taskuuid, - "taskId": id, - "tags": newTags.isNotEmpty ? newTags : null - }), - ); - debugPrint('Modify task response body: ${response.body}'); - if (response.statusCode < 200 || response.statusCode >= 300) { - Get.showSnackbar(GetSnackBar( - title: 'Error', - message: - 'Failed to modify task on Taskwarrior server. ${response.statusCode}', - duration: Duration(seconds: 3), - )); - } - - var taskDatabase = TaskDatabase(); - await taskDatabase.open(); - await taskDatabase.deleteTask( - description: description, due: due, project: project, priority: priority); -} diff --git a/lib/app/v3/net/origin.dart b/lib/app/v3/net/origin.dart deleted file mode 100644 index 4cc70540..00000000 --- a/lib/app/v3/net/origin.dart +++ /dev/null @@ -1 +0,0 @@ -String origin = 'http://localhost:8080'; diff --git a/lib/rust_bridge/api.dart b/lib/rust_bridge/api.dart index 8788ef8b..429c6d9c 100644 --- a/lib/rust_bridge/api.dart +++ b/lib/rust_bridge/api.dart @@ -6,29 +6,35 @@ import 'frb_generated.dart'; import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart'; -// These functions are ignored because they are not marked as `pub`: `get_all_tasks`, `parse_datetime` +// These functions are ignored because they are not marked as `pub`: `add_task_impl`, `delete_task_impl`, `get_all_tasks_json_impl`, `parse_datetime`, `sync_impl`, `update_task_impl` +/// Return every task in the replica as a JSON array string. Future getAllTasksJson({required String taskdbDirPath}) => RustLib.instance.api.crateApiGetAllTasksJson(taskdbDirPath: taskdbDirPath); -Future deleteTask( +/// Delete the task with the given UUID. A no-op if the task does not exist. +Future deleteTask( {required String uuidSt, required String taskdbDirPath}) => RustLib.instance.api .crateApiDeleteTask(uuidSt: uuidSt, taskdbDirPath: taskdbDirPath); -Future updateTask( +/// Update the mutable fields of an existing task from the supplied key/value map. +Future updateTask( {required String uuidSt, required String taskdbDirPath, required Map map}) => RustLib.instance.api.crateApiUpdateTask( uuidSt: uuidSt, taskdbDirPath: taskdbDirPath, map: map); -Future addTask( +/// Create a new task from the supplied key/value map. The map must contain a +/// `uuid` entry. +Future addTask( {required String taskdbDirPath, required Map map}) => RustLib.instance.api .crateApiAddTask(taskdbDirPath: taskdbDirPath, map: map); -Future sync_( +/// Synchronise the local replica with a remote TaskChampion sync server. +Future sync_( {required String taskdbDirPath, required String url, required String clientId, diff --git a/lib/rust_bridge/frb_generated.dart b/lib/rust_bridge/frb_generated.dart index 2dbdeccc..1c4a7038 100644 --- a/lib/rust_bridge/frb_generated.dart +++ b/lib/rust_bridge/frb_generated.dart @@ -79,21 +79,21 @@ class RustLib extends BaseEntrypoint { } abstract class RustLibApi extends BaseApi { - Future crateApiAddTask( + Future crateApiAddTask( {required String taskdbDirPath, required Map map}); - Future crateApiDeleteTask( + Future crateApiDeleteTask( {required String uuidSt, required String taskdbDirPath}); Future crateApiGetAllTasksJson({required String taskdbDirPath}); - Future crateApiSync( + Future crateApiSync( {required String taskdbDirPath, required String url, required String clientId, required String encryptionSecret}); - Future crateApiUpdateTask( + Future crateApiUpdateTask( {required String uuidSt, required String taskdbDirPath, required Map map}); @@ -108,7 +108,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { }); @override - Future crateApiAddTask( + Future crateApiAddTask( {required String taskdbDirPath, required Map map}) { return handler.executeNormal(NormalTask( callFfi: (port_) { @@ -119,8 +119,8 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { funcId: 1, port: port_); }, codec: SseCodec( - decodeSuccessData: sse_decode_i_8, - decodeErrorData: null, + decodeSuccessData: sse_decode_unit, + decodeErrorData: sse_decode_String, ), constMeta: kCrateApiAddTaskConstMeta, argValues: [taskdbDirPath, map], @@ -134,7 +134,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { ); @override - Future crateApiDeleteTask( + Future crateApiDeleteTask( {required String uuidSt, required String taskdbDirPath}) { return handler.executeNormal(NormalTask( callFfi: (port_) { @@ -145,8 +145,8 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { funcId: 2, port: port_); }, codec: SseCodec( - decodeSuccessData: sse_decode_i_8, - decodeErrorData: null, + decodeSuccessData: sse_decode_unit, + decodeErrorData: sse_decode_String, ), constMeta: kCrateApiDeleteTaskConstMeta, argValues: [uuidSt, taskdbDirPath], @@ -170,7 +170,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { }, codec: SseCodec( decodeSuccessData: sse_decode_String, - decodeErrorData: sse_decode_AnyhowException, + decodeErrorData: sse_decode_String, ), constMeta: kCrateApiGetAllTasksJsonConstMeta, argValues: [taskdbDirPath], @@ -184,7 +184,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { ); @override - Future crateApiSync( + Future crateApiSync( {required String taskdbDirPath, required String url, required String clientId, @@ -200,8 +200,8 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { funcId: 4, port: port_); }, codec: SseCodec( - decodeSuccessData: sse_decode_i_8, - decodeErrorData: null, + decodeSuccessData: sse_decode_unit, + decodeErrorData: sse_decode_String, ), constMeta: kCrateApiSyncConstMeta, argValues: [taskdbDirPath, url, clientId, encryptionSecret], @@ -215,7 +215,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { ); @override - Future crateApiUpdateTask( + Future crateApiUpdateTask( {required String uuidSt, required String taskdbDirPath, required Map map}) { @@ -229,8 +229,8 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { funcId: 5, port: port_); }, codec: SseCodec( - decodeSuccessData: sse_decode_i_8, - decodeErrorData: null, + decodeSuccessData: sse_decode_unit, + decodeErrorData: sse_decode_String, ), constMeta: kCrateApiUpdateTaskConstMeta, argValues: [uuidSt, taskdbDirPath, map], @@ -243,12 +243,6 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { argNames: ["uuidSt", "taskdbDirPath", "map"], ); - @protected - AnyhowException dco_decode_AnyhowException(dynamic raw) { - // Codec=Dco (DartCObject based), see doc to use other codecs - return AnyhowException(raw as String); - } - @protected Map dco_decode_Map_String_String_None(dynamic raw) { // Codec=Dco (DartCObject based), see doc to use other codecs @@ -262,12 +256,6 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return raw as String; } - @protected - int dco_decode_i_8(dynamic raw) { - // Codec=Dco (DartCObject based), see doc to use other codecs - return raw as int; - } - @protected Uint8List dco_decode_list_prim_u_8_strict(dynamic raw) { // Codec=Dco (DartCObject based), see doc to use other codecs @@ -305,13 +293,6 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return; } - @protected - AnyhowException sse_decode_AnyhowException(SseDeserializer deserializer) { - // Codec=Sse (Serialization based), see doc to use other codecs - var inner = sse_decode_String(deserializer); - return AnyhowException(inner); - } - @protected Map sse_decode_Map_String_String_None( SseDeserializer deserializer) { @@ -327,12 +308,6 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return utf8.decoder.convert(inner); } - @protected - int sse_decode_i_8(SseDeserializer deserializer) { - // Codec=Sse (Serialization based), see doc to use other codecs - return deserializer.buffer.getInt8(); - } - @protected Uint8List sse_decode_list_prim_u_8_strict(SseDeserializer deserializer) { // Codec=Sse (Serialization based), see doc to use other codecs @@ -385,13 +360,6 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return deserializer.buffer.getUint8() != 0; } - @protected - void sse_encode_AnyhowException( - AnyhowException self, SseSerializer serializer) { - // Codec=Sse (Serialization based), see doc to use other codecs - sse_encode_String(self.message, serializer); - } - @protected void sse_encode_Map_String_String_None( Map self, SseSerializer serializer) { @@ -406,12 +374,6 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { sse_encode_list_prim_u_8_strict(utf8.encoder.convert(self), serializer); } - @protected - void sse_encode_i_8(int self, SseSerializer serializer) { - // Codec=Sse (Serialization based), see doc to use other codecs - serializer.buffer.putInt8(self); - } - @protected void sse_encode_list_prim_u_8_strict( Uint8List self, SseSerializer serializer) { diff --git a/lib/rust_bridge/frb_generated.io.dart b/lib/rust_bridge/frb_generated.io.dart index 77034b1d..0877d754 100644 --- a/lib/rust_bridge/frb_generated.io.dart +++ b/lib/rust_bridge/frb_generated.io.dart @@ -18,18 +18,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { required super.portManager, }); - @protected - AnyhowException dco_decode_AnyhowException(dynamic raw); - @protected Map dco_decode_Map_String_String_None(dynamic raw); @protected String dco_decode_String(dynamic raw); - @protected - int dco_decode_i_8(dynamic raw); - @protected Uint8List dco_decode_list_prim_u_8_strict(dynamic raw); @@ -45,9 +39,6 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected void dco_decode_unit(dynamic raw); - @protected - AnyhowException sse_decode_AnyhowException(SseDeserializer deserializer); - @protected Map sse_decode_Map_String_String_None( SseDeserializer deserializer); @@ -55,9 +46,6 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected String sse_decode_String(SseDeserializer deserializer); - @protected - int sse_decode_i_8(SseDeserializer deserializer); - @protected Uint8List sse_decode_list_prim_u_8_strict(SseDeserializer deserializer); @@ -81,10 +69,6 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected bool sse_decode_bool(SseDeserializer deserializer); - @protected - void sse_encode_AnyhowException( - AnyhowException self, SseSerializer serializer); - @protected void sse_encode_Map_String_String_None( Map self, SseSerializer serializer); @@ -92,9 +76,6 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected void sse_encode_String(String self, SseSerializer serializer); - @protected - void sse_encode_i_8(int self, SseSerializer serializer); - @protected void sse_encode_list_prim_u_8_strict( Uint8List self, SseSerializer serializer); diff --git a/lib/rust_bridge/frb_generated.web.dart b/lib/rust_bridge/frb_generated.web.dart index 34fa9bca..7d7b51f6 100644 --- a/lib/rust_bridge/frb_generated.web.dart +++ b/lib/rust_bridge/frb_generated.web.dart @@ -26,14 +26,10 @@ import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated_web.dart'; - @protected AnyhowException dco_decode_AnyhowException(dynamic raw); - -@protected Map dco_decode_Map_String_String_None(dynamic raw); + @protected Map dco_decode_Map_String_String_None(dynamic raw); @protected String dco_decode_String(dynamic raw); -@protected int dco_decode_i_8(dynamic raw); - @protected Uint8List dco_decode_list_prim_u_8_strict(dynamic raw); @protected List<(String,String)> dco_decode_list_record_string_string(dynamic raw); @@ -44,14 +40,10 @@ import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated_web.dart'; @protected void dco_decode_unit(dynamic raw); -@protected AnyhowException sse_decode_AnyhowException(SseDeserializer deserializer); - @protected Map sse_decode_Map_String_String_None(SseDeserializer deserializer); @protected String sse_decode_String(SseDeserializer deserializer); -@protected int sse_decode_i_8(SseDeserializer deserializer); - @protected Uint8List sse_decode_list_prim_u_8_strict(SseDeserializer deserializer); @protected List<(String,String)> sse_decode_list_record_string_string(SseDeserializer deserializer); @@ -66,14 +58,10 @@ import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated_web.dart'; @protected bool sse_decode_bool(SseDeserializer deserializer); -@protected void sse_encode_AnyhowException(AnyhowException self, SseSerializer serializer); - @protected void sse_encode_Map_String_String_None(Map self, SseSerializer serializer); @protected void sse_encode_String(String self, SseSerializer serializer); -@protected void sse_encode_i_8(int self, SseSerializer serializer); - @protected void sse_encode_list_prim_u_8_strict(Uint8List self, SseSerializer serializer); @protected void sse_encode_list_record_string_string(List<(String,String)> self, SseSerializer serializer); diff --git a/pubspec.lock b/pubspec.lock index c7182030..0659d451 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -5,18 +5,18 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - sha256: da0d9209ca76bde579f2da330aeb9df62b6319c834fa7baae052021b0462401f + sha256: c209688d9f5a5f26b2fb47a188131a6fb9e876ae9e47af3737c0b4f58a93470d url: "https://pub.dev" source: hosted - version: "85.0.0" + version: "91.0.0" analyzer: dependency: transitive description: name: analyzer - sha256: "974859dc0ff5f37bc4313244b3218c791810d03ab3470a579580279ba971a48d" + sha256: f51c8499b35f9b26820cfe914828a6a98a94efd5cc78b37bb7d03debae3a1d08 url: "https://pub.dev" source: hosted - version: "7.7.1" + version: "8.4.1" ansicolor: dependency: transitive description: @@ -25,6 +25,38 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.3" + app_links: + dependency: "direct main" + description: + name: app_links + sha256: "5f88447519add627fe1cbcab4fd1da3d4fed15b9baf29f28b22535c95ecee3e8" + url: "https://pub.dev" + source: hosted + version: "6.4.1" + app_links_linux: + dependency: transitive + description: + name: app_links_linux + sha256: f5f7173a78609f3dfd4c2ff2c95bd559ab43c80a87dc6a095921d96c05688c81 + url: "https://pub.dev" + source: hosted + version: "1.0.3" + app_links_platform_interface: + dependency: transitive + description: + name: app_links_platform_interface + sha256: "05f5379577c513b534a29ddea68176a4d4802c46180ee8e2e966257158772a3f" + url: "https://pub.dev" + source: hosted + version: "2.0.2" + app_links_web: + dependency: transitive + description: + name: app_links_web + sha256: af060ed76183f9e2b87510a9480e56a5352b6c249778d07bd2c95fc35632a555 + url: "https://pub.dev" + source: hosted + version: "1.0.4" archive: dependency: transitive description: @@ -65,6 +97,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.1.0" + build_cli_annotations: + dependency: transitive + description: + name: build_cli_annotations + sha256: e563c2e01de8974566a1998410d3f6f03521788160a02503b0b1f1a46c7b3d95 + url: "https://pub.dev" + source: hosted + version: "2.1.1" build_config: dependency: transitive description: @@ -125,10 +165,10 @@ packages: dependency: transitive description: name: characters - sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 + sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b url: "https://pub.dev" source: hosted - version: "1.4.0" + version: "1.4.1" checked_yaml: dependency: transitive description: @@ -253,10 +293,10 @@ packages: dependency: transitive description: name: dart_style - sha256: "8a0e5fba27e8ee025d2ffb4ee820b4e6e2cf5e4246a6b1a477eb66866947e0bb" + sha256: a9c30492da18ff84efe2422ba2d319a89942d93e58eb0b73d32abe822ef54b7b url: "https://pub.dev" source: hosted - version: "3.1.1" + version: "3.1.3" dartx: dependency: transitive description: @@ -519,6 +559,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.32" + flutter_rust_bridge: + dependency: "direct main" + description: + name: flutter_rust_bridge + sha256: "37ef40bc6f863652e865f0b2563ea07f0d3c58d8efad803cc01933a4b2ee067e" + url: "https://pub.dev" + source: hosted + version: "2.11.1" flutter_slidable: dependency: "direct main" description: @@ -601,6 +649,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.2" + gtk: + dependency: transitive + description: + name: gtk + sha256: "4ff85b2a16724029dd9e5bbb5a94b6918f9973f74ba571c949d2002801879cf5" + url: "https://pub.dev" + source: hosted + version: "2.2.0" hashcodes: dependency: transitive description: @@ -721,14 +777,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.5" - js: - dependency: transitive - description: - name: js - sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3 - url: "https://pub.dev" - source: hosted - version: "0.6.7" json_annotation: dependency: transitive description: @@ -797,18 +845,18 @@ packages: dependency: transitive description: name: matcher - sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 + sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861 url: "https://pub.dev" source: hosted - version: "0.12.17" + version: "0.12.19" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec + sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b" url: "https://pub.dev" source: hosted - version: "0.11.1" + version: "0.13.0" meta: dependency: transitive description: @@ -829,10 +877,10 @@ packages: dependency: "direct main" description: name: mockito - sha256: "2314cbe9165bcd16106513df9cf3c3224713087f09723b128928dc11a4379f99" + sha256: eff30d002f0c8bf073b6f929df4483b543133fcafce056870163587b03f1d422 url: "https://pub.dev" source: hosted - version: "5.5.0" + version: "5.6.4" nm: dependency: transitive description: @@ -1141,10 +1189,10 @@ packages: dependency: transitive description: name: shelf_web_socket - sha256: "9ca081be41c60190ebcb4766b2486a7d50261db7bd0f5d9615f2d653637a84c1" + sha256: "3632775c8e90d6c9712f883e633716432a27758216dfb61bd86a8321c0580925" url: "https://pub.dev" source: hosted - version: "1.0.4" + version: "3.0.0" sizer: dependency: "direct main" description: @@ -1330,26 +1378,26 @@ packages: dependency: "direct main" description: name: test - sha256: "75906bf273541b676716d1ca7627a17e4c4070a3a16272b7a3dc7da3b9f3f6b7" + sha256: "280d6d890011ca966ad08df7e8a4ddfab0fb3aa49f96ed6de56e3521347a9ae7" url: "https://pub.dev" source: hosted - version: "1.26.3" + version: "1.30.0" test_api: dependency: transitive description: name: test_api - sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55 + sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a" url: "https://pub.dev" source: hosted - version: "0.7.7" + version: "0.7.10" test_core: dependency: transitive description: name: test_core - sha256: "0cc24b5ff94b38d2ae73e1eb43cc302b77964fbf67abad1e296025b78deb53d0" + sha256: "0381bd1585d1a924763c308100f2138205252fb90c9d4eeaf28489ee65ccde51" url: "https://pub.dev" source: hosted - version: "0.6.12" + version: "0.6.16" textfield_tags: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 12e80726..1a45a235 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -66,7 +66,7 @@ dependencies: built_collection: ^5.1.1 textfield_tags: ^3.0.1 path_provider: ^2.1.5 - flutter_rust_bridge: ^2.11.1 + flutter_rust_bridge: 2.11.1 ffi: any # Required for FFI app_links: ^6.4.1 diff --git a/rust/Cargo.lock b/rust/Cargo.lock index e77d3b10..560330a9 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -49,12 +49,6 @@ dependencies = [ "backtrace", ] -[[package]] -name = "allocator-api2" -version = "0.2.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" - [[package]] name = "android-tzdata" version = "0.1.1" @@ -146,487 +140,18 @@ dependencies = [ "backtrace", ] -[[package]] -name = "async-stream" -version = "0.3.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476" -dependencies = [ - "async-stream-impl", - "futures-core", - "pin-project-lite", -] - -[[package]] -name = "async-stream-impl" -version = "0.3.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.106", -] - -[[package]] -name = "async-trait" -version = "0.1.89" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.106", -] - [[package]] name = "atomic" version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c59bdb34bc650a32731b31bd8f0829cc15d24a708ee31559e0bb34f2bc320cba" -[[package]] -name = "atomic-waker" -version = "1.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" - [[package]] name = "autocfg" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" -[[package]] -name = "aws-config" -version = "1.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8bc1b40fb26027769f16960d2f4a6bc20c4bb755d403e552c8c1a73af433c246" -dependencies = [ - "aws-credential-types", - "aws-runtime", - "aws-sdk-sso", - "aws-sdk-ssooidc", - "aws-sdk-sts", - "aws-smithy-async", - "aws-smithy-http", - "aws-smithy-json", - "aws-smithy-runtime", - "aws-smithy-runtime-api", - "aws-smithy-types", - "aws-types", - "bytes", - "fastrand", - "hex", - "http 1.3.1", - "ring", - "time", - "tokio", - "tracing", - "url", - "zeroize", -] - -[[package]] -name = "aws-credential-types" -version = "1.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d025db5d9f52cbc413b167136afb3d8aeea708c0d8884783cf6253be5e22f6f2" -dependencies = [ - "aws-smithy-async", - "aws-smithy-runtime-api", - "aws-smithy-types", - "zeroize", -] - -[[package]] -name = "aws-lc-rs" -version = "1.13.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c953fe1ba023e6b7730c0d4b031d06f267f23a46167dcbd40316644b10a17ba" -dependencies = [ - "aws-lc-sys", - "zeroize", -] - -[[package]] -name = "aws-lc-sys" -version = "0.30.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbfd150b5dbdb988bcc8fb1fe787eb6b7ee6180ca24da683b61ea5405f3d43ff" -dependencies = [ - "bindgen", - "cc", - "cmake", - "dunce", - "fs_extra", -] - -[[package]] -name = "aws-runtime" -version = "1.5.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c034a1bc1d70e16e7f4e4caf7e9f7693e4c9c24cd91cf17c2a0b21abaebc7c8b" -dependencies = [ - "aws-credential-types", - "aws-sigv4", - "aws-smithy-async", - "aws-smithy-eventstream", - "aws-smithy-http", - "aws-smithy-runtime", - "aws-smithy-runtime-api", - "aws-smithy-types", - "aws-types", - "bytes", - "fastrand", - "http 0.2.12", - "http-body 0.4.6", - "percent-encoding", - "pin-project-lite", - "tracing", - "uuid", -] - -[[package]] -name = "aws-sdk-s3" -version = "1.104.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38c488cd6abb0ec9811c401894191932e941c5f91dc226043edacd0afa1634bc" -dependencies = [ - "aws-credential-types", - "aws-runtime", - "aws-sigv4", - "aws-smithy-async", - "aws-smithy-checksums", - "aws-smithy-eventstream", - "aws-smithy-http", - "aws-smithy-json", - "aws-smithy-runtime", - "aws-smithy-runtime-api", - "aws-smithy-types", - "aws-smithy-xml", - "aws-types", - "bytes", - "fastrand", - "hex", - "hmac", - "http 0.2.12", - "http 1.3.1", - "http-body 0.4.6", - "lru", - "percent-encoding", - "regex-lite", - "sha2", - "tracing", - "url", -] - -[[package]] -name = "aws-sdk-sso" -version = "1.83.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "643cd43af212d2a1c4dedff6f044d7e1961e5d9e7cfe773d70f31d9842413886" -dependencies = [ - "aws-credential-types", - "aws-runtime", - "aws-smithy-async", - "aws-smithy-http", - "aws-smithy-json", - "aws-smithy-runtime", - "aws-smithy-runtime-api", - "aws-smithy-types", - "aws-types", - "bytes", - "fastrand", - "http 0.2.12", - "regex-lite", - "tracing", -] - -[[package]] -name = "aws-sdk-ssooidc" -version = "1.84.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20ec4a95bd48e0db7a424356a161f8d87bd6a4f0af37204775f0da03d9e39fc3" -dependencies = [ - "aws-credential-types", - "aws-runtime", - "aws-smithy-async", - "aws-smithy-http", - "aws-smithy-json", - "aws-smithy-runtime", - "aws-smithy-runtime-api", - "aws-smithy-types", - "aws-types", - "bytes", - "fastrand", - "http 0.2.12", - "regex-lite", - "tracing", -] - -[[package]] -name = "aws-sdk-sts" -version = "1.85.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "410309ad0df4606bc721aff0d89c3407682845453247213a0ccc5ff8801ee107" -dependencies = [ - "aws-credential-types", - "aws-runtime", - "aws-smithy-async", - "aws-smithy-http", - "aws-smithy-json", - "aws-smithy-query", - "aws-smithy-runtime", - "aws-smithy-runtime-api", - "aws-smithy-types", - "aws-smithy-xml", - "aws-types", - "fastrand", - "http 0.2.12", - "regex-lite", - "tracing", -] - -[[package]] -name = "aws-sigv4" -version = "1.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "084c34162187d39e3740cb635acd73c4e3a551a36146ad6fe8883c929c9f876c" -dependencies = [ - "aws-credential-types", - "aws-smithy-eventstream", - "aws-smithy-http", - "aws-smithy-runtime-api", - "aws-smithy-types", - "bytes", - "crypto-bigint 0.5.5", - "form_urlencoded", - "hex", - "hmac", - "http 0.2.12", - "http 1.3.1", - "p256", - "percent-encoding", - "ring", - "sha2", - "subtle", - "time", - "tracing", - "zeroize", -] - -[[package]] -name = "aws-smithy-async" -version = "1.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e190749ea56f8c42bf15dd76c65e14f8f765233e6df9b0506d9d934ebef867c" -dependencies = [ - "futures-util", - "pin-project-lite", - "tokio", -] - -[[package]] -name = "aws-smithy-checksums" -version = "0.63.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56d2df0314b8e307995a3b86d44565dfe9de41f876901a7d71886c756a25979f" -dependencies = [ - "aws-smithy-http", - "aws-smithy-types", - "bytes", - "crc-fast", - "hex", - "http 0.2.12", - "http-body 0.4.6", - "md-5", - "pin-project-lite", - "sha1", - "sha2", - "tracing", -] - -[[package]] -name = "aws-smithy-eventstream" -version = "0.60.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "182b03393e8c677347fb5705a04a9392695d47d20ef0a2f8cfe28c8e6b9b9778" -dependencies = [ - "aws-smithy-types", - "bytes", - "crc32fast", -] - -[[package]] -name = "aws-smithy-http" -version = "0.62.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c4dacf2d38996cf729f55e7a762b30918229917eca115de45dfa8dfb97796c9" -dependencies = [ - "aws-smithy-eventstream", - "aws-smithy-runtime-api", - "aws-smithy-types", - "bytes", - "bytes-utils", - "futures-core", - "http 0.2.12", - "http 1.3.1", - "http-body 0.4.6", - "percent-encoding", - "pin-project-lite", - "pin-utils", - "tracing", -] - -[[package]] -name = "aws-smithy-http-client" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "147e8eea63a40315d704b97bf9bc9b8c1402ae94f89d5ad6f7550d963309da1b" -dependencies = [ - "aws-smithy-async", - "aws-smithy-runtime-api", - "aws-smithy-types", - "h2 0.3.27", - "h2 0.4.12", - "http 0.2.12", - "http 1.3.1", - "http-body 0.4.6", - "hyper 0.14.32", - "hyper 1.7.0", - "hyper-rustls 0.24.2", - "hyper-rustls 0.27.7", - "hyper-util", - "pin-project-lite", - "rustls 0.21.12", - "rustls 0.23.31", - "rustls-native-certs 0.8.1", - "rustls-pki-types", - "tokio", - "tokio-rustls 0.26.2", - "tower", - "tracing", -] - -[[package]] -name = "aws-smithy-json" -version = "0.61.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eaa31b350998e703e9826b2104dd6f63be0508666e1aba88137af060e8944047" -dependencies = [ - "aws-smithy-types", -] - -[[package]] -name = "aws-smithy-observability" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9364d5989ac4dd918e5cc4c4bdcc61c9be17dcd2586ea7f69e348fc7c6cab393" -dependencies = [ - "aws-smithy-runtime-api", -] - -[[package]] -name = "aws-smithy-query" -version = "0.60.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2fbd61ceb3fe8a1cb7352e42689cec5335833cd9f94103a61e98f9bb61c64bb" -dependencies = [ - "aws-smithy-types", - "urlencoding", -] - -[[package]] -name = "aws-smithy-runtime" -version = "1.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3946acbe1ead1301ba6862e712c7903ca9bb230bdf1fbd1b5ac54158ef2ab1f" -dependencies = [ - "aws-smithy-async", - "aws-smithy-http", - "aws-smithy-http-client", - "aws-smithy-observability", - "aws-smithy-runtime-api", - "aws-smithy-types", - "bytes", - "fastrand", - "http 0.2.12", - "http 1.3.1", - "http-body 0.4.6", - "http-body 1.0.1", - "pin-project-lite", - "pin-utils", - "tokio", - "tracing", -] - -[[package]] -name = "aws-smithy-runtime-api" -version = "1.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07f5e0fc8a6b3f2303f331b94504bbf754d85488f402d6f1dd7a6080f99afe56" -dependencies = [ - "aws-smithy-async", - "aws-smithy-types", - "bytes", - "http 0.2.12", - "http 1.3.1", - "pin-project-lite", - "tokio", - "tracing", - "zeroize", -] - -[[package]] -name = "aws-smithy-types" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d498595448e43de7f4296b7b7a18a8a02c61ec9349128c80a368f7c3b4ab11a8" -dependencies = [ - "base64-simd", - "bytes", - "bytes-utils", - "futures-core", - "http 0.2.12", - "http 1.3.1", - "http-body 0.4.6", - "http-body 1.0.1", - "http-body-util", - "itoa", - "num-integer", - "pin-project-lite", - "pin-utils", - "ryu", - "serde", - "time", - "tokio", - "tokio-util", -] - -[[package]] -name = "aws-smithy-xml" -version = "0.60.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3db87b96cb1b16c024980f133968d52882ca0daaee3a086c6decc500f6c99728" -dependencies = [ - "xmlparser", -] - -[[package]] -name = "aws-types" -version = "1.3.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b069d19bf01e46298eaedd7c6f283fe565a59263e53eebec945f3e6398f42390" -dependencies = [ - "aws-credential-types", - "aws-smithy-async", - "aws-smithy-runtime-api", - "aws-smithy-types", - "rustc_version", - "tracing", -] - [[package]] name = "backtrace" version = "0.3.75" @@ -642,63 +167,12 @@ dependencies = [ "windows-targets 0.52.6", ] -[[package]] -name = "base16ct" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "349a06037c7bf932dd7e7d1f653678b2038b9ad46a74102f1fc7bd7872678cce" - -[[package]] -name = "base64" -version = "0.21.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" - [[package]] name = "base64" version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" -[[package]] -name = "base64-simd" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "339abbe78e73178762e23bea9dfd08e697eb3f3301cd4be981c0f78ba5859195" -dependencies = [ - "outref", - "vsimd", -] - -[[package]] -name = "base64ct" -version = "1.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba" - -[[package]] -name = "bindgen" -version = "0.69.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "271383c67ccabffb7381723dea0672a673f292304fcb45c01cc648c7a8d58088" -dependencies = [ - "bitflags 2.9.4", - "cexpr", - "clang-sys", - "itertools 0.12.1", - "lazy_static", - "lazycell", - "log", - "prettyplease", - "proc-macro2", - "quote", - "regex", - "rustc-hash 1.1.0", - "shlex", - "syn 2.0.106", - "which", -] - [[package]] name = "bitflags" version = "1.3.2" @@ -750,16 +224,6 @@ version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" -[[package]] -name = "bytes-utils" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7dafe3a8757b027e2be6e4e5601ed563c55989fcf1546e933c66c8eb3a058d35" -dependencies = [ - "bytes", - "either", -] - [[package]] name = "camino" version = "1.1.12" @@ -826,32 +290,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5252b3d2648e5eedbc1a6f501e3c795e07025c1e93bbf8bbdd6eef7f447a6d54" dependencies = [ "find-msvc-tools", - "jobserver", - "libc", "shlex", ] -[[package]] -name = "cexpr" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" -dependencies = [ - "nom", -] - [[package]] name = "cfg-if" version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9" -[[package]] -name = "cfg_aliases" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" - [[package]] name = "chrono" version = "0.4.41" @@ -867,17 +314,6 @@ dependencies = [ "windows-link", ] -[[package]] -name = "clang-sys" -version = "1.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" -dependencies = [ - "glob", - "libc", - "libloading", -] - [[package]] name = "clap" version = "4.5.47" @@ -918,15 +354,6 @@ version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675" -[[package]] -name = "cmake" -version = "0.1.54" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7caa3f9de89ddbe2c607f4101924c5abec803763ae9534e4f4d7d8f84aa81f0" -dependencies = [ - "cc", -] - [[package]] name = "colorchoice" version = "1.0.4" @@ -967,38 +394,12 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "const-oid" -version = "0.9.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" - [[package]] name = "convert_case" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fb4a24b1aaf0fd0ce8b45161144d6f42cd91677fd5940fd431183eb023b3a2b8" -[[package]] -name = "core-foundation" -version = "0.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" -dependencies = [ - "core-foundation-sys", - "libc", -] - -[[package]] -name = "core-foundation" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" -dependencies = [ - "core-foundation-sys", - "libc", -] - [[package]] name = "core-foundation-sys" version = "0.8.7" @@ -1014,34 +415,6 @@ dependencies = [ "libc", ] -[[package]] -name = "crc" -version = "3.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9710d3b3739c2e349eb44fe848ad0b7c8cb1e42bd87ee49371df2f7acaf3e675" -dependencies = [ - "crc-catalog", -] - -[[package]] -name = "crc-catalog" -version = "2.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" - -[[package]] -name = "crc-fast" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6bf62af4cc77d8fe1c22dde4e721d87f2f54056139d8c412e1366b740305f56f" -dependencies = [ - "crc", - "digest", - "libc", - "rand", - "regex", -] - [[package]] name = "crc32fast" version = "1.5.0" @@ -1066,28 +439,6 @@ version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" -[[package]] -name = "crypto-bigint" -version = "0.4.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef2b4b23cddf68b89b8f8069890e8c270d54e2d5fe1b143820234805e4cb17ef" -dependencies = [ - "generic-array", - "rand_core 0.6.4", - "subtle", - "zeroize", -] - -[[package]] -name = "crypto-bigint" -version = "0.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" -dependencies = [ - "rand_core 0.6.4", - "subtle", -] - [[package]] name = "crypto-common" version = "0.1.6" @@ -1131,37 +482,6 @@ dependencies = [ "syn 2.0.106", ] -[[package]] -name = "der" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1a467a65c5e759bce6e65eaf91cc29f466cdc57cb65777bd646872a8a1fd4de" -dependencies = [ - "const-oid", - "zeroize", -] - -[[package]] -name = "der" -version = "0.7.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" -dependencies = [ - "const-oid", - "pem-rfc7468", - "zeroize", -] - -[[package]] -name = "deranged" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d630bccd429a5bb5a64b5e94f693bfc48c9f8566418fda4c494cc94f911f87cc" -dependencies = [ - "powerfmt", - "serde", -] - [[package]] name = "derivative" version = "2.2.0" @@ -1181,7 +501,6 @@ checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer", "crypto-common", - "subtle", ] [[package]] @@ -1195,64 +514,17 @@ dependencies = [ "syn 2.0.106", ] -[[package]] -name = "dunce" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" - -[[package]] -name = "ecdsa" -version = "0.14.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "413301934810f597c1d19ca71c8710e99a3f1ba28a0d2ebc01551a2daeea3c5c" -dependencies = [ - "der 0.6.1", - "elliptic-curve", - "rfc6979", - "signature", -] - [[package]] name = "either" version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" -[[package]] -name = "elliptic-curve" -version = "0.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7bb888ab5300a19b8e5bceef25ac745ad065f3c9f7efc6de1b91958110891d3" -dependencies = [ - "base16ct", - "crypto-bigint 0.4.9", - "der 0.6.1", - "digest", - "ff", - "generic-array", - "group", - "pkcs8 0.9.0", - "rand_core 0.6.4", - "sec1", - "subtle", - "zeroize", -] - [[package]] name = "encode_unicode" version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" - -[[package]] -name = "encoding_rs" -version = "0.8.35" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" -dependencies = [ - "cfg-if", -] +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" [[package]] name = "enum-iterator" @@ -1341,16 +613,6 @@ dependencies = [ "log", ] -[[package]] -name = "ff" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d013fc25338cc558c5c2cfbad646908fb23591e2404481826742b651c9af7160" -dependencies = [ - "rand_core 0.6.4", - "subtle", -] - [[package]] name = "filetime" version = "0.2.26" @@ -1430,7 +692,7 @@ dependencies = [ "include_dir", "indicatif", "indicatif-log-bridge", - "itertools 0.10.5", + "itertools", "lazy_static", "log", "notify", @@ -1466,18 +728,6 @@ dependencies = [ "syn 2.0.106", ] -[[package]] -name = "fnv" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" - -[[package]] -name = "foldhash" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" - [[package]] name = "form_urlencoded" version = "1.2.2" @@ -1487,12 +737,6 @@ dependencies = [ "percent-encoding", ] -[[package]] -name = "fs_extra" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" - [[package]] name = "fsevent-sys" version = "4.1.0" @@ -1608,10 +852,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" dependencies = [ "cfg-if", - "js-sys", "libc", "wasi 0.11.1+wasi-snapshot-preview1", - "wasm-bindgen", ] [[package]] @@ -1621,11 +863,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" dependencies = [ "cfg-if", - "js-sys", "libc", "r-efi", "wasi 0.14.4+wasi-0.2.4", - "wasm-bindgen", ] [[package]] @@ -1640,130 +880,6 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" -[[package]] -name = "google-cloud-auth" -version = "0.17.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e57a13fbacc5e9c41ded3ad8d0373175a6b7a6ad430d99e89d314ac121b7ab06" -dependencies = [ - "async-trait", - "base64 0.21.7", - "google-cloud-metadata", - "google-cloud-token", - "home", - "jsonwebtoken", - "reqwest", - "serde", - "serde_json", - "thiserror 1.0.69", - "time", - "tokio", - "tracing", - "urlencoding", -] - -[[package]] -name = "google-cloud-metadata" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d901aeb453fd80e51d64df4ee005014f6cf39f2d736dd64f7239c132d9d39a6a" -dependencies = [ - "reqwest", - "thiserror 1.0.69", - "tokio", -] - -[[package]] -name = "google-cloud-storage" -version = "0.24.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34a73d9e94d35665909050f02e035d8bdc82e419241b1b027ebf1ea51dc8a470" -dependencies = [ - "anyhow", - "async-stream", - "async-trait", - "base64 0.21.7", - "bytes", - "futures-util", - "google-cloud-auth", - "google-cloud-metadata", - "google-cloud-token", - "hex", - "once_cell", - "percent-encoding", - "pkcs8 0.10.2", - "regex", - "reqwest", - "reqwest-middleware", - "ring", - "serde", - "serde_json", - "sha2", - "thiserror 1.0.69", - "time", - "tokio", - "tracing", - "url", -] - -[[package]] -name = "google-cloud-token" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f49c12ba8b21d128a2ce8585955246977fbce4415f680ebf9199b6f9d6d725f" -dependencies = [ - "async-trait", -] - -[[package]] -name = "group" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dfbfb3a6cfbd390d5c9564ab283a0349b9b9fcd46a706c1eb10e0db70bfbac7" -dependencies = [ - "ff", - "rand_core 0.6.4", - "subtle", -] - -[[package]] -name = "h2" -version = "0.3.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0beca50380b1fc32983fc1cb4587bfa4bb9e78fc259aad4a0032d2080309222d" -dependencies = [ - "bytes", - "fnv", - "futures-core", - "futures-sink", - "futures-util", - "http 0.2.12", - "indexmap", - "slab", - "tokio", - "tokio-util", - "tracing", -] - -[[package]] -name = "h2" -version = "0.4.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3c0b69cfcb4e1b9f1bf2f53f95f766e4661169728ec61cd3fe5a0166f2d1386" -dependencies = [ - "atomic-waker", - "bytes", - "fnv", - "futures-core", - "futures-sink", - "http 1.3.1", - "indexmap", - "slab", - "tokio", - "tokio-util", - "tracing", -] - [[package]] name = "hashbrown" version = "0.14.5" @@ -1778,11 +894,6 @@ name = "hashbrown" version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" -dependencies = [ - "allocator-api2", - "equivalent", - "foldhash", -] [[package]] name = "hashlink" @@ -1817,196 +928,6 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" -[[package]] -name = "hmac" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" -dependencies = [ - "digest", -] - -[[package]] -name = "home" -version = "0.5.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589533453244b0995c858700322199b2becb13b627df2851f64a2775d024abcf" -dependencies = [ - "windows-sys 0.59.0", -] - -[[package]] -name = "http" -version = "0.2.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" -dependencies = [ - "bytes", - "fnv", - "itoa", -] - -[[package]] -name = "http" -version = "1.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" -dependencies = [ - "bytes", - "fnv", - "itoa", -] - -[[package]] -name = "http-body" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" -dependencies = [ - "bytes", - "http 0.2.12", - "pin-project-lite", -] - -[[package]] -name = "http-body" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" -dependencies = [ - "bytes", - "http 1.3.1", -] - -[[package]] -name = "http-body-util" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" -dependencies = [ - "bytes", - "futures-core", - "http 1.3.1", - "http-body 1.0.1", - "pin-project-lite", -] - -[[package]] -name = "httparse" -version = "1.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" - -[[package]] -name = "httpdate" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" - -[[package]] -name = "hyper" -version = "0.14.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41dfc780fdec9373c01bae43289ea34c972e40ee3c9f6b3c8801a35f35586ce7" -dependencies = [ - "bytes", - "futures-channel", - "futures-core", - "futures-util", - "h2 0.3.27", - "http 0.2.12", - "http-body 0.4.6", - "httparse", - "httpdate", - "itoa", - "pin-project-lite", - "socket2 0.5.10", - "tokio", - "tower-service", - "tracing", - "want", -] - -[[package]] -name = "hyper" -version = "1.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb3aa54a13a0dfe7fbe3a59e0c76093041720fdc77b110cc0fc260fafb4dc51e" -dependencies = [ - "atomic-waker", - "bytes", - "futures-channel", - "futures-core", - "h2 0.4.12", - "http 1.3.1", - "http-body 1.0.1", - "httparse", - "itoa", - "pin-project-lite", - "pin-utils", - "smallvec", - "tokio", - "want", -] - -[[package]] -name = "hyper-rustls" -version = "0.24.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590" -dependencies = [ - "futures-util", - "http 0.2.12", - "hyper 0.14.32", - "log", - "rustls 0.21.12", - "rustls-native-certs 0.6.3", - "tokio", - "tokio-rustls 0.24.1", -] - -[[package]] -name = "hyper-rustls" -version = "0.27.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" -dependencies = [ - "http 1.3.1", - "hyper 1.7.0", - "hyper-util", - "rustls 0.23.31", - "rustls-native-certs 0.8.1", - "rustls-pki-types", - "tokio", - "tokio-rustls 0.26.2", - "tower-service", - "webpki-roots 1.0.2", -] - -[[package]] -name = "hyper-util" -version = "0.1.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d9b05277c7e8da2c93a568989bb6207bef0112e8d17df7a6eda4a3cf143bc5e" -dependencies = [ - "base64 0.22.1", - "bytes", - "futures-channel", - "futures-core", - "futures-util", - "http 1.3.1", - "http-body 1.0.1", - "hyper 1.7.0", - "ipnet", - "libc", - "percent-encoding", - "pin-project-lite", - "socket2 0.6.0", - "tokio", - "tower-service", - "tracing", -] - [[package]] name = "iana-time-zone" version = "0.1.63" @@ -2221,22 +1142,6 @@ dependencies = [ "libc", ] -[[package]] -name = "ipnet" -version = "2.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" - -[[package]] -name = "iri-string" -version = "0.7.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbc5ebe9c3a1a7a5127f920a418f7585e9e758e911d0466ed004f393b0e380b2" -dependencies = [ - "memchr", - "serde", -] - [[package]] name = "is-terminal" version = "0.4.16" @@ -2263,31 +1168,12 @@ dependencies = [ "either", ] -[[package]] -name = "itertools" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" -dependencies = [ - "either", -] - [[package]] name = "itoa" version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" -[[package]] -name = "jobserver" -version = "0.1.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" -dependencies = [ - "getrandom 0.3.3", - "libc", -] - [[package]] name = "js-sys" version = "0.3.78" @@ -2298,21 +1184,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "jsonwebtoken" -version = "9.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a87cc7a48537badeae96744432de36f4be2b4a34a05a5ef32e9dd8a1c169dde" -dependencies = [ - "base64 0.22.1", - "js-sys", - "pem", - "ring", - "serde", - "serde_json", - "simple_asn1", -] - [[package]] name = "kqueue" version = "1.1.1" @@ -2339,27 +1210,11 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" -[[package]] -name = "lazycell" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" - [[package]] name = "libc" -version = "0.2.175" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543" - -[[package]] -name = "libloading" -version = "0.8.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07033963ba89ebaf1584d767badaa2e8fcec21aedea6b8c0346d487d49c28667" -dependencies = [ - "cfg-if", - "windows-targets 0.53.3", -] +version = "0.2.175" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543" [[package]] name = "libredox" @@ -2383,12 +1238,6 @@ dependencies = [ "vcpkg", ] -[[package]] -name = "linux-raw-sys" -version = "0.4.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" - [[package]] name = "linux-raw-sys" version = "0.9.4" @@ -2417,21 +1266,6 @@ version = "0.4.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" -[[package]] -name = "lru" -version = "0.12.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38" -dependencies = [ - "hashbrown 0.15.5", -] - -[[package]] -name = "lru-slab" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" - [[package]] name = "md-5" version = "0.10.6" @@ -2448,28 +1282,6 @@ version = "2.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" -[[package]] -name = "mime" -version = "0.3.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" - -[[package]] -name = "mime_guess" -version = "2.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" -dependencies = [ - "mime", - "unicase", -] - -[[package]] -name = "minimal-lexical" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" - [[package]] name = "miniz_oxide" version = "0.8.9" @@ -2502,16 +1314,6 @@ dependencies = [ "windows-sys 0.59.0", ] -[[package]] -name = "nom" -version = "7.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" -dependencies = [ - "memchr", - "minimal-lexical", -] - [[package]] name = "notify" version = "6.1.1" @@ -2542,31 +1344,6 @@ dependencies = [ "notify", ] -[[package]] -name = "num-bigint" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" -dependencies = [ - "num-integer", - "num-traits", -] - -[[package]] -name = "num-conv" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" - -[[package]] -name = "num-integer" -version = "0.1.46" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" -dependencies = [ - "num-traits", -] - [[package]] name = "num-traits" version = "0.2.19" @@ -2607,12 +1384,6 @@ version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" -[[package]] -name = "openssl-probe" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" - [[package]] name = "oslog" version = "0.2.0" @@ -2624,23 +1395,6 @@ dependencies = [ "log", ] -[[package]] -name = "outref" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a80800c0488c3a21695ea981a54918fbb37abf04f4d0720c453632255e2ff0e" - -[[package]] -name = "p256" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51f44edd08f51e2ade572f141051021c5af22677e42b7dd28a88155151c33594" -dependencies = [ - "ecdsa", - "elliptic-curve", - "sha2", -] - [[package]] name = "parking_lot" version = "0.12.4" @@ -2676,25 +1430,6 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" -[[package]] -name = "pem" -version = "3.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38af38e8470ac9dee3ce1bae1af9c1671fffc44ddfd8bd1d0a3445bf349a8ef3" -dependencies = [ - "base64 0.22.1", - "serde", -] - -[[package]] -name = "pem-rfc7468" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" -dependencies = [ - "base64ct", -] - [[package]] name = "percent-encoding" version = "2.3.2" @@ -2713,26 +1448,6 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" -[[package]] -name = "pkcs8" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9eca2c590a5f85da82668fa685c09ce2888b9430e83299debf1f34b65fd4a4ba" -dependencies = [ - "der 0.6.1", - "spki 0.6.0", -] - -[[package]] -name = "pkcs8" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" -dependencies = [ - "der 0.7.10", - "spki 0.7.3", -] - [[package]] name = "pkg-config" version = "0.3.32" @@ -2754,31 +1469,6 @@ dependencies = [ "zerovec", ] -[[package]] -name = "powerfmt" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" - -[[package]] -name = "ppv-lite86" -version = "0.2.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" -dependencies = [ - "zerocopy", -] - -[[package]] -name = "prettyplease" -version = "0.2.37" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" -dependencies = [ - "proc-macro2", - "syn 2.0.106", -] - [[package]] name = "proc-macro2" version = "1.0.101" @@ -2788,61 +1478,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "quinn" -version = "0.11.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" -dependencies = [ - "bytes", - "cfg_aliases", - "pin-project-lite", - "quinn-proto", - "quinn-udp", - "rustc-hash 2.1.1", - "rustls 0.23.31", - "socket2 0.6.0", - "thiserror 2.0.16", - "tokio", - "tracing", - "web-time", -] - -[[package]] -name = "quinn-proto" -version = "0.11.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31" -dependencies = [ - "bytes", - "getrandom 0.3.3", - "lru-slab", - "rand", - "ring", - "rustc-hash 2.1.1", - "rustls 0.23.31", - "rustls-pki-types", - "slab", - "thiserror 2.0.16", - "tinyvec", - "tracing", - "web-time", -] - -[[package]] -name = "quinn-udp" -version = "0.5.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" -dependencies = [ - "cfg_aliases", - "libc", - "once_cell", - "socket2 0.6.0", - "tracing", - "windows-sys 0.60.2", -] - [[package]] name = "quote" version = "1.0.40" @@ -2858,44 +1493,6 @@ version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" -[[package]] -name = "rand" -version = "0.9.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" -dependencies = [ - "rand_chacha", - "rand_core 0.9.3", -] - -[[package]] -name = "rand_chacha" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" -dependencies = [ - "ppv-lite86", - "rand_core 0.9.3", -] - -[[package]] -name = "rand_core" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" -dependencies = [ - "getrandom 0.2.16", -] - -[[package]] -name = "rand_core" -version = "0.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" -dependencies = [ - "getrandom 0.3.3", -] - [[package]] name = "redox_syscall" version = "0.5.17" @@ -2928,88 +1525,12 @@ dependencies = [ "regex-syntax", ] -[[package]] -name = "regex-lite" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "943f41321c63ef1c92fd763bfe054d2668f7f225a5c29f0105903dc2fc04ba30" - [[package]] name = "regex-syntax" version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "caf4aa5b0f434c91fe5c7f1ecb6a5ece2130b02ad2a590589dda5146df959001" -[[package]] -name = "reqwest" -version = "0.12.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d429f34c8092b2d42c7c93cec323bb4adeb7c67698f70839adec842ec10c7ceb" -dependencies = [ - "base64 0.22.1", - "bytes", - "encoding_rs", - "futures-core", - "futures-util", - "http 1.3.1", - "http-body 1.0.1", - "http-body-util", - "hyper 1.7.0", - "hyper-rustls 0.27.7", - "hyper-util", - "js-sys", - "log", - "mime", - "mime_guess", - "percent-encoding", - "pin-project-lite", - "quinn", - "rustls 0.23.31", - "rustls-pki-types", - "serde", - "serde_json", - "serde_urlencoded", - "sync_wrapper", - "tokio", - "tokio-rustls 0.26.2", - "tokio-util", - "tower", - "tower-http", - "tower-service", - "url", - "wasm-bindgen", - "wasm-bindgen-futures", - "wasm-streams", - "web-sys", - "webpki-roots 1.0.2", -] - -[[package]] -name = "reqwest-middleware" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57f17d28a6e6acfe1733fe24bcd30774d13bffa4b8a22535b4c8c98423088d4e" -dependencies = [ - "anyhow", - "async-trait", - "http 1.3.1", - "reqwest", - "serde", - "thiserror 1.0.69", - "tower-service", -] - -[[package]] -name = "rfc6979" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7743f17af12fa0b03b803ba12cd6a8d9483a587e89c69445e3909655c0b9fabb" -dependencies = [ - "crypto-bigint 0.4.9", - "hmac", - "zeroize", -] - [[package]] name = "ring" version = "0.17.14" @@ -3044,40 +1565,6 @@ version = "0.1.26" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace" -[[package]] -name = "rustc-hash" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" - -[[package]] -name = "rustc-hash" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" - -[[package]] -name = "rustc_version" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" -dependencies = [ - "semver", -] - -[[package]] -name = "rustix" -version = "0.38.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" -dependencies = [ - "bitflags 2.9.4", - "errno", - "libc", - "linux-raw-sys 0.4.15", - "windows-sys 0.59.0", -] - [[package]] name = "rustix" version = "1.0.8" @@ -3087,69 +1574,23 @@ dependencies = [ "bitflags 2.9.4", "errno", "libc", - "linux-raw-sys 0.9.4", + "linux-raw-sys", "windows-sys 0.60.2", ] [[package]] -name = "rustls" -version = "0.21.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e" -dependencies = [ - "log", - "ring", - "rustls-webpki 0.101.7", - "sct", -] - -[[package]] -name = "rustls" -version = "0.23.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0ebcbd2f03de0fc1122ad9bb24b127a5a6cd51d72604a3f3c50ac459762b6cc" -dependencies = [ - "aws-lc-rs", - "log", - "once_cell", - "ring", - "rustls-pki-types", - "rustls-webpki 0.103.4", - "subtle", - "zeroize", -] - -[[package]] -name = "rustls-native-certs" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9aace74cb666635c918e9c12bc0d348266037aa8eb599b5cba565709a8dff00" -dependencies = [ - "openssl-probe", - "rustls-pemfile", - "schannel", - "security-framework 2.11.1", -] - -[[package]] -name = "rustls-native-certs" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fcff2dd52b58a8d98a70243663a0d234c4e2b79235637849d15913394a247d3" -dependencies = [ - "openssl-probe", - "rustls-pki-types", - "schannel", - "security-framework 3.4.0", -] - -[[package]] -name = "rustls-pemfile" -version = "1.0.4" +name = "rustls" +version = "0.23.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" +checksum = "c0ebcbd2f03de0fc1122ad9bb24b127a5a6cd51d72604a3f3c50ac459762b6cc" dependencies = [ - "base64 0.21.7", + "log", + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", ] [[package]] @@ -3158,27 +1599,15 @@ version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79" dependencies = [ - "web-time", "zeroize", ] -[[package]] -name = "rustls-webpki" -version = "0.101.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" -dependencies = [ - "ring", - "untrusted", -] - [[package]] name = "rustls-webpki" version = "0.103.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0a17884ae0c1b773f1ccd2bd4a8c72f16da897310a98b0e84bf349ad5ead92fc" dependencies = [ - "aws-lc-rs", "ring", "rustls-pki-types", "untrusted", @@ -3205,81 +1634,12 @@ dependencies = [ "winapi-util", ] -[[package]] -name = "schannel" -version = "0.1.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d" -dependencies = [ - "windows-sys 0.59.0", -] - [[package]] name = "scopeguard" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" -[[package]] -name = "sct" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" -dependencies = [ - "ring", - "untrusted", -] - -[[package]] -name = "sec1" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3be24c1842290c45df0a7bf069e0c268a747ad05a192f2fd7dcfdbc1cba40928" -dependencies = [ - "base16ct", - "der 0.6.1", - "generic-array", - "pkcs8 0.9.0", - "subtle", - "zeroize", -] - -[[package]] -name = "security-framework" -version = "2.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" -dependencies = [ - "bitflags 2.9.4", - "core-foundation 0.9.4", - "core-foundation-sys", - "libc", - "security-framework-sys", -] - -[[package]] -name = "security-framework" -version = "3.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60b369d18893388b345804dc0007963c99b7d665ae71d275812d828c6f089640" -dependencies = [ - "bitflags 2.9.4", - "core-foundation 0.10.1", - "core-foundation-sys", - "libc", - "security-framework-sys", -] - -[[package]] -name = "security-framework-sys" -version = "2.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0" -dependencies = [ - "core-foundation-sys", - "libc", -] - [[package]] name = "semver" version = "1.0.26" @@ -3330,18 +1690,6 @@ dependencies = [ "serde", ] -[[package]] -name = "serde_urlencoded" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" -dependencies = [ - "form_urlencoded", - "itoa", - "ryu", - "serde", -] - [[package]] name = "serde_yaml" version = "0.9.34+deprecated" @@ -3391,17 +1739,6 @@ dependencies = [ "digest", ] -[[package]] -name = "sha2" -version = "0.10.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" -dependencies = [ - "cfg-if", - "cpufeatures", - "digest", -] - [[package]] name = "shlex" version = "1.3.0" @@ -3417,28 +1754,6 @@ dependencies = [ "libc", ] -[[package]] -name = "signature" -version = "1.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74233d3b3b2f6d4b006dc19dee745e73e2a6bfb6f93607cd3b02bd5b00797d7c" -dependencies = [ - "digest", - "rand_core 0.6.4", -] - -[[package]] -name = "simple_asn1" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "297f631f50729c8c99b84667867963997ec0b50f32b2a7dbcab828ef0541e8bb" -dependencies = [ - "num-bigint", - "num-traits", - "thiserror 2.0.16", - "time", -] - [[package]] name = "slab" version = "0.4.11" @@ -3451,16 +1766,6 @@ version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" -[[package]] -name = "socket2" -version = "0.5.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" -dependencies = [ - "libc", - "windows-sys 0.52.0", -] - [[package]] name = "socket2" version = "0.6.0" @@ -3471,26 +1776,6 @@ dependencies = [ "windows-sys 0.59.0", ] -[[package]] -name = "spki" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67cf02bbac7a337dc36e4f5a693db6c21e7863f45070f7064577eb4367a3212b" -dependencies = [ - "base64ct", - "der 0.6.1", -] - -[[package]] -name = "spki" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" -dependencies = [ - "base64ct", - "der 0.7.10", -] - [[package]] name = "stable_deref_trait" version = "1.2.0" @@ -3568,15 +1853,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "sync_wrapper" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" -dependencies = [ - "futures-core", -] - [[package]] name = "synstructure" version = "0.13.2" @@ -3595,13 +1871,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b010f5ebe51e88ae490691ed2a43b699e3468c8e3838e244accd8526aca7751b" dependencies = [ "anyhow", - "aws-config", - "aws-credential-types", - "aws-sdk-s3", "byteorder", "chrono", "flate2", - "google-cloud-storage", "log", "ring", "rusqlite", @@ -3610,7 +1882,6 @@ dependencies = [ "strum 0.27.2", "strum_macros 0.27.2", "thiserror 2.0.16", - "tokio", "ureq", "url", "uuid", @@ -3627,6 +1898,7 @@ dependencies = [ "serde", "serde_json", "taskchampion", + "thiserror 1.0.69", "tokio", "uuid", ] @@ -3640,7 +1912,7 @@ dependencies = [ "fastrand", "getrandom 0.3.3", "once_cell", - "rustix 1.0.8", + "rustix", "windows-sys 0.60.2", ] @@ -3693,36 +1965,6 @@ dependencies = [ "num_cpus", ] -[[package]] -name = "time" -version = "0.3.43" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83bde6f1ec10e72d583d91623c939f623002284ef622b87de38cfd546cbf2031" -dependencies = [ - "deranged", - "num-conv", - "powerfmt", - "serde", - "time-core", - "time-macros", -] - -[[package]] -name = "time-core" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b" - -[[package]] -name = "time-macros" -version = "0.2.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30cfb0125f12d9c277f35663a0a33f8c30190f4e4574868a330595412d34ebf3" -dependencies = [ - "num-conv", - "time-core", -] - [[package]] name = "tinystr" version = "0.8.1" @@ -3733,21 +1975,6 @@ dependencies = [ "zerovec", ] -[[package]] -name = "tinyvec" -version = "1.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" -dependencies = [ - "tinyvec_macros", -] - -[[package]] -name = "tinyvec_macros" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" - [[package]] name = "tokio" version = "1.47.1" @@ -3763,7 +1990,7 @@ dependencies = [ "pin-project-lite", "signal-hook-registry", "slab", - "socket2 0.6.0", + "socket2", "tokio-macros", "windows-sys 0.59.0", ] @@ -3779,39 +2006,6 @@ dependencies = [ "syn 2.0.106", ] -[[package]] -name = "tokio-rustls" -version = "0.24.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" -dependencies = [ - "rustls 0.21.12", - "tokio", -] - -[[package]] -name = "tokio-rustls" -version = "0.26.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e727b36a1a0e8b74c376ac2211e40c2c8af09fb4013c60d910495810f008e9b" -dependencies = [ - "rustls 0.23.31", - "tokio", -] - -[[package]] -name = "tokio-util" -version = "0.7.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14307c986784f72ef81c89db7d9e28d6ac26d16213b109ea501696195e6e3ce5" -dependencies = [ - "bytes", - "futures-core", - "futures-sink", - "pin-project-lite", - "tokio", -] - [[package]] name = "toml" version = "0.5.11" @@ -3868,100 +2062,12 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ea68304e134ecd095ac6c3574494fc62b909f416c4fca77e440530221e549d3d" -[[package]] -name = "tower" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" -dependencies = [ - "futures-core", - "futures-util", - "pin-project-lite", - "sync_wrapper", - "tokio", - "tower-layer", - "tower-service", -] - -[[package]] -name = "tower-http" -version = "0.6.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" -dependencies = [ - "bitflags 2.9.4", - "bytes", - "futures-util", - "http 1.3.1", - "http-body 1.0.1", - "iri-string", - "pin-project-lite", - "tower", - "tower-layer", - "tower-service", -] - -[[package]] -name = "tower-layer" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" - -[[package]] -name = "tower-service" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" - -[[package]] -name = "tracing" -version = "0.1.41" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" -dependencies = [ - "pin-project-lite", - "tracing-attributes", - "tracing-core", -] - -[[package]] -name = "tracing-attributes" -version = "0.1.30" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.106", -] - -[[package]] -name = "tracing-core" -version = "0.1.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" -dependencies = [ - "once_cell", -] - -[[package]] -name = "try-lock" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" - [[package]] name = "typenum" version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" -[[package]] -name = "unicase" -version = "2.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" - [[package]] name = "unicode-ident" version = "1.0.18" @@ -3998,11 +2104,11 @@ version = "2.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "02d1a66277ed75f640d608235660df48c8e3c19f3b4edb6a263315626cc3c01d" dependencies = [ - "base64 0.22.1", + "base64", "flate2", "log", "once_cell", - "rustls 0.23.31", + "rustls", "rustls-pki-types", "url", "webpki-roots 0.26.11", @@ -4020,12 +2126,6 @@ dependencies = [ "serde", ] -[[package]] -name = "urlencoding" -version = "2.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" - [[package]] name = "utf8_iter" version = "1.0.4" @@ -4062,12 +2162,6 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" -[[package]] -name = "vsimd" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c3082ca00d5a5ef149bb8b555a72ae84c9c59f7250f013ac822ac2e49b19c64" - [[package]] name = "walkdir" version = "2.5.0" @@ -4078,15 +2172,6 @@ dependencies = [ "winapi-util", ] -[[package]] -name = "want" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" -dependencies = [ - "try-lock", -] - [[package]] name = "wasi" version = "0.11.1+wasi-snapshot-preview1" @@ -4174,19 +2259,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "wasm-streams" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65" -dependencies = [ - "futures-util", - "js-sys", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", -] - [[package]] name = "web-sys" version = "0.3.78" @@ -4225,18 +2297,6 @@ dependencies = [ "rustls-pki-types", ] -[[package]] -name = "which" -version = "4.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7" -dependencies = [ - "either", - "home", - "once_cell", - "rustix 0.38.44", -] - [[package]] name = "winapi" version = "0.3.9" @@ -4570,12 +2630,6 @@ version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" -[[package]] -name = "xmlparser" -version = "0.13.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66fee0b777b0f5ac1c69bb06d361268faafa61cd4682ae064a171c16c433e9e4" - [[package]] name = "yoke" version = "0.8.0" diff --git a/rust/Cargo.toml b/rust/Cargo.toml index 934ed553..80228819 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -1,19 +1,26 @@ [package] name = "tc_helper" version = "0.1.0" -edition = "2024" +edition = "2021" [lib] name="tc_helper" crate-type=["staticlib","cdylib"] [dependencies] -taskchampion = "2.0.3" +# Only the "server-sync" backend (the remote TaskChampion sync server, used by +# our sync_() via ServerConfig::Remote) — NOT server-aws / server-gcp. Those +# cloud backends dragged in the AWS + Google-Cloud SDKs → aws-lc-rs/aws-lc-sys, +# which (a) bloats the binary, (b) needs bindgen for 32-bit ARM, and (c) fails +# to cross-compile for iOS. server-sync uses ureq + ring, which build cleanly +# on every target. `bundled` keeps SQLite compiled in. +taskchampion = { version = "2.0.3", default-features = false, features = ["server-sync", "bundled"] } anyhow = "1.0" tokio = { version = "1.40", features = ["full"] } uuid = "1.0" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" +thiserror = "1.0" # Structured, typed error handling flutter_rust_bridge = "=2.11.1" # Rust runtime bridge flutter_rust_bridge_macros = "2.11.1" # Procedural macros support diff --git a/rust/build.rs b/rust/build.rs new file mode 100644 index 00000000..a66a62ab --- /dev/null +++ b/rust/build.rs @@ -0,0 +1,53 @@ +use std::env; +use std::process::Command; + +/// Build script for `tc_helper`. +/// +/// * Rebuilds whenever anything under `src/` changes so the FFI stays in sync. +/// * Detects the target platform and exposes it as a build-time note. +/// * Optionally regenerates the flutter_rust_bridge bindings when the +/// `FRB_CODEGEN=1` environment variable is set, so contributors can refresh +/// bindings from a plain `cargo build` without memorising the CLI flags. +/// (It is opt-in so ordinary/CI builds never shell out to the codegen tool.) +fn main() { + println!("cargo:rerun-if-changed=src/"); + println!("cargo:rerun-if-env-changed=FRB_CODEGEN"); + + let target = env::var("TARGET").unwrap_or_default(); + let platform = if target.contains("android") { + "android" + } else if target.contains("apple-ios") { + "ios" + } else if target.contains("apple-darwin") { + "macos" + } else if target.contains("linux") { + "linux" + } else if target.contains("windows") { + "windows" + } else { + "unknown" + }; + // Expose the detected platform as a compile-time env var (readable via + // env!("TC_HELPER_PLATFORM")) instead of a per-build warning, so ordinary + // builds stay quiet. + println!("cargo:rustc-env=TC_HELPER_PLATFORM={platform}"); + + if env::var("FRB_CODEGEN").as_deref() == Ok("1") { + let status = Command::new("flutter_rust_bridge_codegen") + .args([ + "generate", + "--rust-input", + "crate::api", + "--rust-root", + ".", + "--dart-output", + "../lib/rust_bridge", + ]) + .status(); + match status { + Ok(s) if s.success() => println!("cargo:warning=flutter_rust_bridge bindings regenerated"), + Ok(s) => println!("cargo:warning=flutter_rust_bridge_codegen exited with {s}"), + Err(e) => println!("cargo:warning=could not run flutter_rust_bridge_codegen: {e}"), + } + } +} diff --git a/rust/src/api.rs b/rust/src/api.rs index 5ee13205..87a6dfe4 100644 --- a/rust/src/api.rs +++ b/rust/src/api.rs @@ -1,11 +1,14 @@ use flutter_rust_bridge::frb; +use std::{collections::HashMap, str::FromStr}; use taskchampion::{ chrono::{DateTime, Utc}, - Operations, Replica, ServerConfig, StorageConfig, Tag, + Operations, Tag, ServerConfig, }; use uuid::Uuid; -use std::{collections::HashMap, path::PathBuf, str::FromStr}; -use serde_json; + +use crate::serialize::task_to_json; +use crate::storage::open_replica; +use crate::utils::error::TcHelperError; fn parse_datetime(input: &str) -> Option> { if input.trim().is_empty() { @@ -14,90 +17,69 @@ fn parse_datetime(input: &str) -> Option> { input.parse::>().ok() } +/// Return every task in the replica as a JSON array string. #[frb] -pub fn get_all_tasks_json(taskdb_dir_path: String) -> Result { - let tasks = get_all_tasks(taskdb_dir_path); // your Vec> - let json = serde_json::to_string(&tasks) - .map_err(|e| taskchampion::Error::Other(anyhow::anyhow!(e)))?; - Ok(json) +pub fn get_all_tasks_json(taskdb_dir_path: String) -> Result { + get_all_tasks_json_impl(&taskdb_dir_path).map_err(|e| e.to_string()) } -fn get_all_tasks(taskdb_dir_path: String) -> Vec> { - let taskdb_dir = PathBuf::from(taskdb_dir_path); - let storage = StorageConfig::OnDisk { - taskdb_dir, - create_if_missing: true, - access_mode: taskchampion::storage::AccessMode::ReadWrite, - } - .into_storage() - .unwrap(); - - let mut replica = Replica::new(storage); - let mut vector: Vec> = Vec::new(); +fn get_all_tasks_json_impl(taskdb_dir_path: &str) -> Result { + let mut replica = open_replica(taskdb_dir_path)?; + let tasks = replica + .all_tasks() + .map_err(|e| TcHelperError::Champion(e.to_string()))?; - for (_, value) in replica.all_tasks().unwrap() { - let mut map: HashMap = HashMap::new(); - let mut tags = "".to_string(); - - for (k, v) in value.get_taskmap() { - if k.contains("tag_") { - if let Some(stripped) = k.strip_prefix("tag_") { - tags.push_str(stripped); - tags.push(' '); - } - } else { - map.insert(k.into(), v.into()); - } - } - map.insert("tags".into(), tags.trim().into()); - map.insert("uuid".into(), value.get_uuid().to_string()); - vector.push(map); - } - vector + let json_tasks: Vec = tasks.values().map(task_to_json).collect(); + Ok(serde_json::to_string(&json_tasks)?) } +/// Delete the task with the given UUID. A no-op if the task does not exist. #[frb] -pub fn delete_task(uuid_st: String, taskdb_dir_path: String) -> i8 { - let taskdb_dir = PathBuf::from(taskdb_dir_path); - let storage = StorageConfig::OnDisk { - taskdb_dir, - create_if_missing: true, - access_mode: taskchampion::storage::AccessMode::ReadWrite, - } - .into_storage() - .unwrap(); +pub fn delete_task(uuid_st: String, taskdb_dir_path: String) -> Result<(), String> { + delete_task_impl(&uuid_st, &taskdb_dir_path).map_err(|e| e.to_string()) +} - let mut replica = Replica::new(storage); +fn delete_task_impl(uuid_st: &str, taskdb_dir_path: &str) -> Result<(), TcHelperError> { + let mut replica = open_replica(taskdb_dir_path)?; let mut ops = Operations::new(); - let uuid = Uuid::parse_str(&uuid_st).unwrap(); + let uuid = Uuid::parse_str(uuid_st).map_err(|_| TcHelperError::InvalidUuid(uuid_st.to_string()))?; - if let Some(mut t) = replica.get_task_data(uuid).unwrap() { + if let Some(mut t) = replica + .get_task_data(uuid) + .map_err(|e| TcHelperError::Champion(e.to_string()))? + { t.delete(&mut ops); } - replica.commit_operations(ops).unwrap(); - 0 + replica + .commit_operations(ops) + .map_err(|e| TcHelperError::Commit(e.to_string()))?; + Ok(()) } +/// Update the mutable fields of an existing task from the supplied key/value map. #[frb] pub fn update_task( uuid_st: String, taskdb_dir_path: String, map: HashMap, -) -> i8 { - let taskdb_dir = PathBuf::from(taskdb_dir_path); - let storage = StorageConfig::OnDisk { - taskdb_dir, - create_if_missing: true, - access_mode: taskchampion::storage::AccessMode::ReadWrite, - } - .into_storage() - .unwrap(); +) -> Result<(), String> { + update_task_impl(&uuid_st, &taskdb_dir_path, map).map_err(|e| e.to_string()) +} - let mut replica = Replica::new(storage); +#[allow(deprecated)] // `get_taskmap` is deprecated upstream; used to enumerate existing tags. +fn update_task_impl( + uuid_st: &str, + taskdb_dir_path: &str, + map: HashMap, +) -> Result<(), TcHelperError> { + let mut replica = open_replica(taskdb_dir_path)?; let mut ops = Operations::new(); - let uuid = Uuid::parse_str(&uuid_st).unwrap(); + let uuid = Uuid::parse_str(uuid_st).map_err(|_| TcHelperError::InvalidUuid(uuid_st.to_string()))?; - if let Some(mut t) = replica.get_task(uuid).unwrap() { + if let Some(mut t) = replica + .get_task(uuid) + .map_err(|e| TcHelperError::Champion(e.to_string()))? + { let _ = t.set_status(taskchampion::Status::Pending, &mut ops); for (key, value) in map { match key.as_str() { @@ -121,21 +103,20 @@ pub fn update_task( let _ = t.set_priority(value, &mut ops); } "tags" => { - let existing_tags: Vec = t - .get_taskmap() - .iter() - .filter_map(|(k, _)| k.strip_prefix("tag_").map(|s| s.to_string())) - .collect(); - for tag_name in existing_tags { - println!("removing tag at rust side {}", tag_name); - let mut tag = Tag::from_str(&tag_name).unwrap(); - let _ = t.remove_tag(&mut tag, &mut ops); - } - - for part in value.split_whitespace() { - println!("tag at rust side {}", part); - let mut tag = Tag::from_str(part).unwrap(); - let _ = t.add_tag(&mut tag, &mut ops); + let existing_tags: Vec = t + .get_taskmap() + .iter() + .filter_map(|(k, _)| k.strip_prefix("tag_").map(|s| s.to_string())) + .collect(); + for tag_name in existing_tags { + if let Ok(mut tag) = Tag::from_str(&tag_name) { + let _ = t.remove_tag(&mut tag, &mut ops); + } + } + for part in value.split_whitespace() { + if let Ok(mut tag) = Tag::from_str(part) { + let _ = t.add_tag(&mut tag, &mut ops); + } } } "project" => { @@ -148,35 +129,37 @@ pub fn update_task( "deleted" => taskchampion::Status::Deleted, _ => taskchampion::Status::Pending, }; - // print!("status at rust side {}", value); - println!("status at rust side {}", value); let _ = t.set_status(status, &mut ops); } _ => {} } } - replica.commit_operations(ops).unwrap(); + replica + .commit_operations(ops) + .map_err(|e| TcHelperError::Commit(e.to_string()))?; } - 0 + Ok(()) } +/// Create a new task from the supplied key/value map. The map must contain a +/// `uuid` entry. #[frb] -pub fn add_task(taskdb_dir_path: String, map: HashMap) -> i8 { - let taskdb_dir = PathBuf::from(taskdb_dir_path); - let storage = StorageConfig::OnDisk { - taskdb_dir, - create_if_missing: true, - access_mode: taskchampion::storage::AccessMode::ReadWrite, - } - .into_storage() - .unwrap(); +pub fn add_task(taskdb_dir_path: String, map: HashMap) -> Result<(), String> { + add_task_impl(&taskdb_dir_path, map).map_err(|e| e.to_string()) +} - let mut replica = Replica::new(storage); +fn add_task_impl(taskdb_dir_path: &str, map: HashMap) -> Result<(), TcHelperError> { + let mut replica = open_replica(taskdb_dir_path)?; let mut ops = Operations::new(); - if let Some(uuid_str) = map.get("uuid") { - let uuid = Uuid::parse_str(&uuid_str).unwrap(); - let mut t = replica.create_task(uuid, &mut ops).unwrap(); + let uuid_str = map + .get("uuid") + .ok_or_else(|| TcHelperError::InvalidUuid("".to_string()))?; + let uuid = Uuid::parse_str(uuid_str).map_err(|_| TcHelperError::InvalidUuid(uuid_str.clone()))?; + + let mut t = replica + .create_task(uuid, &mut ops) + .map_err(|e| TcHelperError::Champion(e.to_string()))?; let _ = t.set_status(taskchampion::Status::Pending, &mut ops); for (key, value) in map { @@ -198,8 +181,9 @@ pub fn add_task(taskdb_dir_path: String, map: HashMap) -> i8 { } "tags" => { for part in value.split_whitespace() { - let mut tag = Tag::from_str(part).unwrap(); - let _ = t.add_tag(&mut tag, &mut ops); + if let Ok(mut tag) = Tag::from_str(part) { + let _ = t.add_tag(&mut tag, &mut ops); + } } } "project" => { @@ -208,43 +192,53 @@ pub fn add_task(taskdb_dir_path: String, map: HashMap) -> i8 { _ => {} } } - replica.commit_operations(ops).unwrap(); - return 0; -} - 1 + replica + .commit_operations(ops) + .map_err(|e| TcHelperError::Commit(e.to_string()))?; + Ok(()) } +/// Synchronise the local replica with a remote TaskChampion sync server. #[frb] pub async fn sync( taskdb_dir_path: String, url: String, client_id: String, encryption_secret: String, -) -> i8 { - let taskdb_dir = PathBuf::from(taskdb_dir_path); - let storage = StorageConfig::OnDisk { - taskdb_dir, - create_if_missing: true, - access_mode: taskchampion::storage::AccessMode::ReadWrite, - } - .into_storage() - .unwrap(); +) -> Result<(), String> { + sync_impl(&taskdb_dir_path, url, &client_id, encryption_secret).map_err(|e| e.to_string()) +} + +fn sync_impl( + taskdb_dir_path: &str, + url: String, + client_id: &str, + encryption_secret: String, +) -> Result<(), TcHelperError> { + let mut replica = open_replica(taskdb_dir_path)?; + let client_uuid = + Uuid::parse_str(client_id).map_err(|_| TcHelperError::InvalidUuid(client_id.to_string()))?; - let mut replica = Replica::new(storage); let config = ServerConfig::Remote { url: url.into(), - client_id: Uuid::parse_str(&client_id).unwrap(), + client_id: client_uuid, encryption_secret: encryption_secret.into(), }; - let mut server = config.into_server().unwrap(); - replica.sync(&mut server, false).unwrap(); - 0 + let mut server = config + .into_server() + .map_err(|e| TcHelperError::Sync(e.to_string()))?; + replica + .sync(&mut server, false) + .map_err(|e| TcHelperError::Sync(e.to_string()))?; + Ok(()) } #[test] fn test_add_task_with_tags() { use std::{collections::HashMap, env, fs}; + use serde_json::Value; + // create unique temporary directory for taskdb let tmp = env::temp_dir().join(format!("taskdb_test_{}", Uuid::new_v4())); let taskdb_path = tmp.to_string_lossy().into_owned(); @@ -258,19 +252,96 @@ fn test_add_task_with_tags() { map.insert("tags".to_string(), "tag1 tag2".to_string()); // add task - let res = add_task(taskdb_path.clone(), map); - assert_eq!(res, 0); + add_task(taskdb_path.clone(), map).expect("add_task"); - // read tasks as json and verify tags are present + // read tasks as json and verify the surfaced attributes let json = get_all_tasks_json(taskdb_path.clone()).expect("get_all_tasks_json"); - let tasks: Vec> = serde_json::from_str(&json).expect("parse json"); - let found = tasks.into_iter().find(|m| m.get("uuid").map(|s| s == &uuid).unwrap_or(false)); - assert!(found.is_some(), "task with uuid not found"); - let task = found.unwrap(); - let tags = task.get("tags").map(|s| s.as_str()).unwrap_or(""); + let tasks: Vec = serde_json::from_str(&json).expect("parse json"); + let task = tasks + .into_iter() + .find(|t| t.get("uuid").and_then(|u| u.as_str()) == Some(uuid.as_str())) + .expect("task with uuid not found"); + + let tags = task.get("tags").and_then(|t| t.as_str()).unwrap_or(""); assert!(tags.contains("tag1"), "tag1 missing in tags: {}", tags); assert!(tags.contains("tag2"), "tag2 missing in tags: {}", tags); + // newly surfaced attributes should be present with sensible defaults + assert!(task.get("annotations").map(|v| v.is_array()).unwrap_or(false)); + assert!(task.get("depends").map(|v| v.is_array()).unwrap_or(false)); + assert_eq!(task.get("is_blocked").and_then(|v| v.as_bool()), Some(false)); + assert_eq!(task.get("is_blocking").and_then(|v| v.as_bool()), Some(false)); + // cleanup fs::remove_dir_all(&tmp).ok(); } + +#[test] +fn test_dependencies_and_annotations_surface() { + // Exercises the POPULATED cases of the enriched serializer: a real + // dependency (A depends on B) must surface as depends[]/is_blocked/is_blocking, + // and an annotation must surface as {entry (RFC3339), description}. + use std::{env, fs}; + use serde_json::Value; + use taskchampion::{chrono::{DateTime, Utc}, Annotation, Operations, Status}; + + let tmp = env::temp_dir().join(format!("taskdb_deptest_{}", Uuid::new_v4())); + let taskdb_path = tmp.to_string_lossy().into_owned(); + fs::create_dir_all(&tmp).expect("create temp taskdb dir"); + + let uuid_a = Uuid::new_v4(); // dependent task + let uuid_b = Uuid::new_v4(); // blocker task + + { + let mut replica = open_replica(&taskdb_path).expect("open replica"); + let mut ops = Operations::new(); + + let mut b = replica.create_task(uuid_b, &mut ops).expect("create B"); + let _ = b.set_status(Status::Pending, &mut ops); + let _ = b.set_description("blocker".to_string(), &mut ops); + + let mut a = replica.create_task(uuid_a, &mut ops).expect("create A"); + let _ = a.set_status(Status::Pending, &mut ops); + let _ = a.set_description("dependent".to_string(), &mut ops); + a.add_dependency(uuid_b, &mut ops).expect("add dependency A->B"); + a.add_annotation( + Annotation { entry: Utc::now(), description: "note-one".to_string() }, + &mut ops, + ).expect("add annotation"); + + replica.commit_operations(ops).expect("commit"); + } + + // Read back through the SAME path the app uses (fresh replica + all_tasks()). + let json = get_all_tasks_json(taskdb_path.clone()).expect("get_all_tasks_json"); + let tasks: Vec = serde_json::from_str(&json).expect("parse json"); + let find = |u: &Uuid| { + tasks + .iter() + .find(|t| t.get("uuid").and_then(|v| v.as_str()) == Some(u.to_string().as_str())) + .cloned() + .expect("task present") + }; + let a = find(&uuid_a); + let b = find(&uuid_b); + + // A depends on B → depends[] carries B, and A is blocked (unresolved dep). + let deps: Vec = a["depends"].as_array().unwrap() + .iter().map(|v| v.as_str().unwrap().to_string()).collect(); + assert!(deps.contains(&uuid_b.to_string()), "A.depends must contain B: {:?}", deps); + assert_eq!(a["is_blocked"].as_bool(), Some(true), "A must be blocked"); + assert_eq!(a["is_blocking"].as_bool(), Some(false), "A must not be blocking"); + + // A's annotation surfaces with description + RFC3339 entry. + let anns = a["annotations"].as_array().unwrap(); + assert_eq!(anns.len(), 1, "A must have one annotation"); + assert_eq!(anns[0]["description"].as_str(), Some("note-one")); + let entry = anns[0]["entry"].as_str().unwrap(); + assert!(DateTime::parse_from_rfc3339(entry).is_ok(), "entry must be RFC3339: {}", entry); + + // B is depended-upon → B is blocking, not blocked. + assert_eq!(b["is_blocking"].as_bool(), Some(true), "B must be blocking"); + assert_eq!(b["is_blocked"].as_bool(), Some(false), "B must not be blocked"); + + fs::remove_dir_all(&tmp).ok(); +} diff --git a/rust/src/frb_generated.rs b/rust/src/frb_generated.rs index 9f690200..8590af35 100644 --- a/rust/src/frb_generated.rs +++ b/rust/src/frb_generated.rs @@ -72,9 +72,8 @@ fn wire__crate__api__add_task_impl( >::sse_decode(&mut deserializer); deserializer.end(); move |context| { - transform_result_sse::<_, ()>((move || { - let output_ok = - Result::<_, ()>::Ok(crate::api::add_task(api_taskdb_dir_path, api_map))?; + transform_result_sse::<_, String>((move || { + let output_ok = crate::api::add_task(api_taskdb_dir_path, api_map)?; Ok(output_ok) })()) } @@ -107,11 +106,8 @@ fn wire__crate__api__delete_task_impl( let api_taskdb_dir_path = ::sse_decode(&mut deserializer); deserializer.end(); move |context| { - transform_result_sse::<_, ()>((move || { - let output_ok = Result::<_, ()>::Ok(crate::api::delete_task( - api_uuid_st, - api_taskdb_dir_path, - ))?; + transform_result_sse::<_, String>((move || { + let output_ok = crate::api::delete_task(api_uuid_st, api_taskdb_dir_path)?; Ok(output_ok) })()) } @@ -143,12 +139,10 @@ fn wire__crate__api__get_all_tasks_json_impl( let api_taskdb_dir_path = ::sse_decode(&mut deserializer); deserializer.end(); move |context| { - transform_result_sse::<_, flutter_rust_bridge::for_generated::anyhow::Error>( - (move || { - let output_ok = crate::api::get_all_tasks_json(api_taskdb_dir_path)?; - Ok(output_ok) - })(), - ) + transform_result_sse::<_, String>((move || { + let output_ok = crate::api::get_all_tasks_json(api_taskdb_dir_path)?; + Ok(output_ok) + })()) } }, ) @@ -181,17 +175,15 @@ fn wire__crate__api__sync_impl( let api_encryption_secret = ::sse_decode(&mut deserializer); deserializer.end(); move |context| async move { - transform_result_sse::<_, ()>( + transform_result_sse::<_, String>( (move || async move { - let output_ok = Result::<_, ()>::Ok( - crate::api::sync( - api_taskdb_dir_path, - api_url, - api_client_id, - api_encryption_secret, - ) - .await, - )?; + let output_ok = crate::api::sync( + api_taskdb_dir_path, + api_url, + api_client_id, + api_encryption_secret, + ) + .await?; Ok(output_ok) })() .await, @@ -228,12 +220,9 @@ fn wire__crate__api__update_task_impl( >::sse_decode(&mut deserializer); deserializer.end(); move |context| { - transform_result_sse::<_, ()>((move || { - let output_ok = Result::<_, ()>::Ok(crate::api::update_task( - api_uuid_st, - api_taskdb_dir_path, - api_map, - ))?; + transform_result_sse::<_, String>((move || { + let output_ok = + crate::api::update_task(api_uuid_st, api_taskdb_dir_path, api_map)?; Ok(output_ok) })()) } @@ -243,14 +232,6 @@ fn wire__crate__api__update_task_impl( // Section: dart2rust -impl SseDecode for flutter_rust_bridge::for_generated::anyhow::Error { - // Codec=Sse (Serialization based), see doc to use other codecs - fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { - let mut inner = ::sse_decode(deserializer); - return flutter_rust_bridge::for_generated::anyhow::anyhow!("{}", inner); - } -} - impl SseDecode for std::collections::HashMap { // Codec=Sse (Serialization based), see doc to use other codecs fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { @@ -267,13 +248,6 @@ impl SseDecode for String { } } -impl SseDecode for i8 { - // Codec=Sse (Serialization based), see doc to use other codecs - fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { - deserializer.cursor.read_i8().unwrap() - } -} - impl SseDecode for Vec { // Codec=Sse (Serialization based), see doc to use other codecs fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { @@ -365,13 +339,6 @@ fn pde_ffi_dispatcher_sync_impl( // Section: rust2dart -impl SseEncode for flutter_rust_bridge::for_generated::anyhow::Error { - // Codec=Sse (Serialization based), see doc to use other codecs - fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { - ::sse_encode(format!("{:?}", self), serializer); - } -} - impl SseEncode for std::collections::HashMap { // Codec=Sse (Serialization based), see doc to use other codecs fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { @@ -386,13 +353,6 @@ impl SseEncode for String { } } -impl SseEncode for i8 { - // Codec=Sse (Serialization based), see doc to use other codecs - fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { - serializer.cursor.write_i8(self).unwrap(); - } -} - impl SseEncode for Vec { // Codec=Sse (Serialization based), see doc to use other codecs fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { diff --git a/rust/src/lib.rs b/rust/src/lib.rs index b07ba846..a4db1e3d 100644 --- a/rust/src/lib.rs +++ b/rust/src/lib.rs @@ -1,2 +1,5 @@ mod frb_generated; /* AUTO INJECTED BY flutter_rust_bridge. This line may not be accurate, and you can change it according to your needs. */ -mod api; \ No newline at end of file +mod api; +mod serialize; +mod storage; +mod utils; \ No newline at end of file diff --git a/rust/src/serialize.rs b/rust/src/serialize.rs new file mode 100644 index 00000000..18b6b4f3 --- /dev/null +++ b/rust/src/serialize.rs @@ -0,0 +1,66 @@ +use serde_json::{json, Value}; +use taskchampion::Task; + +/// Serialise a TaskChampion [`Task`] into the JSON object the Flutter layer +/// consumes. +/// +/// Beyond the flat properties the previous serialiser emitted, this now +/// surfaces attributes the Dart model already anticipated but never received: +/// +/// * `tags` — space-joined user tags (synthetic tags are excluded) +/// * `annotations` — array of `{ entry, description }` objects +/// * `depends` — array of dependency UUID strings +/// * `is_blocked` — whether the task has at least one unresolved dependency +/// * `is_blocking` — whether at least one other task depends on this one +/// * `recur` — recurrence rule, when present +/// +/// `urgency` is intentionally omitted: TaskChampion 2.0.3 does not compute or +/// store an urgency value on [`Task`], so there is nothing authoritative to +/// surface here. +#[allow(deprecated)] // `get_taskmap` is deprecated upstream; retained for the raw property view. +pub fn task_to_json(task: &Task) -> Value { + let mut map = serde_json::Map::new(); + let mut tags: Vec = Vec::new(); + + for (key, value) in task.get_taskmap() { + if let Some(tag) = key.strip_prefix("tag_") { + // User tags are stored as `tag_` properties. + tags.push(tag.to_string()); + } else if key.starts_with("dep_") || key.starts_with("annotation_") { + // Raw dependency/annotation properties are surfaced below as + // structured arrays, so skip their flat representation here. + continue; + } else { + // Flat properties (description, status, due, priority, project, + // recur, ...) pass straight through as strings. + map.insert(key.clone(), Value::String(value.clone())); + } + } + + let annotations: Vec = task + .get_annotations() + .map(|a| { + json!({ + // RFC 3339 / ISO-8601 so consumers get an unambiguous, + // directly-parseable timestamp (not a bare epoch number). + "entry": a.entry.to_rfc3339(), + "description": a.description, + }) + }) + .collect(); + + let depends: Vec = task + .get_dependencies() + .map(|uuid| Value::String(uuid.to_string())) + .collect(); + + map.insert("uuid".into(), Value::String(task.get_uuid().to_string())); + map.insert("tags".into(), Value::String(tags.join(" "))); + map.insert("annotations".into(), Value::Array(annotations)); + map.insert("depends".into(), Value::Array(depends)); + map.insert("is_blocked".into(), Value::Bool(task.is_blocked())); + map.insert("is_blocking".into(), Value::Bool(task.is_blocking())); + // `recur` is already carried through the flat-property loop above. + + Value::Object(map) +} diff --git a/rust/src/storage.rs b/rust/src/storage.rs new file mode 100644 index 00000000..ecee1037 --- /dev/null +++ b/rust/src/storage.rs @@ -0,0 +1,25 @@ +use std::path::PathBuf; +use taskchampion::{Replica, StorageConfig}; + +use crate::utils::error::TcHelperError; + +/// Open (creating if necessary) the on-disk TaskChampion replica at +/// `taskdb_dir_path` in read/write mode. +/// +/// This centralises the storage-configuration boilerplate that was previously +/// duplicated across every FFI entry point. +pub fn open_replica(taskdb_dir_path: &str) -> Result { + let taskdb_dir = PathBuf::from(taskdb_dir_path); + let storage = StorageConfig::OnDisk { + taskdb_dir, + create_if_missing: true, + access_mode: taskchampion::storage::AccessMode::ReadWrite, + } + .into_storage() + .map_err(|e| TcHelperError::ReplicaOpen { + path: taskdb_dir_path.to_string(), + message: e.to_string(), + })?; + + Ok(Replica::new(storage)) +} diff --git a/rust/src/utils/error.rs b/rust/src/utils/error.rs new file mode 100644 index 00000000..a032803b --- /dev/null +++ b/rust/src/utils/error.rs @@ -0,0 +1,29 @@ +use thiserror::Error; + +/// Errors raised by the `tc_helper` FFI layer. +/// +/// Each FFI entry point in [`crate::api`] converts one of these into a plain +/// error string, which flutter_rust_bridge surfaces to the Dart side as a +/// thrown exception. Modelling the failure modes as a typed enum keeps the +/// Rust code free of `.unwrap()` panics and makes the source of a failure +/// explicit at the call site. +#[derive(Error, Debug)] +pub enum TcHelperError { + #[error("cannot open replica at '{path}': {message}")] + ReplicaOpen { path: String, message: String }, + + #[error("invalid UUID '{0}'")] + InvalidUuid(String), + + #[error("sync failed: {0}")] + Sync(String), + + #[error("task operation failed: {0}")] + Commit(String), + + #[error("taskchampion error: {0}")] + Champion(String), + + #[error("JSON serialization error: {0}")] + Serialization(#[from] serde_json::Error), +} diff --git a/rust/src/utils/mod.rs b/rust/src/utils/mod.rs new file mode 100644 index 00000000..a91e7351 --- /dev/null +++ b/rust/src/utils/mod.rs @@ -0,0 +1 @@ +pub mod error; diff --git a/scripts/update_build_log.py b/scripts/update_build_log.py new file mode 100644 index 00000000..a2542037 --- /dev/null +++ b/scripts/update_build_log.py @@ -0,0 +1,61 @@ +#!/usr/bin/env python3 +"""Record a nightly build in the website's build-log data file. + +Prepends an entry describing the current commit to +``website/data/nightly_builds.json`` (newest first) and keeps only the most +recent ``MAX_ENTRIES``. The Hugo ``nightly-builds`` shortcode renders this file. + +Run from the repository root (as the CI workflow does): + + python3 scripts/update_build_log.py + +All arguments are optional: + status "success" (default) or "failure" + artifact_url link to the built APK ("" if none) + build_number CI run number ("" if unknown) +""" + +import json +import subprocess +import sys +from datetime import datetime, timezone +from pathlib import Path + +LOG = Path("website/data/nightly_builds.json") +MAX_ENTRIES = 90 + + +def _git(*args: str) -> str: + return subprocess.check_output(["git", *args]).decode().strip() + + +def main(status: str = "success", artifact: str = "", build_number: str = "") -> None: + entry = { + "ts": datetime.now(timezone.utc).isoformat(timespec="seconds"), + "sha": _git("rev-parse", "HEAD")[:8], + "msg": _git("log", "-1", "--pretty=%s"), + "status": status, + "artifact": artifact, + "build": build_number, + } + + entries = [] + if LOG.exists(): + try: + entries = json.loads(LOG.read_text() or "[]") + except json.JSONDecodeError: + entries = [] + + entries.insert(0, entry) + entries = entries[:MAX_ENTRIES] + + LOG.parent.mkdir(parents=True, exist_ok=True) + LOG.write_text(json.dumps(entries, indent=2) + "\n") + print( + f"Recorded nightly build {entry['sha']} ({status}); " + f"{len(entries)} entr{'y' if len(entries) == 1 else 'ies'} in {LOG}." + ) + + +if __name__ == "__main__": + main(*sys.argv[1:]) diff --git a/test/api_service_test.dart b/test/api_service_test.dart index 85d84391..1429ccae 100644 --- a/test/api_service_test.dart +++ b/test/api_service_test.dart @@ -1,33 +1,17 @@ -import 'dart:convert'; - import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:mockito/annotations.dart'; -import 'package:mockito/mockito.dart'; -import 'package:http/http.dart' as http; import 'package:sqflite_common_ffi/sqflite_ffi.dart'; -import 'package:taskwarrior/app/utils/taskchampion/credentials_storage.dart'; import 'package:taskwarrior/app/v3/db/task_database.dart'; import 'package:taskwarrior/app/v3/models/task.dart'; -import 'package:taskwarrior/app/v3/net/fetch.dart'; -import 'package:taskwarrior/app/v3/net/origin.dart'; - -import 'api_service_test.mocks.dart'; - -class MockCredentialsStorage extends Mock implements CredentialsStorage {} - -class MockMethodChannel extends Mock implements MethodChannel {} -@GenerateMocks([MockMethodChannel, http.Client]) void main() { TestWidgetsFlutterBinding.ensureInitialized(); databaseFactory = databaseFactoryFfi; - MockClient mockClient = MockClient(); setUpAll(() { sqfliteFfiInit(); - + // Mock SharedPreferences plugin const MethodChannel('plugins.flutter.io/shared_preferences') .setMockMethodCallHandler((MethodCall methodCall) async { @@ -102,30 +86,6 @@ void main() { }); }); - group('fetchTasks', () { - test('Fetch data successfully', () async { - final responseJson = jsonEncode({'data': 'Mock data'}); - var baseUrl = await CredentialsStorage.getApiUrl(); - when(mockClient.get( - Uri.parse( - '$baseUrl/tasks?email=email&origin=$origin&UUID=123&encryptionSecret=secret'), - headers: { - "Content-Type": "application/json", - })).thenAnswer((_) async => http.Response(responseJson, 200)); - - final result = await fetchTasks('123', 'secret'); - - expect(result, isA>()); - }); - - test('fetchTasks returns empty array', () async { - const uuid = '123'; - const encryptionSecret = 'secret'; - - expect(await fetchTasks(uuid, encryptionSecret), isEmpty); - }); - }); - group('TaskDatabase', () { late TaskDatabase taskDatabase; diff --git a/test/api_service_test.mocks.dart b/test/api_service_test.mocks.dart deleted file mode 100644 index c23b5109..00000000 --- a/test/api_service_test.mocks.dart +++ /dev/null @@ -1,401 +0,0 @@ -// Mocks generated by Mockito 5.4.4 from annotations -// in taskwarrior/test/api_service_test.dart. -// Do not manually edit this file. - -// ignore_for_file: no_leading_underscores_for_library_prefixes -import 'dart:async' as _i6; -import 'dart:convert' as _i7; -import 'dart:typed_data' as _i8; - -import 'package:flutter/services.dart' as _i2; -import 'package:http/http.dart' as _i3; -import 'package:mockito/mockito.dart' as _i1; -import 'package:mockito/src/dummies.dart' as _i5; - -import 'api_service_test.dart' as _i4; - -// ignore_for_file: type=lint -// ignore_for_file: avoid_redundant_argument_values -// ignore_for_file: avoid_setters_without_getters -// ignore_for_file: comment_references -// ignore_for_file: deprecated_member_use -// ignore_for_file: deprecated_member_use_from_same_package -// ignore_for_file: implementation_imports -// ignore_for_file: invalid_use_of_visible_for_testing_member -// ignore_for_file: prefer_const_constructors -// ignore_for_file: unnecessary_parenthesis -// ignore_for_file: camel_case_types -// ignore_for_file: subtype_of_sealed_class - -class _FakeMethodCodec_0 extends _i1.SmartFake implements _i2.MethodCodec { - _FakeMethodCodec_0( - Object parent, - Invocation parentInvocation, - ) : super( - parent, - parentInvocation, - ); -} - -class _FakeBinaryMessenger_1 extends _i1.SmartFake - implements _i2.BinaryMessenger { - _FakeBinaryMessenger_1( - Object parent, - Invocation parentInvocation, - ) : super( - parent, - parentInvocation, - ); -} - -class _FakeResponse_2 extends _i1.SmartFake implements _i3.Response { - _FakeResponse_2( - Object parent, - Invocation parentInvocation, - ) : super( - parent, - parentInvocation, - ); -} - -class _FakeStreamedResponse_3 extends _i1.SmartFake - implements _i3.StreamedResponse { - _FakeStreamedResponse_3( - Object parent, - Invocation parentInvocation, - ) : super( - parent, - parentInvocation, - ); -} - -/// A class which mocks [MockMethodChannel]. -/// -/// See the documentation for Mockito's code generation for more information. -class MockMockMethodChannel extends _i1.Mock implements _i4.MockMethodChannel { - MockMockMethodChannel() { - _i1.throwOnMissingStub(this); - } - - @override - String get name => (super.noSuchMethod( - Invocation.getter(#name), - returnValue: _i5.dummyValue( - this, - Invocation.getter(#name), - ), - ) as String); - - @override - _i2.MethodCodec get codec => (super.noSuchMethod( - Invocation.getter(#codec), - returnValue: _FakeMethodCodec_0( - this, - Invocation.getter(#codec), - ), - ) as _i2.MethodCodec); - - @override - _i2.BinaryMessenger get binaryMessenger => (super.noSuchMethod( - Invocation.getter(#binaryMessenger), - returnValue: _FakeBinaryMessenger_1( - this, - Invocation.getter(#binaryMessenger), - ), - ) as _i2.BinaryMessenger); - - @override - _i6.Future invokeMethod( - String? method, [ - dynamic arguments, - ]) => - (super.noSuchMethod( - Invocation.method( - #invokeMethod, - [ - method, - arguments, - ], - ), - returnValue: _i6.Future.value(), - ) as _i6.Future); - - @override - _i6.Future?> invokeListMethod( - String? method, [ - dynamic arguments, - ]) => - (super.noSuchMethod( - Invocation.method( - #invokeListMethod, - [ - method, - arguments, - ], - ), - returnValue: _i6.Future?>.value(), - ) as _i6.Future?>); - - @override - _i6.Future?> invokeMapMethod( - String? method, [ - dynamic arguments, - ]) => - (super.noSuchMethod( - Invocation.method( - #invokeMapMethod, - [ - method, - arguments, - ], - ), - returnValue: _i6.Future?>.value(), - ) as _i6.Future?>); - - @override - void setMethodCallHandler( - _i6.Future Function(_i2.MethodCall)? handler) => - super.noSuchMethod( - Invocation.method( - #setMethodCallHandler, - [handler], - ), - returnValueForMissingStub: null, - ); -} - -/// A class which mocks [Client]. -/// -/// See the documentation for Mockito's code generation for more information. -class MockClient extends _i1.Mock implements _i3.Client { - MockClient() { - _i1.throwOnMissingStub(this); - } - - @override - _i6.Future<_i3.Response> head( - Uri? url, { - Map? headers, - }) => - (super.noSuchMethod( - Invocation.method( - #head, - [url], - {#headers: headers}, - ), - returnValue: _i6.Future<_i3.Response>.value(_FakeResponse_2( - this, - Invocation.method( - #head, - [url], - {#headers: headers}, - ), - )), - ) as _i6.Future<_i3.Response>); - - @override - _i6.Future<_i3.Response> get( - Uri? url, { - Map? headers, - }) => - (super.noSuchMethod( - Invocation.method( - #get, - [url], - {#headers: headers}, - ), - returnValue: _i6.Future<_i3.Response>.value(_FakeResponse_2( - this, - Invocation.method( - #get, - [url], - {#headers: headers}, - ), - )), - ) as _i6.Future<_i3.Response>); - - @override - _i6.Future<_i3.Response> post( - Uri? url, { - Map? headers, - Object? body, - _i7.Encoding? encoding, - }) => - (super.noSuchMethod( - Invocation.method( - #post, - [url], - { - #headers: headers, - #body: body, - #encoding: encoding, - }, - ), - returnValue: _i6.Future<_i3.Response>.value(_FakeResponse_2( - this, - Invocation.method( - #post, - [url], - { - #headers: headers, - #body: body, - #encoding: encoding, - }, - ), - )), - ) as _i6.Future<_i3.Response>); - - @override - _i6.Future<_i3.Response> put( - Uri? url, { - Map? headers, - Object? body, - _i7.Encoding? encoding, - }) => - (super.noSuchMethod( - Invocation.method( - #put, - [url], - { - #headers: headers, - #body: body, - #encoding: encoding, - }, - ), - returnValue: _i6.Future<_i3.Response>.value(_FakeResponse_2( - this, - Invocation.method( - #put, - [url], - { - #headers: headers, - #body: body, - #encoding: encoding, - }, - ), - )), - ) as _i6.Future<_i3.Response>); - - @override - _i6.Future<_i3.Response> patch( - Uri? url, { - Map? headers, - Object? body, - _i7.Encoding? encoding, - }) => - (super.noSuchMethod( - Invocation.method( - #patch, - [url], - { - #headers: headers, - #body: body, - #encoding: encoding, - }, - ), - returnValue: _i6.Future<_i3.Response>.value(_FakeResponse_2( - this, - Invocation.method( - #patch, - [url], - { - #headers: headers, - #body: body, - #encoding: encoding, - }, - ), - )), - ) as _i6.Future<_i3.Response>); - - @override - _i6.Future<_i3.Response> delete( - Uri? url, { - Map? headers, - Object? body, - _i7.Encoding? encoding, - }) => - (super.noSuchMethod( - Invocation.method( - #delete, - [url], - { - #headers: headers, - #body: body, - #encoding: encoding, - }, - ), - returnValue: _i6.Future<_i3.Response>.value(_FakeResponse_2( - this, - Invocation.method( - #delete, - [url], - { - #headers: headers, - #body: body, - #encoding: encoding, - }, - ), - )), - ) as _i6.Future<_i3.Response>); - - @override - _i6.Future read( - Uri? url, { - Map? headers, - }) => - (super.noSuchMethod( - Invocation.method( - #read, - [url], - {#headers: headers}, - ), - returnValue: _i6.Future.value(_i5.dummyValue( - this, - Invocation.method( - #read, - [url], - {#headers: headers}, - ), - )), - ) as _i6.Future); - - @override - _i6.Future<_i8.Uint8List> readBytes( - Uri? url, { - Map? headers, - }) => - (super.noSuchMethod( - Invocation.method( - #readBytes, - [url], - {#headers: headers}, - ), - returnValue: _i6.Future<_i8.Uint8List>.value(_i8.Uint8List(0)), - ) as _i6.Future<_i8.Uint8List>); - - @override - _i6.Future<_i3.StreamedResponse> send(_i3.BaseRequest? request) => - (super.noSuchMethod( - Invocation.method( - #send, - [request], - ), - returnValue: - _i6.Future<_i3.StreamedResponse>.value(_FakeStreamedResponse_3( - this, - Invocation.method( - #send, - [request], - ), - )), - ) as _i6.Future<_i3.StreamedResponse>); - - @override - void close() => super.noSuchMethod( - Invocation.method( - #close, - [], - ), - returnValueForMissingStub: null, - ); -} diff --git a/test/utils/language/bengali_sentences_test.dart b/test/utils/language/bengali_sentences_test.dart index 413c587c..8d8a429c 100644 --- a/test/utils/language/bengali_sentences_test.dart +++ b/test/utils/language/bengali_sentences_test.dart @@ -103,9 +103,9 @@ void main() { expect(bengali.reportsPageAddTasksToSeeReports, 'রিপোর্ট দেখতে টাস্ক যোগ করুন'); expect(bengali.taskchampionTileDescription, - 'Taskwarrior সিঙ্কিং CCSync বা Taskchampion সিঙ্ক সার্ভারে পরিবর্তন করুন'); + 'Taskwarrior সিঙ্কিং TaskChampion সিঙ্ক সার্ভারে পরিবর্তন করুন'); expect(bengali.taskchampionTileTitle, 'Taskchampion সিঙ্ক'); - expect(bengali.ccsyncCredentials, 'CCSync ক্রেডেনশিয়াল'); + expect(bengali.syncServerCredentials, 'TaskChampion ক্রেডেনশিয়াল'); expect(bengali.deleteTaskConfirmation, 'টাস্ক মুছুন'); expect(bengali.deleteTaskTitle, 'সব টাস্ক মুছুন?'); expect(bengali.deleteTaskWarning, diff --git a/test/utils/language/english_sentences_test.dart b/test/utils/language/english_sentences_test.dart index a2e14071..c898749b 100644 --- a/test/utils/language/english_sentences_test.dart +++ b/test/utils/language/english_sentences_test.dart @@ -101,9 +101,9 @@ void main() { expect(english.reportsPageNoTasksFound, 'No Tasks Found'); expect(english.reportsPageAddTasksToSeeReports, 'Add Tasks To See Reports'); expect(english.taskchampionTileDescription, - 'Switch to Taskwarrior sync with CCSync or Taskchampion Sync Server'); + 'Switch to Taskwarrior sync with a TaskChampion sync server'); expect(english.taskchampionTileTitle, 'Taskchampion sync'); - expect(english.ccsyncCredentials, 'CCync credentials'); + expect(english.syncServerCredentials, 'TaskChampion credentials'); expect(english.deleteTaskConfirmation, 'Delete Tasks'); expect(english.deleteTaskTitle, 'Delete All Tasks?'); expect(english.deleteTaskWarning, diff --git a/test/utils/language/french_sentences_test.dart b/test/utils/language/french_sentences_test.dart index 023647ac..51885971 100644 --- a/test/utils/language/french_sentences_test.dart +++ b/test/utils/language/french_sentences_test.dart @@ -107,9 +107,9 @@ void main() { expect(french.reportsPageAddTasksToSeeReports, 'Ajoutez des tâches pour voir les rapports'); expect(french.taskchampionTileDescription, - 'Basculez la synchronisation de Taskwarrior vers le serveur de synchronisation CCSync ou Taskchampion'); + 'Basculez la synchronisation de Taskwarrior vers le serveur de synchronisation TaskChampion'); expect(french.taskchampionTileTitle, 'Synchronisation Taskchampion'); - expect(french.ccsyncCredentials, 'Identifiants CCSync'); + expect(french.syncServerCredentials, 'Identifiants TaskChampion'); expect(french.deleteTaskConfirmation, 'Supprimer la tâche'); expect(french.deleteTaskTitle, 'Supprimer toutes les tâches ?'); expect(french.deleteTaskWarning, diff --git a/test/utils/language/hindi_sentences_test.dart b/test/utils/language/hindi_sentences_test.dart index 45c41702..e6ddd15b 100644 --- a/test/utils/language/hindi_sentences_test.dart +++ b/test/utils/language/hindi_sentences_test.dart @@ -104,9 +104,9 @@ void main() { expect(hindi.reportsPageAddTasksToSeeReports, 'रिपोर्ट देखने के लिए कार्य जोड़ें'); expect(hindi.taskchampionTileDescription, - 'CCSync या Taskchampion सिंक सर्वर के साथ Taskwarrior सिंक पर स्विच करें'); + 'TaskChampion सिंक सर्वर के साथ Taskwarrior सिंक पर स्विच करें'); expect(hindi.taskchampionTileTitle, 'Taskchampion सिंक'); - expect(hindi.ccsyncCredentials, 'CCync क्रेडेन्शियल'); + expect(hindi.syncServerCredentials, 'TaskChampion क्रेडेन्शियल'); expect(hindi.deleteTaskConfirmation, 'कार्य हटाएं'); expect(hindi.deleteTaskTitle, 'सभी कार्य हटाएं?'); expect(hindi.deleteTaskWarning, diff --git a/test/utils/language/marathi_sentences_test.dart b/test/utils/language/marathi_sentences_test.dart index 57e8104c..1e2c4f6e 100644 --- a/test/utils/language/marathi_sentences_test.dart +++ b/test/utils/language/marathi_sentences_test.dart @@ -103,9 +103,9 @@ void main() { expect( marathi.reportsPageAddTasksToSeeReports, 'अहवाल पाहण्यासाठी काम जोडा'); expect(marathi.taskchampionTileDescription, - 'CCSync किंवा Taskchampion Sync Server सह Taskwarrior सिंक वर स्विच करा'); + 'TaskChampion Sync Server सह Taskwarrior सिंक वर स्विच करा'); expect(marathi.taskchampionTileTitle, 'Taskchampion सिंक'); - expect(marathi.ccsyncCredentials, 'CCync क्रेडेन्शियल'); + expect(marathi.syncServerCredentials, 'TaskChampion क्रेडेन्शियल'); expect(marathi.deleteTaskConfirmation, 'कार्य हटवा'); expect(marathi.deleteTaskTitle, 'सर्व कार्य हटवायचे का?'); expect(marathi.deleteTaskWarning, diff --git a/test/utils/language/sentences_test.dart b/test/utils/language/sentences_test.dart index 7c073bdf..68e5655d 100644 --- a/test/utils/language/sentences_test.dart +++ b/test/utils/language/sentences_test.dart @@ -60,7 +60,7 @@ void main() { expect(sentences.navDrawerReports, isA()); expect(sentences.navDrawerAbout, isA()); expect(sentences.navDrawerSettings, isA()); - expect(sentences.ccsyncCredentials, isA()); + expect(sentences.syncServerCredentials, isA()); expect(sentences.deleteTaskTitle, isA()); expect(sentences.deleteTaskConfirmation, isA()); expect(sentences.deleteTaskWarning, isA()); diff --git a/test/utils/language/spanish_sentences_test.dart b/test/utils/language/spanish_sentences_test.dart index f51722c4..2b97a8b9 100644 --- a/test/utils/language/spanish_sentences_test.dart +++ b/test/utils/language/spanish_sentences_test.dart @@ -105,9 +105,9 @@ void main() { expect(spanish.reportsPageAddTasksToSeeReports, 'Agrega tareas para ver informes'); expect(spanish.taskchampionTileDescription, - 'Cambia la sincronización de Taskwarrior al servidor de sincronización CCSync o Taskchampion'); + 'Cambia la sincronización de Taskwarrior al servidor de sincronización TaskChampion'); expect(spanish.taskchampionTileTitle, 'Sincronización Taskchampion'); - expect(spanish.ccsyncCredentials, 'Credenciales de CCSync'); + expect(spanish.syncServerCredentials, 'Credenciales de TaskChampion'); expect(spanish.deleteTaskConfirmation, 'Eliminar tarea'); expect(spanish.deleteTaskTitle, '¿Eliminar todas las tareas?'); expect(spanish.deleteTaskWarning, diff --git a/test/utils/taskchampion/task_for_replica_urgency_test.dart b/test/utils/taskchampion/task_for_replica_urgency_test.dart new file mode 100644 index 00000000..59e9791d --- /dev/null +++ b/test/utils/taskchampion/task_for_replica_urgency_test.dart @@ -0,0 +1,99 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:taskwarrior/app/v3/champion/models/task_for_replica.dart'; +import 'package:taskwarrior/app/v3/models/annotation.dart'; + +/// Verifies [TaskForReplica.computeUrgency] against Taskwarrior's documented +/// default urgency coefficients. A fixed [now] makes the age/due/waiting terms +/// deterministic. +void main() { + final DateTime now = DateTime.utc(2024, 6, 1, 12); + int epoch(DateTime d) => d.millisecondsSinceEpoch ~/ 1000; + double u(TaskForReplica t) => t.computeUrgency(clock: now); + + group('TaskForReplica.computeUrgency', () { + test('an empty task has zero urgency', () { + expect(u(TaskForReplica(uuid: 'u')), 0.0); + }); + + test('priority H / M / L → 6.0 / 3.9 / 1.8', () { + expect(u(TaskForReplica(uuid: 'u', priority: 'H')), closeTo(6.0, 1e-9)); + expect(u(TaskForReplica(uuid: 'u', priority: 'M')), closeTo(3.9, 1e-9)); + expect(u(TaskForReplica(uuid: 'u', priority: 'L')), closeTo(1.8, 1e-9)); + }); + + test('belonging to a project adds 1.0', () { + expect(u(TaskForReplica(uuid: 'u', project: 'Home')), closeTo(1.0, 1e-9)); + }); + + test('an active (started) task adds 4.0', () { + expect(u(TaskForReplica(uuid: 'u', start: now.toIso8601String())), + closeTo(4.0, 1e-9)); + }); + + test('tag counts 1 / 2 / 3 → 0.8 / 0.9 / 1.0', () { + expect(u(TaskForReplica(uuid: 'u', tags: ['a'])), closeTo(0.8, 1e-9)); + expect(u(TaskForReplica(uuid: 'u', tags: ['a', 'b'])), closeTo(0.9, 1e-9)); + expect( + u(TaskForReplica(uuid: 'u', tags: ['a', 'b', 'c'])), closeTo(1.0, 1e-9)); + }); + + test('the "next" tag adds 15.0 on top of its tag-count term', () { + expect(u(TaskForReplica(uuid: 'u', tags: ['next'])), closeTo(15.8, 1e-9)); + }); + + test('annotation counts 1 / 2 / 3 → 0.8 / 0.9 / 1.0', () { + Annotation a(String d) => Annotation(description: d); + expect(u(TaskForReplica(uuid: 'u', annotations: [a('1')])), + closeTo(0.8, 1e-9)); + expect(u(TaskForReplica(uuid: 'u', annotations: [a('1'), a('2')])), + closeTo(0.9, 1e-9)); + expect(u(TaskForReplica(uuid: 'u', annotations: [a('1'), a('2'), a('3')])), + closeTo(1.0, 1e-9)); + }); + + test('blocking adds 8.0, blocked subtracts 5.0', () { + expect(u(TaskForReplica(uuid: 'u', isBlocking: true)), closeTo(8.0, 1e-9)); + expect(u(TaskForReplica(uuid: 'u', isBlocked: true)), closeTo(-5.0, 1e-9)); + }); + + test('a future wait date subtracts 3.0', () { + expect( + u(TaskForReplica( + uuid: 'u', + wait: now.add(const Duration(days: 1)).toIso8601String())), + closeTo(-3.0, 1e-9)); + }); + + test('due ≥7 days overdue → full 12.0', () { + final due = now.subtract(const Duration(days: 10)).toIso8601String(); + expect(u(TaskForReplica(uuid: 'u', due: due)), closeTo(12.0, 1e-9)); + }); + + test('due exactly now → 8.8', () { + expect(u(TaskForReplica(uuid: 'u', due: now.toIso8601String())), + closeTo(8.8, 1e-9)); + }); + + test('due more than 14 days out → 2.4', () { + final due = now.add(const Duration(days: 20)).toIso8601String(); + expect(u(TaskForReplica(uuid: 'u', due: due)), closeTo(2.4, 1e-9)); + }); + + test('entry 365 days ago → full age term 2.0', () { + final e = epoch(now.subtract(const Duration(days: 365))); + expect(u(TaskForReplica(uuid: 'u', entry: e)), closeTo(2.0, 1e-9)); + }); + + test('terms combine additively (H + project + 2 tags + overdue)', () { + final due = now.subtract(const Duration(days: 8)).toIso8601String(); + final task = TaskForReplica( + uuid: 'u', + priority: 'H', // 6.0 + project: 'X', // 1.0 + tags: ['a', 'b'], // 0.9 + due: due, // 12.0 + ); + expect(u(task), closeTo(19.9, 1e-9)); + }); + }); +} diff --git a/test/utils/taskchampion/taskchampion_test.dart b/test/utils/taskchampion/taskchampion_test.dart index 0e0fddb1..9dda50a8 100644 --- a/test/utils/taskchampion/taskchampion_test.dart +++ b/test/utils/taskchampion/taskchampion_test.dart @@ -20,7 +20,7 @@ void main() { expect(controller.encryptionSecretController.text, ''); expect(controller.clientIdController.text, ''); - expect(controller.ccsyncBackendUrlController.text, ''); + expect(controller.syncServerUrlController.text, ''); }); test('should load existing credentials', () async { @@ -34,13 +34,13 @@ void main() { await controller.loadCredentials(); expect(controller.encryptionSecretController.text, 'mysecret'); expect(controller.clientIdController.text, 'client123'); - expect(controller.ccsyncBackendUrlController.text, 'https://example.com'); + expect(controller.syncServerUrlController.text, 'https://example.com'); }); test('should save credentials', () async { controller.encryptionSecretController.text = 'secret123'; controller.clientIdController.text = 'clientABC'; - controller.ccsyncBackendUrlController.text = 'https://backend.url'; + controller.syncServerUrlController.text = 'https://backend.url'; await controller.saveCredentials(); diff --git a/website/.gitignore b/website/.gitignore new file mode 100644 index 00000000..454de3e4 --- /dev/null +++ b/website/.gitignore @@ -0,0 +1,4 @@ +# Hugo build output and caches +/public/ +/resources/ +.hugo_build.lock diff --git a/website/README.md b/website/README.md new file mode 100644 index 00000000..0240d6ba --- /dev/null +++ b/website/README.md @@ -0,0 +1,74 @@ +# TaskWarrior Mobile — Project Website + +A self-contained [Hugo](https://gohugo.io) static site for the TaskWarrior Mobile +app: landing page, downloads (with an auto-updated **nightly build log**), and a +documentation section. It has **no external theme** — all layouts live in +`layouts/`, so it builds from the single Hugo binary with no submodules or network +fetches. + +## Local development + +Requires Hugo **extended** (pinned to the version in +[`.github/workflows/deploy-website.yml`](../.github/workflows/deploy-website.yml)): + +```bash +# from this directory +hugo server # live-reload dev server at http://localhost:1313 +hugo --minify # production build into ./public +``` + +## Layout + +``` +website/ +├── hugo.toml # site config (baseURL, menus, params) +├── content/ # Markdown pages (_index, downloads, docs) +├── layouts/ # self-contained templates (no theme) +│ ├── _default/ # baseof / single / list +│ ├── index.html # homepage +│ └── shortcodes/ +│ └── nightly-builds.html # renders data/nightly_builds.json +├── data/ +│ └── nightly_builds.json # build log (populated by CI; ships empty) +└── static/ + ├── css/style.css + └── CNAME # custom domain +``` + +## Nightly build log + +[`scripts/update_build_log.py`](../scripts/update_build_log.py) prepends an entry +`{ts, sha, msg, status, artifact, build}` to `data/nightly_builds.json` (keeping the +90 most recent). The `{{}}` shortcode on the downloads page +renders it. The file ships as `[]`; CI fills it in. + +## CI + +- **`build-nightly.yml`** — daily at 02:00 UTC (and on demand): builds the signed + nightly APK, records the result via `update_build_log.py`, and commits the updated + log to `main`. +- **`deploy-website.yml`** — on any push touching `website/**` (including the nightly + log commit): builds the site with Hugo and deploys it to GitHub Pages. +- `nightlydepolyci.yml` now ignores `website/**` and `scripts/**` so a build-log + commit does not trigger a redundant APK rebuild. + +## One-time infrastructure setup (needs repo-admin / DNS access) + +These cannot be done from code and must be configured by a maintainer: + +1. **Enable GitHub Pages** → repo *Settings → Pages → Build and deployment → + Source: **GitHub Actions***. +2. **Custom domain / DNS** — point `taskwarrior.ccextractor.org` at GitHub Pages + with a `CNAME` DNS record to `.github.io`. The site already ships a + `static/CNAME`, so Pages will pick up the domain on first deploy. +3. **Branch protection** — if `main` is protected, allow `github-actions[bot]` to + push the nightly build-log commit (or point that commit at an unprotected branch). +4. The nightly signing secrets (`NIGHTLY_KEYSTORE_B64`, `NIGHTLY_PROPERTIES_B64`) are + the same ones the existing F-Droid workflow already uses. + +> **Note on the deploy target.** The proposal described renaming the `fdroid-repo` +> branch to `public-site` and serving from a branch. This implementation instead uses +> the modern **GitHub Actions Pages deployment** (`actions/deploy-pages`), which keeps +> the site source in `website/` on `main` and needs no dedicated deploy branch — fewer +> moving parts and no history juggling. The F-Droid APK repo on `fdroid-repo` is +> untouched. diff --git a/website/content/_index.md b/website/content/_index.md new file mode 100644 index 00000000..dd48717b --- /dev/null +++ b/website/content/_index.md @@ -0,0 +1,12 @@ +--- +title: "TaskWarrior Mobile" +--- + +**TaskWarrior Mobile** is the cross-platform [Flutter](https://flutter.dev) client for +[TaskWarrior](https://taskwarrior.org), maintained by [CCExtractor](https://ccextractor.org). +It brings TaskWarrior's fast, unobtrusive task management to Android, iOS, Windows, +macOS, and Linux, backed by [TaskChampion](https://github.com/GothenburgBitFactory/taskchampion) +for native, offline-first sync. + +New here? Head to the [downloads page](downloads/) to install the latest build, or browse +the [nightly build history](downloads/#nightly-builds) to see what has changed recently. diff --git a/website/content/docs/_index.md b/website/content/docs/_index.md new file mode 100644 index 00000000..d6268b0f --- /dev/null +++ b/website/content/docs/_index.md @@ -0,0 +1,18 @@ +--- +title: "Documentation" +description: "Developer and user documentation for TaskWarrior Mobile." +--- + +Documentation for TaskWarrior Mobile — a living set of guides that grows alongside the +app. New here? Start with [Getting started](getting-started/), then +[set up sync](sync-setup/) when you are ready to connect a server. Curious how it all +fits together? See the [architecture overview](architecture/). + +## Contributing + +Contributions are welcome. Start with the +[CONTRIBUTING guide](https://github.com/CCExtractor/taskwarrior-flutter/blob/main/CONTRIBUTING.md) +in the repository, and join the [CCExtractor community](https://ccextractor.org/) on Slack +or Zulip. + +## Guides diff --git a/website/content/docs/architecture.md b/website/content/docs/architecture.md new file mode 100644 index 00000000..394dc050 --- /dev/null +++ b/website/content/docs/architecture.md @@ -0,0 +1,42 @@ +--- +title: "Architecture" +description: "How TaskWarrior Mobile is built — Flutter UI over a native Rust core." +weight: 3 +--- + +TaskWarrior Mobile is a Flutter app sitting on top of a native Rust core, so the same +task engine that powers TaskWarrior on the desktop runs unchanged on your phone. + +## Layers + +- **UI — Flutter (Dart).** Screens, navigation, and state management use + [GetX](https://pub.dev/packages/get). This layer never talks to the network for task + data; it reads and writes through the core. +- **Core — Rust (`tc_helper`).** A thin wrapper around + [TaskChampion](https://github.com/GothenburgBitFactory/taskchampion) exposed to Dart + through [`flutter_rust_bridge`](https://github.com/fzyzcjy/flutter_rust_bridge). It + owns task storage, mutations, and sync. +- **Storage & sync — TaskChampion.** Tasks live in a local replica on the device. + Syncing reconciles that replica with a TaskChampion sync server; there is no + bespoke HTTP API in between. + +## Offline-first data flow + +``` +tap "complete" ─▶ Flutter ─▶ Rust FFI ─▶ local replica (instant, no network) + │ + (later, when online) + ▼ + TaskChampion sync server +``` + +Every create, edit, and complete is committed to the local replica synchronously, so the +UI never waits on the network. Synchronisation is a separate, best-effort step that runs +when connectivity is available. + +## Why a Rust core? + +Reusing TaskChampion means the mobile app shares TaskWarrior's battle-tested task model — +recurrence, dependencies, annotations, tags, and the sync protocol — instead of +reimplementing them. The Rust bridge surfaces those capabilities to the Flutter UI with a +small, typed FFI surface. diff --git a/website/content/docs/faq.md b/website/content/docs/faq.md new file mode 100644 index 00000000..c308c347 --- /dev/null +++ b/website/content/docs/faq.md @@ -0,0 +1,39 @@ +--- +title: "FAQ" +description: "Frequently asked questions about TaskWarrior Mobile." +weight: 4 +--- + +## Do I need a sync server to use the app? + +No. TaskWarrior Mobile is fully usable offline — tasks are stored in a local replica on +your device. A sync server is only needed if you want your tasks mirrored to another +device or backed up remotely. + +## Is my data encrypted? + +Yes. When you sync, your tasks are encrypted locally with your encryption secret before +being uploaded. The server stores only ciphertext; only clients that hold the secret can +read your tasks. + +## Which platforms are supported? + +The app targets Android, iOS, Windows, macOS, and Linux from a single Flutter codebase. +Android builds are published as APKs on the [downloads page](../../downloads/). + +## Does it work with my existing TaskWarrior setup? + +If your TaskWarrior 3.x desktop syncs through a TaskChampion sync server, point the app at +the same server, client ID, and encryption secret and your tasks will sync between them. +See [setting up sync](../sync-setup/). + +## What's the difference between stable and nightly builds? + +Stable builds are tagged releases meant for everyday use. Nightly builds are automatic +snapshots of the `main` branch — useful for trying the latest changes, but less tested. + +## Where do I report a bug or request a feature? + +Open an issue on the +[GitHub repository](https://github.com/CCExtractor/taskwarrior-flutter/issues). Pull +requests are welcome too — see the [contributing notes](../../docs/#contributing). diff --git a/website/content/docs/getting-started.md b/website/content/docs/getting-started.md new file mode 100644 index 00000000..cc9b29b9 --- /dev/null +++ b/website/content/docs/getting-started.md @@ -0,0 +1,31 @@ +--- +title: "Getting started" +description: "Install TaskWarrior Mobile and create your first task." +weight: 1 +--- + +## Install the app + +Grab a build from the [downloads page](../../downloads/): + +- **Stable** — the latest signed APK from the GitHub [releases page](https://github.com/CCExtractor/taskwarrior-flutter/releases). +- **Nightly** — an automatically-built snapshot from the `main` branch, for testing the newest changes. + +On Android you may need to allow installation from your browser or file manager the +first time you sideload an APK. + +## Create your first task + +1. Open the app — you land on the task list. +2. Tap the **+** button. +3. Give the task a description, and optionally a project, tags, priority, and due date. +4. Save. The task is written to the local database immediately — no network required. + +Everything you do works fully offline. Sync is optional and layered on top; set it up +whenever you want your tasks mirrored to a server or another device. + +## Next steps + +- [Set up sync](../sync-setup/) with a TaskChampion server. +- Learn how the app is [put together](../architecture/). +- Browse the [FAQ](../faq/) for common questions. diff --git a/website/content/docs/sync-setup.md b/website/content/docs/sync-setup.md new file mode 100644 index 00000000..535328b6 --- /dev/null +++ b/website/content/docs/sync-setup.md @@ -0,0 +1,40 @@ +--- +title: "Setting up sync" +description: "Connect TaskWarrior Mobile to a TaskChampion sync server." +weight: 2 +--- + +TaskWarrior Mobile syncs through a [TaskChampion](https://github.com/GothenburgBitFactory/taskchampion) +sync server — the same protocol used by TaskWarrior 3.x on the desktop. Sync is +**optional and offline-first**: your tasks always live in a local replica, and syncing +simply reconciles that replica with the server when you are online. + +## What you need + +Three values from your TaskChampion sync server: + +| Field | Description | +|---|---| +| **Sync server URL** | The base URL of the TaskChampion server, e.g. `https://example.org/taskchampion/`. | +| **Client ID** | A UUID identifying your replica on the server. | +| **Encryption secret** | The secret used to encrypt your data end-to-end. Only clients that share it can read your tasks. | + +The server never sees your task data in the clear — everything is encrypted locally with +your encryption secret before it is uploaded. + +## Connecting + +1. Open **Profile** → change the sync server → choose **Taskchampion**. +2. Tap **Configure** and paste the three values above. +3. Tap **Save**. The app performs a live sync against the server to confirm the + credentials work *before* it stores them — if anything is wrong, you find out + immediately instead of silently failing later. + +Once connected, changes flow both ways whenever you have connectivity. You can keep +working offline at any time; the next sync catches the server up. + +## Self-hosting a server + +Any TaskChampion-compatible sync server works. See the upstream +[TaskChampion sync-server](https://github.com/GothenburgBitFactory/taskchampion-sync-server) +project to run your own. diff --git a/website/content/downloads.md b/website/content/downloads.md new file mode 100644 index 00000000..5029cd30 --- /dev/null +++ b/website/content/downloads.md @@ -0,0 +1,20 @@ +--- +title: "Downloads" +description: "Install TaskWarrior Mobile — stable releases and automatically-built nightly APKs." +--- + +## Stable releases + +Tagged releases are published on GitHub. Download the latest signed APK from the +[releases page](https://github.com/CCExtractor/taskwarrior-flutter/releases). + +## Nightly builds {#nightly-builds} + +A signed nightly APK is built automatically from the `main` branch every night and +published to the project's F-Droid repository. Each entry below links to the APK produced +by that build, newest first. + +{{< nightly-builds limit="20" >}} + +_Nightly builds are unstable snapshots intended for testing. For everyday use, prefer a +stable release above._ diff --git a/website/content/features.md b/website/content/features.md new file mode 100644 index 00000000..6a0daf04 --- /dev/null +++ b/website/content/features.md @@ -0,0 +1,44 @@ +--- +title: "Features" +description: "What TaskWarrior Mobile can do — offline-first task management with native TaskChampion sync." +--- + +TaskWarrior Mobile brings the full TaskWarrior task model to your phone and desktop, +without giving up speed or working offline. + +## Task management + +- **Rich tasks** — descriptions, projects, tags, priorities, and due dates. +- **Recurring tasks** — repeat tasks on a schedule. +- **Dependencies** — mark tasks as blocking or blocked by others so you always know + what is actionable. +- **Annotations** — attach timestamped notes to any task. +- **Fast capture and edit** — add or change a task in a couple of taps. + +## Offline-first + +- Every create, edit, and complete is written to a **local replica instantly** — no + spinner, no waiting on the network. +- The app is fully usable with no connectivity at all; sync is optional. + +## Native TaskChampion sync + +- Syncs through a [TaskChampion](https://github.com/GothenburgBitFactory/taskchampion) + sync server — the same protocol used by TaskWarrior 3.x on the desktop. +- **End-to-end encrypted**: your tasks are encrypted locally before upload, so the server + only ever stores ciphertext. +- Credentials are **validated against the server before they are saved**, so a bad URL or + secret is caught immediately. + +## Cross-platform + +- One Flutter codebase targeting **Android, iOS, Windows, macOS, and Linux**. +- A shared native Rust core keeps behaviour consistent everywhere. + +## Open source + +- Developed in the open on [GitHub](https://github.com/CCExtractor/taskwarrior-flutter) + and maintained by [CCExtractor](https://ccextractor.org/). +- Automated [nightly builds](../downloads/#nightly-builds) let you try the latest changes. + +Ready to try it? Head to the [downloads page](../downloads/). diff --git a/website/data/nightly_builds.json b/website/data/nightly_builds.json new file mode 100644 index 00000000..fe51488c --- /dev/null +++ b/website/data/nightly_builds.json @@ -0,0 +1 @@ +[] diff --git a/website/hugo.toml b/website/hugo.toml new file mode 100644 index 00000000..ca8ebeb3 --- /dev/null +++ b/website/hugo.toml @@ -0,0 +1,34 @@ +baseURL = "https://taskwarrior.ccextractor.org/" +locale = "en-us" +title = "TaskWarrior Mobile" +enableGitInfo = true + +# Self-contained site: layouts live in ./layouts, so no external theme (and thus +# no submodule / network fetch) is required — Hugo builds it from its single +# binary alone. + +[params] + description = "The cross-platform TaskWarrior mobile app by CCExtractor — offline-first task management with native TaskChampion sync." + repo = "https://github.com/CCExtractor/taskwarrior-flutter" + org = "https://ccextractor.org/" + +[markup.goldmark.renderer] + unsafe = true + +[menu] + [[menu.main]] + name = "Home" + url = "/" + weight = 1 + [[menu.main]] + name = "Features" + url = "/features/" + weight = 2 + [[menu.main]] + name = "Downloads" + url = "/downloads/" + weight = 3 + [[menu.main]] + name = "Docs" + url = "/docs/" + weight = 4 diff --git a/website/layouts/_default/baseof.html b/website/layouts/_default/baseof.html new file mode 100644 index 00000000..0f314d08 --- /dev/null +++ b/website/layouts/_default/baseof.html @@ -0,0 +1,29 @@ + + + + + + {{ if .IsHome }}{{ .Site.Title }}{{ else }}{{ .Title }} · {{ .Site.Title }}{{ end }} + + + + + + +
+ {{ block "main" . }}{{ end }} +
+ +
+

{{ .Site.Title }} · maintained by CCExtractor · built with Hugo

+
+ + diff --git a/website/layouts/_default/list.html b/website/layouts/_default/list.html new file mode 100644 index 00000000..8932309c --- /dev/null +++ b/website/layouts/_default/list.html @@ -0,0 +1,13 @@ +{{ define "main" }} +
+

{{ .Title }}

+ {{ .Content }} + {{ with .Pages }} + + {{ end }} +
+{{ end }} diff --git a/website/layouts/_default/single.html b/website/layouts/_default/single.html new file mode 100644 index 00000000..2eee85e2 --- /dev/null +++ b/website/layouts/_default/single.html @@ -0,0 +1,6 @@ +{{ define "main" }} +
+

{{ .Title }}

+ {{ .Content }} +
+{{ end }} diff --git a/website/layouts/index.html b/website/layouts/index.html new file mode 100644 index 00000000..92dd3063 --- /dev/null +++ b/website/layouts/index.html @@ -0,0 +1,45 @@ +{{ define "main" }} +
+

Your tasks, everywhere.

+

{{ .Site.Params.description }}

+

+ Get the app + View source +

+
+ +
+
+

Offline-first

+

Every create, edit, and complete is written to a local database instantly — no spinner, no network wait.

+
+
+

Native sync

+

Synchronise with a TaskChampion sync server through the native Rust bridge whenever you are online.

+
+
+

Cross-platform

+

One app across Android, iOS, Windows, macOS, and Linux, built with Flutter.

+
+
+

End-to-end encrypted

+

Your tasks are encrypted on-device before they sync — the server only ever stores ciphertext it cannot read.

+
+
+

Full task model

+

Projects, tags, priorities, due dates, recurrence, dependencies, and annotations — the complete TaskWarrior model.

+
+
+

Open source

+

Developed in the open on GitHub and maintained by CCExtractor, with automated nightly builds.

+
+
+ +
+ Explore all features → +
+ +
+ {{ .Content }} +
+{{ end }} diff --git a/website/layouts/shortcodes/nightly-builds.html b/website/layouts/shortcodes/nightly-builds.html new file mode 100644 index 00000000..3788f4ea --- /dev/null +++ b/website/layouts/shortcodes/nightly-builds.html @@ -0,0 +1,37 @@ +{{/* + nightly-builds shortcode — renders the build log collected by + scripts/update_build_log.py into website/data/nightly_builds.json. + Usage in content: {{< nightly-builds >}} (optionally {{< nightly-builds limit="15" >}}) +*/}} +{{ $builds := hugo.Data.nightly_builds }} +{{ $limit := int (.Get "limit" | default "20") }} +
+ {{ if $builds }} +
+ + + + + + + + + + + + {{ range first $limit $builds }} + + + + + + + + {{ end }} + +
Date (UTC)CommitMessageStatusAPK
{{ .ts }}{{ .sha }}{{ .msg }}{{ .status }}{{ with .artifact }}download{{ else }}—{{ end }}
+
+ {{ else }} +

No nightly builds have been recorded yet. Check back after the next scheduled build.

+ {{ end }} +
diff --git a/website/static/CNAME b/website/static/CNAME new file mode 100644 index 00000000..f71f76ef --- /dev/null +++ b/website/static/CNAME @@ -0,0 +1 @@ +taskwarrior.ccextractor.org diff --git a/website/static/css/style.css b/website/static/css/style.css new file mode 100644 index 00000000..37f1d885 --- /dev/null +++ b/website/static/css/style.css @@ -0,0 +1,165 @@ +:root { + --bg: #0f1419; + --surface: #171d26; + --text: #e6e9ee; + --muted: #9aa4b2; + --accent: #7c5cff; + --accent-2: #4dd0e1; + --border: #263041; + --ok: #3fb950; + --fail: #f85149; + --radius: 12px; + --maxw: 900px; +} + +* { box-sizing: border-box; } + +html { -webkit-text-size-adjust: 100%; } + +body { + margin: 0; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; + background: var(--bg); + color: var(--text); + line-height: 1.6; +} + +a { color: var(--accent-2); text-decoration: none; } +a:hover { text-decoration: underline; } + +code { + font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; + background: var(--surface); + border: 1px solid var(--border); + border-radius: 6px; + padding: 0.1em 0.4em; + font-size: 0.9em; +} + +/* Header */ +.site-header { + display: flex; + flex-wrap: wrap; + align-items: center; + justify-content: space-between; + gap: 1rem; + padding: 1rem 1.5rem; + border-bottom: 1px solid var(--border); + background: var(--surface); +} +.brand { font-weight: 700; font-size: 1.15rem; color: var(--text); } +.brand span { color: var(--accent); } +.site-header nav { display: flex; flex-wrap: wrap; gap: 1.1rem; } +.site-header nav a { color: var(--muted); font-weight: 500; } +.site-header nav a:hover { color: var(--text); text-decoration: none; } + +/* Layout */ +.content { max-width: var(--maxw); margin: 0 auto; padding: 2.5rem 1.5rem 4rem; } + +/* Hero */ +.hero { text-align: center; padding: 2rem 0 1rem; } +.hero h1 { font-size: clamp(2rem, 6vw, 3rem); margin: 0 0 0.5rem; } +.tagline { color: var(--muted); font-size: 1.15rem; max-width: 640px; margin: 0 auto 1.5rem; } +.cta { display: flex; gap: 0.75rem; justify-content: center; flex-wrap: wrap; } + +.button { + display: inline-block; + background: var(--accent); + color: #fff; + padding: 0.7rem 1.4rem; + border-radius: var(--radius); + font-weight: 600; +} +.button:hover { filter: brightness(1.1); text-decoration: none; } +.button-ghost { background: transparent; color: var(--text); border: 1px solid var(--border); } + +/* Features */ +.features { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); + gap: 1rem; + margin: 2.5rem 0; +} +.feature { + background: var(--surface); + border: 1px solid var(--border); + border-radius: var(--radius); + padding: 1.25rem; +} +.feature h3 { margin: 0 0 0.4rem; color: var(--accent-2); } +.feature p { margin: 0; color: var(--muted); } + +/* Pages */ +.page h1 { border-bottom: 1px solid var(--border); padding-bottom: 0.5rem; } +.page h2 { margin-top: 2rem; } +.page-list { padding-left: 1.2rem; } + +/* Content tables (markdown) */ +.page table { + width: 100%; + border-collapse: collapse; + margin: 1.25rem 0; + font-size: 0.95rem; +} +.page th, .page td { + text-align: left; + padding: 0.55rem 0.7rem; + border-bottom: 1px solid var(--border); + vertical-align: top; +} +.page th { color: var(--muted); font-weight: 600; } +.page thead tr { border-bottom: 2px solid var(--border); } + +/* Code blocks */ +.page pre { + background: var(--surface); + border: 1px solid var(--border); + border-radius: var(--radius); + padding: 1rem; + overflow-x: auto; +} +.page pre code { background: none; border: none; padding: 0; } +.page blockquote { + margin: 1.25rem 0; + padding: 0.25rem 1rem; + border-left: 3px solid var(--accent); + color: var(--muted); +} + +/* Nightly build table */ +.table-scroll { overflow-x: auto; } +.nightly-builds table { + width: 100%; + border-collapse: collapse; + margin: 1rem 0; + font-size: 0.92rem; +} +.nightly-builds th, .nightly-builds td { + text-align: left; + padding: 0.55rem 0.7rem; + border-bottom: 1px solid var(--border); + vertical-align: top; +} +.nightly-builds th { color: var(--muted); font-weight: 600; } +.nowrap { white-space: nowrap; } +.empty { color: var(--muted); font-style: italic; } + +.status { + display: inline-block; + padding: 0.1em 0.6em; + border-radius: 999px; + font-size: 0.8em; + font-weight: 600; + text-transform: capitalize; +} +.status-success { background: rgba(63,185,80,0.15); color: var(--ok); } +.status-failure { background: rgba(248,81,73,0.15); color: var(--fail); } + +/* Footer */ +.site-footer { + border-top: 1px solid var(--border); + padding: 1.5rem; + text-align: center; + color: var(--muted); + font-size: 0.9rem; +}