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/models/report.dart b/lib/app/models/report.dart
new file mode 100644
index 00000000..b3ca74d0
--- /dev/null
+++ b/lib/app/models/report.dart
@@ -0,0 +1,88 @@
+/// Data model for the reporting engine (Issue #418).
+///
+/// A *report* is a named, purpose-built bundle of a filter expression, a sort
+/// order, and a set of display columns — mirroring Taskwarrior's report system
+/// (`report..filter` / `.sort` / `.columns` / `.description`). Reports let
+/// users invoke preset views like "next", "ready", or "overdue" instead of
+/// hand-building a filter/sort every time.
+library;
+
+/// A single column in a report's display (e.g. `description`, `due`).
+class ColumnSpec {
+ final String field;
+ final String? label;
+
+ const ColumnSpec(this.field, {this.label});
+
+ /// Parses a Taskwarrior `report.*.columns` value such as
+ /// `"id,description,due"` into a list of columns. Taskwarrior column formats
+ /// (e.g. `due.relative`) are reduced to their base attribute.
+ static List parseList(String value) {
+ return value
+ .split(',')
+ .map((c) => c.trim())
+ .where((c) => c.isNotEmpty)
+ .map((c) => ColumnSpec(c.split('.').first))
+ .toList();
+ }
+}
+
+/// One sort key: an attribute plus a direction. Taskwarrior encodes these as
+/// `field+` (ascending) or `field-` (descending), optionally chained with
+/// commas (e.g. `"urgency-,due+"`).
+class SortCriterion {
+ final String field;
+ final bool ascending;
+
+ const SortCriterion(this.field, {this.ascending = true});
+
+ /// Parses a Taskwarrior `report.*.sort` value into an ordered list of keys.
+ /// A trailing `+`/`-` sets direction (default ascending). A `/` break marker
+ /// (e.g. `urgency-/`) is ignored — only the ordering matters here.
+ static List parseList(String value) {
+ return value
+ .split(',')
+ .map((s) => s.trim().replaceAll('/', ''))
+ .where((s) => s.isNotEmpty)
+ .map((s) {
+ if (s.endsWith('-')) {
+ return SortCriterion(s.substring(0, s.length - 1), ascending: false);
+ }
+ if (s.endsWith('+')) {
+ return SortCriterion(s.substring(0, s.length - 1), ascending: true);
+ }
+ return SortCriterion(s); // no direction → ascending
+ }).toList();
+ }
+}
+
+/// A complete report definition.
+class ReportDefinition {
+ /// Short identifier, e.g. `next` — also the display title.
+ final String name;
+
+ /// Human-readable one-line summary shown under the name.
+ final String description;
+
+ /// Columns to display (advisory for the UI; may be empty).
+ final List columns;
+
+ /// Ordered sort keys applied after filtering.
+ final List sortCriteria;
+
+ /// The filter expression (e.g. `"status:pending +READY"`), or null/empty for
+ /// "everything".
+ final String? filterExpression;
+
+ /// True for reports read from a user `.taskrc` (grouped above the defaults).
+ final bool isCustom;
+
+ const ReportDefinition({
+ required this.name,
+ required this.description,
+ this.columns = const [],
+ this.sortCriteria = const [],
+ this.filterExpression,
+ this.isCustom = false,
+ });
+}
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