From ae88d0f42a50dccfb29a127cccfefd31d5e80a72 Mon Sep 17 00:00:00 2001 From: Nikolai Giman Date: Sat, 11 Jul 2026 10:49:38 +0200 Subject: [PATCH] feat: mobile widgets --- .../capacitor-widget-bridge/.gitignore | 6 + .../capacitor-widget-bridge/Package.swift | 24 ++ .../android/build.gradle | 51 ++++ .../android/src/main/AndroidManifest.xml | 2 + .../widgetbridge/WidgetBridgePlugin.java | 48 ++++ .../WidgetBridgePlugin.swift | 44 +++ .../capacitor-widget-bridge/package.json | 41 +++ .../src/definitions.ts | 32 +++ .../capacitor-widget-bridge/src/index.ts | 12 + .../capacitor-widget-bridge/tsconfig.json | 24 ++ web/android/app/build.gradle | 4 +- web/android/app/capacitor.build.gradle | 1 + web/android/app/src/main/AndroidManifest.xml | 18 ++ .../app/widget/TodayWidgetFactory.java | 141 ++++++++++ .../app/widget/TodayWidgetProvider.java | 90 +++++++ .../app/widget/TodayWidgetService.java | 12 + .../src/main/res/drawable/widget_badge_bg.xml | 6 + .../app/src/main/res/drawable/widget_bg.xml | 6 + .../res/drawable/widget_checkbox_high.xml | 8 + .../main/res/drawable/widget_checkbox_low.xml | 8 + .../res/drawable/widget_checkbox_medium.xml | 8 + .../main/res/drawable/widget_header_bg.xml | 8 + .../app/src/main/res/layout/widget_today.xml | 76 ++++++ .../src/main/res/layout/widget_today_item.xml | 39 +++ .../main/res/values-night/widget_colors.xml | 8 + .../src/main/res/values-ru/strings_widget.xml | 8 + .../src/main/res/values/strings_widget.xml | 8 + .../app/src/main/res/values/widget_colors.xml | 13 + .../src/main/res/xml/widget_today_info.xml | 14 + web/android/capacitor.settings.gradle | 21 +- web/capacitor.config.ts | 3 + web/ios/App/App.xcodeproj/project.pbxproj | 216 ++++++++++++++- web/ios/App/App/App.entitlements | 4 + web/ios/App/CapApp-SPM/Package.swift | 20 +- .../Assets.xcassets/Contents.json | 6 + .../WidgetLogo.imageset/Contents.json | 12 + .../WidgetLogo.imageset/logo.png | Bin 0 -> 10700 bytes .../App/TaskViewWidget/OpenTaskIntent.swift | 39 +++ .../TaskViewWidget.entitlements | 10 + .../TaskViewWidget/TaskViewWidgetBundle.swift | 9 + web/ios/App/TaskViewWidget/TodayWidget.swift | 251 ++++++++++++++++++ .../TaskViewWidget/WidgetSnapshotModel.swift | 94 +++++++ web/package.json | 5 +- web/src/composables/useLogout.ts | 7 + web/src/composables/useUpdater.ts | 8 +- web/src/composables/useWidgetDeepLink.ts | 67 +++++ web/src/composables/useWidgetSnapshot.ts | 74 ++++++ web/src/layouts/UserLayout.vue | 5 + 48 files changed, 1581 insertions(+), 30 deletions(-) create mode 100644 taskview-packages/capacitor-widget-bridge/.gitignore create mode 100644 taskview-packages/capacitor-widget-bridge/Package.swift create mode 100644 taskview-packages/capacitor-widget-bridge/android/build.gradle create mode 100644 taskview-packages/capacitor-widget-bridge/android/src/main/AndroidManifest.xml create mode 100644 taskview-packages/capacitor-widget-bridge/android/src/main/java/tech/taskview/plugins/widgetbridge/WidgetBridgePlugin.java create mode 100644 taskview-packages/capacitor-widget-bridge/ios/Sources/WidgetBridgePlugin/WidgetBridgePlugin.swift create mode 100644 taskview-packages/capacitor-widget-bridge/package.json create mode 100644 taskview-packages/capacitor-widget-bridge/src/definitions.ts create mode 100644 taskview-packages/capacitor-widget-bridge/src/index.ts create mode 100644 taskview-packages/capacitor-widget-bridge/tsconfig.json create mode 100644 web/android/app/src/main/java/com/handscream/taskview/app/widget/TodayWidgetFactory.java create mode 100644 web/android/app/src/main/java/com/handscream/taskview/app/widget/TodayWidgetProvider.java create mode 100644 web/android/app/src/main/java/com/handscream/taskview/app/widget/TodayWidgetService.java create mode 100644 web/android/app/src/main/res/drawable/widget_badge_bg.xml create mode 100644 web/android/app/src/main/res/drawable/widget_bg.xml create mode 100644 web/android/app/src/main/res/drawable/widget_checkbox_high.xml create mode 100644 web/android/app/src/main/res/drawable/widget_checkbox_low.xml create mode 100644 web/android/app/src/main/res/drawable/widget_checkbox_medium.xml create mode 100644 web/android/app/src/main/res/drawable/widget_header_bg.xml create mode 100644 web/android/app/src/main/res/layout/widget_today.xml create mode 100644 web/android/app/src/main/res/layout/widget_today_item.xml create mode 100644 web/android/app/src/main/res/values-night/widget_colors.xml create mode 100644 web/android/app/src/main/res/values-ru/strings_widget.xml create mode 100644 web/android/app/src/main/res/values/strings_widget.xml create mode 100644 web/android/app/src/main/res/values/widget_colors.xml create mode 100644 web/android/app/src/main/res/xml/widget_today_info.xml create mode 100644 web/ios/App/TaskViewWidget/Assets.xcassets/Contents.json create mode 100644 web/ios/App/TaskViewWidget/Assets.xcassets/WidgetLogo.imageset/Contents.json create mode 100644 web/ios/App/TaskViewWidget/Assets.xcassets/WidgetLogo.imageset/logo.png create mode 100644 web/ios/App/TaskViewWidget/OpenTaskIntent.swift create mode 100644 web/ios/App/TaskViewWidget/TaskViewWidget.entitlements create mode 100644 web/ios/App/TaskViewWidget/TaskViewWidgetBundle.swift create mode 100644 web/ios/App/TaskViewWidget/TodayWidget.swift create mode 100644 web/ios/App/TaskViewWidget/WidgetSnapshotModel.swift create mode 100644 web/src/composables/useWidgetDeepLink.ts create mode 100644 web/src/composables/useWidgetSnapshot.ts diff --git a/taskview-packages/capacitor-widget-bridge/.gitignore b/taskview-packages/capacitor-widget-bridge/.gitignore new file mode 100644 index 0000000..259d2b7 --- /dev/null +++ b/taskview-packages/capacitor-widget-bridge/.gitignore @@ -0,0 +1,6 @@ +android/build/ +android/.gradle/ +android/local.properties +.swiftpm/ +ios/.build/ +DerivedData/ diff --git a/taskview-packages/capacitor-widget-bridge/Package.swift b/taskview-packages/capacitor-widget-bridge/Package.swift new file mode 100644 index 0000000..1b1d17b --- /dev/null +++ b/taskview-packages/capacitor-widget-bridge/Package.swift @@ -0,0 +1,24 @@ +// swift-tools-version: 5.9 +import PackageDescription + +let package = Package( + name: "CapacitorWidgetBridge", + platforms: [.iOS(.v15)], + products: [ + .library( + name: "CapacitorWidgetBridge", + targets: ["WidgetBridgePlugin"]) + ], + dependencies: [ + .package(url: "https://github.com/ionic-team/capacitor-swift-pm.git", from: "8.0.0") + ], + targets: [ + .target( + name: "WidgetBridgePlugin", + dependencies: [ + .product(name: "Capacitor", package: "capacitor-swift-pm"), + .product(name: "Cordova", package: "capacitor-swift-pm") + ], + path: "ios/Sources/WidgetBridgePlugin") + ] +) diff --git a/taskview-packages/capacitor-widget-bridge/android/build.gradle b/taskview-packages/capacitor-widget-bridge/android/build.gradle new file mode 100644 index 0000000..aa19f4b --- /dev/null +++ b/taskview-packages/capacitor-widget-bridge/android/build.gradle @@ -0,0 +1,51 @@ +ext { + junitVersion = project.hasProperty('junitVersion') ? rootProject.ext.junitVersion : '4.13.2' + androidxAppCompatVersion = project.hasProperty('androidxAppCompatVersion') ? rootProject.ext.androidxAppCompatVersion : '1.7.1' +} + +buildscript { + repositories { + google() + mavenCentral() + } + dependencies { + classpath 'com.android.tools.build:gradle:8.13.0' + } +} + +apply plugin: 'com.android.library' + +android { + namespace = "tech.taskview.plugins.widgetbridge" + compileSdk = project.hasProperty('compileSdkVersion') ? rootProject.ext.compileSdkVersion : 36 + defaultConfig { + minSdkVersion project.hasProperty('minSdkVersion') ? rootProject.ext.minSdkVersion : 24 + targetSdkVersion project.hasProperty('targetSdkVersion') ? rootProject.ext.targetSdkVersion : 36 + versionCode 1 + versionName "1.0" + } + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + lintOptions { + abortOnError = false + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_21 + targetCompatibility JavaVersion.VERSION_21 + } +} + +repositories { + google() + mavenCentral() +} + +dependencies { + implementation fileTree(dir: 'libs', include: ['*.jar']) + implementation project(':capacitor-android') + implementation "androidx.appcompat:appcompat:$androidxAppCompatVersion" +} diff --git a/taskview-packages/capacitor-widget-bridge/android/src/main/AndroidManifest.xml b/taskview-packages/capacitor-widget-bridge/android/src/main/AndroidManifest.xml new file mode 100644 index 0000000..b2d3ea1 --- /dev/null +++ b/taskview-packages/capacitor-widget-bridge/android/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + diff --git a/taskview-packages/capacitor-widget-bridge/android/src/main/java/tech/taskview/plugins/widgetbridge/WidgetBridgePlugin.java b/taskview-packages/capacitor-widget-bridge/android/src/main/java/tech/taskview/plugins/widgetbridge/WidgetBridgePlugin.java new file mode 100644 index 0000000..72e4580 --- /dev/null +++ b/taskview-packages/capacitor-widget-bridge/android/src/main/java/tech/taskview/plugins/widgetbridge/WidgetBridgePlugin.java @@ -0,0 +1,48 @@ +package tech.taskview.plugins.widgetbridge; + +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; + +import com.getcapacitor.Plugin; +import com.getcapacitor.PluginCall; +import com.getcapacitor.PluginMethod; +import com.getcapacitor.annotation.CapacitorPlugin; + +@CapacitorPlugin(name = "WidgetBridge") +public class WidgetBridgePlugin extends Plugin { + + public static final String PREFS_NAME = "taskview_widget"; + public static final String SNAPSHOT_KEY = "widgetSnapshot"; + public static final String ACTION_WIDGET_UPDATE = "tech.taskview.widget.UPDATE"; + + @PluginMethod + public void setSnapshot(PluginCall call) { + String snapshot = call.getString("snapshot"); + if (snapshot == null) { + call.reject("snapshot is required"); + return; + } + prefs().edit().putString(SNAPSHOT_KEY, snapshot).apply(); + notifyWidgets(); + call.resolve(); + } + + @PluginMethod + public void clearSnapshot(PluginCall call) { + prefs().edit().remove(SNAPSHOT_KEY).apply(); + notifyWidgets(); + call.resolve(); + } + + private SharedPreferences prefs() { + return getContext().getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE); + } + + private void notifyWidgets() { + Context context = getContext(); + Intent intent = new Intent(ACTION_WIDGET_UPDATE); + intent.setPackage(context.getPackageName()); + context.sendBroadcast(intent); + } +} diff --git a/taskview-packages/capacitor-widget-bridge/ios/Sources/WidgetBridgePlugin/WidgetBridgePlugin.swift b/taskview-packages/capacitor-widget-bridge/ios/Sources/WidgetBridgePlugin/WidgetBridgePlugin.swift new file mode 100644 index 0000000..5f7c60b --- /dev/null +++ b/taskview-packages/capacitor-widget-bridge/ios/Sources/WidgetBridgePlugin/WidgetBridgePlugin.swift @@ -0,0 +1,44 @@ +import Foundation +import Capacitor +import WidgetKit + +@objc(WidgetBridgePlugin) +public class WidgetBridgePlugin: CAPPlugin, CAPBridgedPlugin { + public let identifier = "WidgetBridgePlugin" + public let jsName = "WidgetBridge" + public let pluginMethods: [CAPPluginMethod] = [ + CAPPluginMethod(name: "setSnapshot", returnType: CAPPluginReturnPromise), + CAPPluginMethod(name: "clearSnapshot", returnType: CAPPluginReturnPromise) + ] + + static let snapshotKey = "widgetSnapshot" + + private var sharedDefaults: UserDefaults? { + guard let appGroup = getConfig().getString("appGroup") else { return nil } + return UserDefaults(suiteName: appGroup) + } + + @objc func setSnapshot(_ call: CAPPluginCall) { + guard let snapshot = call.getString("snapshot") else { + call.reject("snapshot is required") + return + } + guard let defaults = sharedDefaults else { + call.reject("WidgetBridge appGroup is not configured in capacitor.config") + return + } + defaults.set(snapshot, forKey: Self.snapshotKey) + WidgetCenter.shared.reloadAllTimelines() + call.resolve() + } + + @objc func clearSnapshot(_ call: CAPPluginCall) { + guard let defaults = sharedDefaults else { + call.reject("WidgetBridge appGroup is not configured in capacitor.config") + return + } + defaults.removeObject(forKey: Self.snapshotKey) + WidgetCenter.shared.reloadAllTimelines() + call.resolve() + } +} diff --git a/taskview-packages/capacitor-widget-bridge/package.json b/taskview-packages/capacitor-widget-bridge/package.json new file mode 100644 index 0000000..fcb2e26 --- /dev/null +++ b/taskview-packages/capacitor-widget-bridge/package.json @@ -0,0 +1,41 @@ +{ + "name": "capacitor-widget-bridge", + "private": false, + "version": "0.1.0", + "type": "module", + "description": "Capacitor bridge that shares a data snapshot with native home-screen widgets (iOS WidgetKit / Android App Widgets)", + "main": "./dist/index.js", + "module": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + } + }, + "files": [ + "dist", + "ios", + "android", + "Package.swift" + ], + "scripts": { + "build": "tsc", + "type-check": "tsc --noEmit" + }, + "capacitor": { + "ios": { + "src": "ios" + }, + "android": { + "src": "android" + } + }, + "peerDependencies": { + "@capacitor/core": "^8.0.0" + }, + "devDependencies": { + "@capacitor/core": "^8.1.0", + "typescript": "~5.8.3" + } +} diff --git a/taskview-packages/capacitor-widget-bridge/src/definitions.ts b/taskview-packages/capacitor-widget-bridge/src/definitions.ts new file mode 100644 index 0000000..8d23965 --- /dev/null +++ b/taskview-packages/capacitor-widget-bridge/src/definitions.ts @@ -0,0 +1,32 @@ +export type WidgetSnapshotTask = { + id: number + title: string + priority: 1 | 2 | 3 + overdue: boolean + endTime: string | null + endDate: string | null + path: string +} + +export type WidgetSnapshotMode = 'today' | 'upcoming' + +export type WidgetSnapshot = { + v: 3 + generatedAt: string + locale: string + orgSlug: string | null + mode: WidgetSnapshotMode + todayCount: number + overdueCount: number + upcomingCount: number + tasks: WidgetSnapshotTask[] +} + +export type SetSnapshotOptions = { + snapshot: string +} + +export type WidgetBridgePlugin = { + setSnapshot(options: SetSnapshotOptions): Promise + clearSnapshot(): Promise +} diff --git a/taskview-packages/capacitor-widget-bridge/src/index.ts b/taskview-packages/capacitor-widget-bridge/src/index.ts new file mode 100644 index 0000000..084f3db --- /dev/null +++ b/taskview-packages/capacitor-widget-bridge/src/index.ts @@ -0,0 +1,12 @@ +import { registerPlugin } from '@capacitor/core' +import type { WidgetBridgePlugin } from './definitions' + +export const WidgetBridge = registerPlugin('WidgetBridge') + +export type { + WidgetBridgePlugin, + WidgetSnapshot, + WidgetSnapshotMode, + WidgetSnapshotTask, + SetSnapshotOptions, +} from './definitions' diff --git a/taskview-packages/capacitor-widget-bridge/tsconfig.json b/taskview-packages/capacitor-widget-bridge/tsconfig.json new file mode 100644 index 0000000..fc34b70 --- /dev/null +++ b/taskview-packages/capacitor-widget-bridge/tsconfig.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ESNext", + "lib": [ + "ES2020" + ], + "skipLibCheck": true, + "moduleResolution": "bundler", + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "declaration": true, + "declarationMap": true, + "outDir": "./dist", + "noEmit": false, + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": [ + "src" + ] +} diff --git a/web/android/app/build.gradle b/web/android/app/build.gradle index db1f52b..5949f8e 100644 --- a/web/android/app/build.gradle +++ b/web/android/app/build.gradle @@ -7,8 +7,8 @@ android { applicationId "com.handscreamgnl.taskview.app" minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion - versionCode 14801 - versionName "1.48.1" + versionCode 14902 + versionName "1.49.2" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" aaptOptions { // Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps. diff --git a/web/android/app/capacitor.build.gradle b/web/android/app/capacitor.build.gradle index 81423f1..739bad6 100644 --- a/web/android/app/capacitor.build.gradle +++ b/web/android/app/capacitor.build.gradle @@ -17,6 +17,7 @@ dependencies { implementation project(':capacitor-push-notifications') implementation project(':capacitor-splash-screen') implementation project(':capgo-capacitor-updater') + implementation project(':capacitor-widget-bridge') } diff --git a/web/android/app/src/main/AndroidManifest.xml b/web/android/app/src/main/AndroidManifest.xml index 2602d18..cb904cd 100644 --- a/web/android/app/src/main/AndroidManifest.xml +++ b/web/android/app/src/main/AndroidManifest.xml @@ -29,6 +29,24 @@ + + + + + + + + + + tasks = new ArrayList<>(); + private boolean upcoming = false; + + public TodayWidgetFactory(Context context) { + this.context = context; + } + + @Override + public void onCreate() { + } + + @Override + public void onDataSetChanged() { + tasks.clear(); + upcoming = false; + String snapshot = context + .getSharedPreferences(WidgetBridgePlugin.PREFS_NAME, Context.MODE_PRIVATE) + .getString(WidgetBridgePlugin.SNAPSHOT_KEY, null); + if (snapshot == null) return; + try { + JSONObject parsed = new JSONObject(snapshot); + upcoming = "upcoming".equals(parsed.optString("mode")); + JSONArray items = parsed.optJSONArray("tasks"); + if (items == null) return; + for (int i = 0; i < items.length(); i++) { + JSONObject task = items.optJSONObject(i); + if (task != null) tasks.add(task); + } + } catch (JSONException ignored) { + } + } + + @Override + public void onDestroy() { + tasks.clear(); + } + + @Override + public int getCount() { + return tasks.size(); + } + + @Override + public RemoteViews getViewAt(int position) { + JSONObject task = tasks.get(position); + RemoteViews row = new RemoteViews(context.getPackageName(), R.layout.widget_today_item); + + row.setTextViewText(R.id.widget_item_title, task.optString("title")); + row.setImageViewResource(R.id.widget_item_checkbox, priorityCheckbox(task.optInt("priority", 1))); + + boolean overdue = !upcoming && task.optBoolean("overdue", false); + String meta = upcoming + ? formatEndDate(task) + : (overdue ? context.getString(R.string.widget_overdue) : formatEndTime(task)); + if (meta == null || meta.isEmpty()) { + row.setViewVisibility(R.id.widget_item_meta, View.GONE); + } else { + row.setViewVisibility(R.id.widget_item_meta, View.VISIBLE); + row.setTextViewText(R.id.widget_item_meta, meta); + row.setTextColor( + R.id.widget_item_meta, + context.getColor(overdue ? R.color.widget_overdue : R.color.widget_text_secondary)); + } + + Intent fillIn = new Intent(); + String path = task.optString("path", ""); + if (!path.isEmpty()) { + fillIn.setData(Uri.parse("taskview://open?path=" + Uri.encode(path))); + } + row.setOnClickFillInIntent(R.id.widget_item_root, fillIn); + + return row; + } + + private int priorityCheckbox(int priority) { + switch (priority) { + case 3: + return R.drawable.widget_checkbox_high; + case 2: + return R.drawable.widget_checkbox_medium; + default: + return R.drawable.widget_checkbox_low; + } + } + + private String formatEndTime(JSONObject task) { + if (task.isNull("endTime")) return null; + String endTime = task.optString("endTime", ""); + return endTime.length() >= 5 ? endTime.substring(0, 5) : endTime; + } + + private String formatEndDate(JSONObject task) { + if (task.isNull("endDate")) return null; + String endDate = task.optString("endDate", ""); + String[] parts = endDate.split("-"); + return parts.length == 3 ? parts[2] + "." + parts[1] : endDate; + } + + @Override + public RemoteViews getLoadingView() { + return null; + } + + @Override + public int getViewTypeCount() { + return 1; + } + + @Override + public long getItemId(int position) { + return tasks.get(position).optLong("id", position); + } + + @Override + public boolean hasStableIds() { + return true; + } +} diff --git a/web/android/app/src/main/java/com/handscream/taskview/app/widget/TodayWidgetProvider.java b/web/android/app/src/main/java/com/handscream/taskview/app/widget/TodayWidgetProvider.java new file mode 100644 index 0000000..805f62c --- /dev/null +++ b/web/android/app/src/main/java/com/handscream/taskview/app/widget/TodayWidgetProvider.java @@ -0,0 +1,90 @@ +package com.handscream.taskview.app.widget; + +import android.app.PendingIntent; +import android.appwidget.AppWidgetManager; +import android.appwidget.AppWidgetProvider; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import android.widget.RemoteViews; + +import com.handscream.taskview.app.MainActivity; +import com.handscream.taskview.app.R; + +import org.json.JSONObject; + +import tech.taskview.plugins.widgetbridge.WidgetBridgePlugin; + +public class TodayWidgetProvider extends AppWidgetProvider { + + @Override + public void onUpdate(Context context, AppWidgetManager manager, int[] appWidgetIds) { + for (int appWidgetId : appWidgetIds) { + manager.updateAppWidget(appWidgetId, buildViews(context, appWidgetId)); + } + } + + @Override + public void onReceive(Context context, Intent intent) { + super.onReceive(context, intent); + if (WidgetBridgePlugin.ACTION_WIDGET_UPDATE.equals(intent.getAction())) { + AppWidgetManager manager = AppWidgetManager.getInstance(context); + int[] ids = manager.getAppWidgetIds(new ComponentName(context, TodayWidgetProvider.class)); + if (ids.length == 0) return; + manager.notifyAppWidgetViewDataChanged(ids, R.id.widget_today_list); + onUpdate(context, manager, ids); + } + } + + private RemoteViews buildViews(Context context, int appWidgetId) { + RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.widget_today); + + JSONObject snapshot = readSnapshot(context); + boolean upcoming = snapshot != null && "upcoming".equals(snapshot.optString("mode")); + int count = snapshot == null + ? 0 + : (upcoming ? snapshot.optInt("upcomingCount", 0) : snapshot.optInt("todayCount", 0)); + views.setTextViewText( + R.id.widget_today_title, + context.getString(upcoming ? R.string.widget_upcoming_title : R.string.widget_today_title)); + views.setTextViewText(R.id.widget_today_count, String.valueOf(count)); + + Intent adapter = new Intent(context, TodayWidgetService.class); + adapter.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId); + adapter.setData(Uri.parse(adapter.toUri(Intent.URI_INTENT_SCHEME))); + views.setRemoteAdapter(R.id.widget_today_list, adapter); + views.setEmptyView(R.id.widget_today_list, R.id.widget_today_empty); + + PendingIntent openApp = PendingIntent.getActivity( + context, + 0, + new Intent(context, MainActivity.class), + PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE); + views.setOnClickPendingIntent(R.id.widget_today_header, openApp); + views.setOnClickPendingIntent(R.id.widget_today_empty, openApp); + + Intent template = new Intent(context, MainActivity.class); + template.setAction(Intent.ACTION_VIEW); + PendingIntent templateIntent = PendingIntent.getActivity( + context, + 1, + template, + PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_MUTABLE); + views.setPendingIntentTemplate(R.id.widget_today_list, templateIntent); + + return views; + } + + private JSONObject readSnapshot(Context context) { + String snapshot = context + .getSharedPreferences(WidgetBridgePlugin.PREFS_NAME, Context.MODE_PRIVATE) + .getString(WidgetBridgePlugin.SNAPSHOT_KEY, null); + if (snapshot == null) return null; + try { + return new JSONObject(snapshot); + } catch (Exception e) { + return null; + } + } +} diff --git a/web/android/app/src/main/java/com/handscream/taskview/app/widget/TodayWidgetService.java b/web/android/app/src/main/java/com/handscream/taskview/app/widget/TodayWidgetService.java new file mode 100644 index 0000000..7528861 --- /dev/null +++ b/web/android/app/src/main/java/com/handscream/taskview/app/widget/TodayWidgetService.java @@ -0,0 +1,12 @@ +package com.handscream.taskview.app.widget; + +import android.content.Intent; +import android.widget.RemoteViewsService; + +public class TodayWidgetService extends RemoteViewsService { + + @Override + public RemoteViewsFactory onGetViewFactory(Intent intent) { + return new TodayWidgetFactory(getApplicationContext()); + } +} diff --git a/web/android/app/src/main/res/drawable/widget_badge_bg.xml b/web/android/app/src/main/res/drawable/widget_badge_bg.xml new file mode 100644 index 0000000..df7869c --- /dev/null +++ b/web/android/app/src/main/res/drawable/widget_badge_bg.xml @@ -0,0 +1,6 @@ + + + + + diff --git a/web/android/app/src/main/res/drawable/widget_bg.xml b/web/android/app/src/main/res/drawable/widget_bg.xml new file mode 100644 index 0000000..e5b273b --- /dev/null +++ b/web/android/app/src/main/res/drawable/widget_bg.xml @@ -0,0 +1,6 @@ + + + + + diff --git a/web/android/app/src/main/res/drawable/widget_checkbox_high.xml b/web/android/app/src/main/res/drawable/widget_checkbox_high.xml new file mode 100644 index 0000000..5a78202 --- /dev/null +++ b/web/android/app/src/main/res/drawable/widget_checkbox_high.xml @@ -0,0 +1,8 @@ + + + + + diff --git a/web/android/app/src/main/res/drawable/widget_checkbox_low.xml b/web/android/app/src/main/res/drawable/widget_checkbox_low.xml new file mode 100644 index 0000000..f63c1b3 --- /dev/null +++ b/web/android/app/src/main/res/drawable/widget_checkbox_low.xml @@ -0,0 +1,8 @@ + + + + + diff --git a/web/android/app/src/main/res/drawable/widget_checkbox_medium.xml b/web/android/app/src/main/res/drawable/widget_checkbox_medium.xml new file mode 100644 index 0000000..5a5ad68 --- /dev/null +++ b/web/android/app/src/main/res/drawable/widget_checkbox_medium.xml @@ -0,0 +1,8 @@ + + + + + diff --git a/web/android/app/src/main/res/drawable/widget_header_bg.xml b/web/android/app/src/main/res/drawable/widget_header_bg.xml new file mode 100644 index 0000000..8f224c9 --- /dev/null +++ b/web/android/app/src/main/res/drawable/widget_header_bg.xml @@ -0,0 +1,8 @@ + + + + + diff --git a/web/android/app/src/main/res/layout/widget_today.xml b/web/android/app/src/main/res/layout/widget_today.xml new file mode 100644 index 0000000..ed7260c --- /dev/null +++ b/web/android/app/src/main/res/layout/widget_today.xml @@ -0,0 +1,76 @@ + + + + + + + + + + + + + + + + + + + diff --git a/web/android/app/src/main/res/layout/widget_today_item.xml b/web/android/app/src/main/res/layout/widget_today_item.xml new file mode 100644 index 0000000..e18e6c0 --- /dev/null +++ b/web/android/app/src/main/res/layout/widget_today_item.xml @@ -0,0 +1,39 @@ + + + + + + + + + diff --git a/web/android/app/src/main/res/values-night/widget_colors.xml b/web/android/app/src/main/res/values-night/widget_colors.xml new file mode 100644 index 0000000..aa13d73 --- /dev/null +++ b/web/android/app/src/main/res/values-night/widget_colors.xml @@ -0,0 +1,8 @@ + + + #18181B + #27272A + #3F3F46 + #FAFAFA + #A1A1AA + diff --git a/web/android/app/src/main/res/values-ru/strings_widget.xml b/web/android/app/src/main/res/values-ru/strings_widget.xml new file mode 100644 index 0000000..c2a385a --- /dev/null +++ b/web/android/app/src/main/res/values-ru/strings_widget.xml @@ -0,0 +1,8 @@ + + + Сегодня + Ближайшие + Нет задач на сегодня + Просрочено + Ваши задачи на сегодня + diff --git a/web/android/app/src/main/res/values/strings_widget.xml b/web/android/app/src/main/res/values/strings_widget.xml new file mode 100644 index 0000000..7b79d13 --- /dev/null +++ b/web/android/app/src/main/res/values/strings_widget.xml @@ -0,0 +1,8 @@ + + + Today + Upcoming + No tasks for today + Overdue + Your tasks for today at a glance + diff --git a/web/android/app/src/main/res/values/widget_colors.xml b/web/android/app/src/main/res/values/widget_colors.xml new file mode 100644 index 0000000..18cb493 --- /dev/null +++ b/web/android/app/src/main/res/values/widget_colors.xml @@ -0,0 +1,13 @@ + + + #FFFFFF + #F4F4F5 + #E4E4E7 + #18181B + #71717A + #16A34A + #FF1744 + #38D681 + #FF9100 + #FF1744 + diff --git a/web/android/app/src/main/res/xml/widget_today_info.xml b/web/android/app/src/main/res/xml/widget_today_info.xml new file mode 100644 index 0000000..9d71a24 --- /dev/null +++ b/web/android/app/src/main/res/xml/widget_today_info.xml @@ -0,0 +1,14 @@ + + diff --git a/web/android/capacitor.settings.gradle b/web/android/capacitor.settings.gradle index 2d705a9..94cdeda 100644 --- a/web/android/capacitor.settings.gradle +++ b/web/android/capacitor.settings.gradle @@ -1,27 +1,30 @@ // DO NOT EDIT THIS FILE! IT IS GENERATED EACH TIME "capacitor update" IS RUN include ':capacitor-android' -project(':capacitor-android').projectDir = new File('../../node_modules/.pnpm/@capacitor+android@8.1.0_@capacitor+core@8.1.0/node_modules/@capacitor/android/capacitor') +project(':capacitor-android').projectDir = new File('../../../node_modules/.pnpm/@capacitor+android@8.1.0_@capacitor+core@8.1.0/node_modules/@capacitor/android/capacitor') include ':capacitor-firebase-messaging' -project(':capacitor-firebase-messaging').projectDir = new File('../../node_modules/.pnpm/@capacitor-firebase+messaging@8.1.0_@capacitor+core@8.1.0_firebase@12.10.0/node_modules/@capacitor-firebase/messaging/android') +project(':capacitor-firebase-messaging').projectDir = new File('../../../node_modules/.pnpm/@capacitor-firebase+messaging@8.1.0_@capacitor+core@8.1.0_firebase@12.11.0/node_modules/@capacitor-firebase/messaging/android') include ':capacitor-app' -project(':capacitor-app').projectDir = new File('../../node_modules/.pnpm/@capacitor+app@8.0.1_@capacitor+core@8.1.0/node_modules/@capacitor/app/android') +project(':capacitor-app').projectDir = new File('../../../node_modules/.pnpm/@capacitor+app@8.0.1_@capacitor+core@8.1.0/node_modules/@capacitor/app/android') include ':capacitor-browser' -project(':capacitor-browser').projectDir = new File('../../node_modules/.pnpm/@capacitor+browser@8.0.1_@capacitor+core@8.1.0/node_modules/@capacitor/browser/android') +project(':capacitor-browser').projectDir = new File('../../../node_modules/.pnpm/@capacitor+browser@8.0.1_@capacitor+core@8.1.0/node_modules/@capacitor/browser/android') include ':capacitor-device' -project(':capacitor-device').projectDir = new File('../../node_modules/.pnpm/@capacitor+device@8.0.1_@capacitor+core@8.1.0/node_modules/@capacitor/device/android') +project(':capacitor-device').projectDir = new File('../../../node_modules/.pnpm/@capacitor+device@8.0.1_@capacitor+core@8.1.0/node_modules/@capacitor/device/android') include ':capacitor-preferences' -project(':capacitor-preferences').projectDir = new File('../../node_modules/.pnpm/@capacitor+preferences@8.0.1_@capacitor+core@8.1.0/node_modules/@capacitor/preferences/android') +project(':capacitor-preferences').projectDir = new File('../../../node_modules/.pnpm/@capacitor+preferences@8.0.1_@capacitor+core@8.1.0/node_modules/@capacitor/preferences/android') include ':capacitor-push-notifications' -project(':capacitor-push-notifications').projectDir = new File('../../node_modules/.pnpm/@capacitor+push-notifications@8.0.2_@capacitor+core@8.1.0/node_modules/@capacitor/push-notifications/android') +project(':capacitor-push-notifications').projectDir = new File('../../../node_modules/.pnpm/@capacitor+push-notifications@8.0.3_@capacitor+core@8.1.0/node_modules/@capacitor/push-notifications/android') include ':capacitor-splash-screen' -project(':capacitor-splash-screen').projectDir = new File('../../node_modules/.pnpm/@capacitor+splash-screen@8.0.1_@capacitor+core@8.1.0/node_modules/@capacitor/splash-screen/android') +project(':capacitor-splash-screen').projectDir = new File('../../../node_modules/.pnpm/@capacitor+splash-screen@8.0.1_@capacitor+core@8.1.0/node_modules/@capacitor/splash-screen/android') include ':capgo-capacitor-updater' -project(':capgo-capacitor-updater').projectDir = new File('../../node_modules/.pnpm/@capgo+capacitor-updater@8.43.8_@capacitor+core@8.1.0/node_modules/@capgo/capacitor-updater/android') +project(':capgo-capacitor-updater').projectDir = new File('../../../node_modules/.pnpm/@capgo+capacitor-updater@8.43.2_@capacitor+core@8.1.0/node_modules/@capgo/capacitor-updater/android') + +include ':capacitor-widget-bridge' +project(':capacitor-widget-bridge').projectDir = new File('../node_modules/capacitor-widget-bridge/android') diff --git a/web/capacitor.config.ts b/web/capacitor.config.ts index 0f094af..626eee9 100644 --- a/web/capacitor.config.ts +++ b/web/capacitor.config.ts @@ -25,6 +25,9 @@ const config: CapacitorConfig = { CapacitorHttp: { enabled: true, }, + WidgetBridge: { + appGroup: 'group.com.handscream.taskview.app', + }, }, } diff --git a/web/ios/App/App.xcodeproj/project.pbxproj b/web/ios/App/App.xcodeproj/project.pbxproj index 02a0c63..bbb2320 100644 --- a/web/ios/App/App.xcodeproj/project.pbxproj +++ b/web/ios/App/App.xcodeproj/project.pbxproj @@ -8,6 +8,9 @@ /* Begin PBXBuildFile section */ 2FAD9763203C412B000D30F8 /* config.xml in Resources */ = {isa = PBXBuildFile; fileRef = 2FAD9762203C412B000D30F8 /* config.xml */; }; + 30163AB74EEE183517D215DC /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 41EECE9262690D5E2EF23CE9 /* Assets.xcassets */; }; + 30F5ABA9CC301D3E3BA985C7 /* TaskViewWidgetBundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D90258576D7FDAEEBD7298B /* TaskViewWidgetBundle.swift */; }; + 336711805E8F2928D054F35E /* TodayWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4822ABF5AC8CD3030A5CE020 /* TodayWidget.swift */; }; 4D22ABE92AF431CB00220026 /* CapApp-SPM in Frameworks */ = {isa = PBXBuildFile; productRef = 4D22ABE82AF431CB00220026 /* CapApp-SPM */; }; 50379B232058CBB4000EE86E /* capacitor.config.json in Resources */ = {isa = PBXBuildFile; fileRef = 50379B222058CBB4000EE86E /* capacitor.config.json */; }; 504EC3081FED79650016851F /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 504EC3071FED79650016851F /* AppDelegate.swift */; }; @@ -15,11 +18,43 @@ 504EC30F1FED79650016851F /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 504EC30E1FED79650016851F /* Assets.xcassets */; }; 504EC3121FED79650016851F /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 504EC3101FED79650016851F /* LaunchScreen.storyboard */; }; 50B271D11FEDC1A000F3C39B /* public in Resources */ = {isa = PBXBuildFile; fileRef = 50B271D01FEDC1A000F3C39B /* public */; }; + 7E638BA1BFCF4E185865F85D /* TaskViewWidget.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 7F4057F5AC9B7A7AC7E70240 /* TaskViewWidget.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; + A71181DC51C8DA03879B72ED /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 029A385E0A8D9B1FCBD9CC75 /* Foundation.framework */; }; AAA632B22F6BB598007C705D /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = AAA632B12F6BB598007C705D /* GoogleService-Info.plist */; }; + ABED08BA434D23E174534E03 /* WidgetSnapshotModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 873F85DE4FF5908A8CA6A467 /* WidgetSnapshotModel.swift */; }; + AB10F00D00000000000000A2 /* OpenTaskIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB10F00D00000000000000A1 /* OpenTaskIntent.swift */; }; + AB10F00D00000000000000A3 /* OpenTaskIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB10F00D00000000000000A1 /* OpenTaskIntent.swift */; }; /* End PBXBuildFile section */ +/* Begin PBXContainerItemProxy section */ + 8471300B26377F4336A38D36 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 504EC2FC1FED79650016851F /* Project object */; + proxyType = 1; + remoteGlobalIDString = 13D58B519FF779A139CB09EB; + remoteInfo = TaskViewWidget; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + E6DDB52E9A683D16F43B337C /* Embed Foundation Extensions */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 13; + files = ( + 7E638BA1BFCF4E185865F85D /* TaskViewWidget.appex in Embed Foundation Extensions */, + ); + name = "Embed Foundation Extensions"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + /* Begin PBXFileReference section */ + 029A385E0A8D9B1FCBD9CC75 /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS18.0.sdk/System/Library/Frameworks/Foundation.framework; sourceTree = DEVELOPER_DIR; }; 2FAD9762203C412B000D30F8 /* config.xml */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = config.xml; sourceTree = ""; }; + 41EECE9262690D5E2EF23CE9 /* Assets.xcassets */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 4822ABF5AC8CD3030A5CE020 /* TodayWidget.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TodayWidget.swift; sourceTree = ""; }; 50379B222058CBB4000EE86E /* capacitor.config.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = capacitor.config.json; sourceTree = ""; }; 504EC3041FED79650016851F /* App.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = App.app; sourceTree = BUILT_PRODUCTS_DIR; }; 504EC3071FED79650016851F /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; @@ -28,9 +63,15 @@ 504EC3111FED79650016851F /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 504EC3131FED79650016851F /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 50B271D01FEDC1A000F3C39B /* public */ = {isa = PBXFileReference; lastKnownFileType = folder; path = public; sourceTree = ""; }; + 7F4057F5AC9B7A7AC7E70240 /* TaskViewWidget.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = TaskViewWidget.appex; sourceTree = BUILT_PRODUCTS_DIR; }; + 873F85DE4FF5908A8CA6A467 /* WidgetSnapshotModel.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = WidgetSnapshotModel.swift; sourceTree = ""; }; + AB10F00D00000000000000A1 /* OpenTaskIntent.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = OpenTaskIntent.swift; sourceTree = ""; }; 958DCC722DB07C7200EA8C5F /* debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = debug.xcconfig; path = ../debug.xcconfig; sourceTree = SOURCE_ROOT; }; + 9D90258576D7FDAEEBD7298B /* TaskViewWidgetBundle.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TaskViewWidgetBundle.swift; sourceTree = ""; }; AAA632B02F69F429007C705D /* App.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = App.entitlements; sourceTree = ""; }; AAA632B12F6BB598007C705D /* GoogleService-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = ""; }; + EB7495A9A40AB2365CC0624E /* Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + F1EEEB83D95DC32C9728EAC4 /* TaskViewWidget.entitlements */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.entitlements; path = TaskViewWidget.entitlements; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -42,9 +83,25 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 8331741D08C05C5C85275E50 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + A71181DC51C8DA03879B72ED /* Foundation.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 4961C8EBB434EDD15B55395B /* Frameworks */ = { + isa = PBXGroup; + children = ( + 7E3EA782F4406B2B6D7A0121 /* iOS */, + ); + name = Frameworks; + sourceTree = ""; + }; 504EC2FB1FED79650016851F = { isa = PBXGroup; children = ( @@ -52,6 +109,8 @@ 958DCC722DB07C7200EA8C5F /* debug.xcconfig */, 504EC3061FED79650016851F /* App */, 504EC3051FED79650016851F /* Products */, + 4961C8EBB434EDD15B55395B /* Frameworks */, + 79D12E01B1FBD77844324865 /* TaskViewWidget */, ); sourceTree = ""; }; @@ -59,6 +118,7 @@ isa = PBXGroup; children = ( 504EC3041FED79650016851F /* App.app */, + 7F4057F5AC9B7A7AC7E70240 /* TaskViewWidget.appex */, ); name = Products; sourceTree = ""; @@ -79,9 +139,49 @@ path = App; sourceTree = ""; }; + 79D12E01B1FBD77844324865 /* TaskViewWidget */ = { + isa = PBXGroup; + children = ( + 9D90258576D7FDAEEBD7298B /* TaskViewWidgetBundle.swift */, + 4822ABF5AC8CD3030A5CE020 /* TodayWidget.swift */, + 873F85DE4FF5908A8CA6A467 /* WidgetSnapshotModel.swift */, + AB10F00D00000000000000A1 /* OpenTaskIntent.swift */, + EB7495A9A40AB2365CC0624E /* Info.plist */, + F1EEEB83D95DC32C9728EAC4 /* TaskViewWidget.entitlements */, + 41EECE9262690D5E2EF23CE9 /* Assets.xcassets */, + ); + name = TaskViewWidget; + path = TaskViewWidget; + sourceTree = ""; + }; + 7E3EA782F4406B2B6D7A0121 /* iOS */ = { + isa = PBXGroup; + children = ( + 029A385E0A8D9B1FCBD9CC75 /* Foundation.framework */, + ); + name = iOS; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ + 13D58B519FF779A139CB09EB /* TaskViewWidget */ = { + isa = PBXNativeTarget; + buildConfigurationList = BA6B4CB7C019DA2E47F2AE5D /* Build configuration list for PBXNativeTarget "TaskViewWidget" */; + buildPhases = ( + 2B48B6699C53A2E975CEE972 /* Sources */, + 8331741D08C05C5C85275E50 /* Frameworks */, + 8F380AD759A2116160808CCC /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = TaskViewWidget; + productName = TaskViewWidget; + productReference = 7F4057F5AC9B7A7AC7E70240 /* TaskViewWidget.appex */; + productType = "com.apple.product-type.app-extension"; + }; 504EC3031FED79650016851F /* App */ = { isa = PBXNativeTarget; buildConfigurationList = 504EC3161FED79650016851F /* Build configuration list for PBXNativeTarget "App" */; @@ -89,10 +189,12 @@ 504EC3001FED79650016851F /* Sources */, 504EC3011FED79650016851F /* Frameworks */, 504EC3021FED79650016851F /* Resources */, + E6DDB52E9A683D16F43B337C /* Embed Foundation Extensions */, ); buildRules = ( ); dependencies = ( + 70738FEC670F89727813A33B /* PBXTargetDependency */, ); name = App; packageProductDependencies = ( @@ -135,6 +237,7 @@ projectRoot = ""; targets = ( 504EC3031FED79650016851F /* App */, + 13D58B519FF779A139CB09EB /* TaskViewWidget */, ); }; /* End PBXProject section */ @@ -154,19 +257,48 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 8F380AD759A2116160808CCC /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 30163AB74EEE183517D215DC /* Assets.xcassets in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXResourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ + 2B48B6699C53A2E975CEE972 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 30F5ABA9CC301D3E3BA985C7 /* TaskViewWidgetBundle.swift in Sources */, + 336711805E8F2928D054F35E /* TodayWidget.swift in Sources */, + ABED08BA434D23E174534E03 /* WidgetSnapshotModel.swift in Sources */, + AB10F00D00000000000000A2 /* OpenTaskIntent.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; 504EC3001FED79650016851F /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( 504EC3081FED79650016851F /* AppDelegate.swift in Sources */, + AB10F00D00000000000000A3 /* OpenTaskIntent.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXSourcesBuildPhase section */ +/* Begin PBXTargetDependency section */ + 70738FEC670F89727813A33B /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + name = TaskViewWidget; + target = 13D58B519FF779A139CB09EB /* TaskViewWidget */; + targetProxy = 8471300B26377F4336A38D36 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + /* Begin PBXVariantGroup section */ 504EC30B1FED79650016851F /* Main.storyboard */ = { isa = PBXVariantGroup; @@ -304,7 +436,7 @@ CODE_SIGN_ALLOW_ENTITLEMENTS_MODIFICATION = YES; CODE_SIGN_ENTITLEMENTS = App/App.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1.48.1; + CURRENT_PROJECT_VERSION = 1.49.2; DEVELOPMENT_TEAM = H2W2SG48JT; INFOPLIST_FILE = App/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 15.0; @@ -312,7 +444,12 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.48.1; + MARKETING_VERSION = 1.49.2; + OTHER_LDFLAGS = ( + "$(inherited)", + "-weak_framework", + AppIntents, + ); OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\""; PRODUCT_BUNDLE_IDENTIFIER = com.handscream.taskview.app; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -329,7 +466,7 @@ CODE_SIGN_ALLOW_ENTITLEMENTS_MODIFICATION = YES; CODE_SIGN_ENTITLEMENTS = App/App.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1.48.1; + CURRENT_PROJECT_VERSION = 1.49.2; DEVELOPMENT_TEAM = H2W2SG48JT; INFOPLIST_FILE = App/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 15.0; @@ -337,7 +474,12 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.48.1; + MARKETING_VERSION = 1.49.2; + OTHER_LDFLAGS = ( + "$(inherited)", + "-weak_framework", + AppIntents, + ); PRODUCT_BUNDLE_IDENTIFIER = com.handscream.taskview.app; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_ACTIVE_COMPILATION_CONDITIONS = ""; @@ -346,6 +488,63 @@ }; name = Release; }; + 578B436A7C18D66B2BDFCF0B /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_ENABLE_OBJC_WEAK = NO; + CODE_SIGN_ENTITLEMENTS = TaskViewWidget/TaskViewWidget.entitlements; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1.49.2; + DEVELOPMENT_TEAM = H2W2SG48JT; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = TaskViewWidget/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = "TaskView Widget"; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + MARKETING_VERSION = 1.49.2; + PRODUCT_BUNDLE_IDENTIFIER = com.handscream.taskview.app.widget; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) WIDGET_EXTENSION"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + AC8696991AE5C61127E97D40 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_ENABLE_OBJC_WEAK = NO; + CODE_SIGN_ENTITLEMENTS = TaskViewWidget/TaskViewWidget.entitlements; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1.49.2; + DEVELOPMENT_TEAM = H2W2SG48JT; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = TaskViewWidget/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = "TaskView Widget"; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + MARKETING_VERSION = 1.49.2; + PRODUCT_BUNDLE_IDENTIFIER = com.handscream.taskview.app.widget; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) WIDGET_EXTENSION"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ @@ -367,6 +566,15 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; + BA6B4CB7C019DA2E47F2AE5D /* Build configuration list for PBXNativeTarget "TaskViewWidget" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + AC8696991AE5C61127E97D40 /* Release */, + 578B436A7C18D66B2BDFCF0B /* Debug */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; /* End XCConfigurationList section */ /* Begin XCLocalSwiftPackageReference section */ diff --git a/web/ios/App/App/App.entitlements b/web/ios/App/App/App.entitlements index 903def2..837078c 100644 --- a/web/ios/App/App/App.entitlements +++ b/web/ios/App/App/App.entitlements @@ -4,5 +4,9 @@ aps-environment development + com.apple.security.application-groups + + group.com.handscream.taskview.app + diff --git a/web/ios/App/CapApp-SPM/Package.swift b/web/ios/App/CapApp-SPM/Package.swift index da91ba5..c0c83da 100644 --- a/web/ios/App/CapApp-SPM/Package.swift +++ b/web/ios/App/CapApp-SPM/Package.swift @@ -12,14 +12,15 @@ let package = Package( ], dependencies: [ .package(url: "https://github.com/ionic-team/capacitor-swift-pm.git", exact: "8.1.0"), - .package(name: "CapacitorFirebaseMessaging", path: "../../../../node_modules/.pnpm/@capacitor-firebase+messaging@8.1.0_@capacitor+core@8.1.0_firebase@12.10.0/node_modules/@capacitor-firebase/messaging"), - .package(name: "CapacitorApp", path: "../../../../node_modules/.pnpm/@capacitor+app@8.0.1_@capacitor+core@8.1.0/node_modules/@capacitor/app"), - .package(name: "CapacitorBrowser", path: "../../../../node_modules/.pnpm/@capacitor+browser@8.0.1_@capacitor+core@8.1.0/node_modules/@capacitor/browser"), - .package(name: "CapacitorDevice", path: "../../../../node_modules/.pnpm/@capacitor+device@8.0.1_@capacitor+core@8.1.0/node_modules/@capacitor/device"), - .package(name: "CapacitorPreferences", path: "../../../../node_modules/.pnpm/@capacitor+preferences@8.0.1_@capacitor+core@8.1.0/node_modules/@capacitor/preferences"), - .package(name: "CapacitorPushNotifications", path: "../../../../node_modules/.pnpm/@capacitor+push-notifications@8.0.2_@capacitor+core@8.1.0/node_modules/@capacitor/push-notifications"), - .package(name: "CapacitorSplashScreen", path: "../../../../node_modules/.pnpm/@capacitor+splash-screen@8.0.1_@capacitor+core@8.1.0/node_modules/@capacitor/splash-screen"), - .package(name: "CapgoCapacitorUpdater", path: "../../../../node_modules/.pnpm/@capgo+capacitor-updater@8.43.8_@capacitor+core@8.1.0/node_modules/@capgo/capacitor-updater") + .package(name: "CapacitorFirebaseMessaging", path: "../../../../../node_modules/.pnpm/@capacitor-firebase+messaging@8.1.0_@capacitor+core@8.1.0_firebase@12.11.0/node_modules/@capacitor-firebase/messaging"), + .package(name: "CapacitorApp", path: "../../../../../node_modules/.pnpm/@capacitor+app@8.0.1_@capacitor+core@8.1.0/node_modules/@capacitor/app"), + .package(name: "CapacitorBrowser", path: "../../../../../node_modules/.pnpm/@capacitor+browser@8.0.1_@capacitor+core@8.1.0/node_modules/@capacitor/browser"), + .package(name: "CapacitorDevice", path: "../../../../../node_modules/.pnpm/@capacitor+device@8.0.1_@capacitor+core@8.1.0/node_modules/@capacitor/device"), + .package(name: "CapacitorPreferences", path: "../../../../../node_modules/.pnpm/@capacitor+preferences@8.0.1_@capacitor+core@8.1.0/node_modules/@capacitor/preferences"), + .package(name: "CapacitorPushNotifications", path: "../../../../../node_modules/.pnpm/@capacitor+push-notifications@8.0.3_@capacitor+core@8.1.0/node_modules/@capacitor/push-notifications"), + .package(name: "CapacitorSplashScreen", path: "../../../../../node_modules/.pnpm/@capacitor+splash-screen@8.0.1_@capacitor+core@8.1.0/node_modules/@capacitor/splash-screen"), + .package(name: "CapgoCapacitorUpdater", path: "../../../../../node_modules/.pnpm/@capgo+capacitor-updater@8.43.2_@capacitor+core@8.1.0/node_modules/@capgo/capacitor-updater"), + .package(name: "CapacitorWidgetBridge", path: "../../../node_modules/capacitor-widget-bridge") ], targets: [ .target( @@ -34,7 +35,8 @@ let package = Package( .product(name: "CapacitorPreferences", package: "CapacitorPreferences"), .product(name: "CapacitorPushNotifications", package: "CapacitorPushNotifications"), .product(name: "CapacitorSplashScreen", package: "CapacitorSplashScreen"), - .product(name: "CapgoCapacitorUpdater", package: "CapgoCapacitorUpdater") + .product(name: "CapgoCapacitorUpdater", package: "CapgoCapacitorUpdater"), + .product(name: "CapacitorWidgetBridge", package: "CapacitorWidgetBridge") ] ) ] diff --git a/web/ios/App/TaskViewWidget/Assets.xcassets/Contents.json b/web/ios/App/TaskViewWidget/Assets.xcassets/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/web/ios/App/TaskViewWidget/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/web/ios/App/TaskViewWidget/Assets.xcassets/WidgetLogo.imageset/Contents.json b/web/ios/App/TaskViewWidget/Assets.xcassets/WidgetLogo.imageset/Contents.json new file mode 100644 index 0000000..7c58c47 --- /dev/null +++ b/web/ios/App/TaskViewWidget/Assets.xcassets/WidgetLogo.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "logo.png", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/web/ios/App/TaskViewWidget/Assets.xcassets/WidgetLogo.imageset/logo.png b/web/ios/App/TaskViewWidget/Assets.xcassets/WidgetLogo.imageset/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..0df96db654385499780bcac0930aea220be41ae7 GIT binary patch literal 10700 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 some IntentResult & OpensIntent { + .result(opensIntent: OpenURLIntent(resolvedURL)) + } + #else + @MainActor + func perform() async throws -> some IntentResult { + await UIApplication.shared.open(resolvedURL) + return .result() + } + #endif +} diff --git a/web/ios/App/TaskViewWidget/TaskViewWidget.entitlements b/web/ios/App/TaskViewWidget/TaskViewWidget.entitlements new file mode 100644 index 0000000..bd2204d --- /dev/null +++ b/web/ios/App/TaskViewWidget/TaskViewWidget.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.security.application-groups + + group.com.handscream.taskview.app + + + diff --git a/web/ios/App/TaskViewWidget/TaskViewWidgetBundle.swift b/web/ios/App/TaskViewWidget/TaskViewWidgetBundle.swift new file mode 100644 index 0000000..d56c64a --- /dev/null +++ b/web/ios/App/TaskViewWidget/TaskViewWidgetBundle.swift @@ -0,0 +1,9 @@ +import WidgetKit +import SwiftUI + +@main +struct TaskViewWidgetBundle: WidgetBundle { + var body: some Widget { + TodayWidget() + } +} diff --git a/web/ios/App/TaskViewWidget/TodayWidget.swift b/web/ios/App/TaskViewWidget/TodayWidget.swift new file mode 100644 index 0000000..e7b1cfa --- /dev/null +++ b/web/ios/App/TaskViewWidget/TodayWidget.swift @@ -0,0 +1,251 @@ +import WidgetKit +import SwiftUI + +struct TodayEntry: TimelineEntry { + let date: Date + let snapshot: WidgetSnapshot? +} + +struct TodayProvider: TimelineProvider { + func placeholder(in context: Context) -> TodayEntry { + TodayEntry(date: .now, snapshot: nil) + } + + func getSnapshot(in context: Context, completion: @escaping (TodayEntry) -> Void) { + completion(TodayEntry(date: .now, snapshot: WidgetSnapshot.load())) + } + + func getTimeline(in context: Context, completion: @escaping (Timeline) -> Void) { + let entry = TodayEntry(date: .now, snapshot: WidgetSnapshot.load()) + let refresh = Calendar.current.date(byAdding: .minute, value: 30, to: .now) ?? .now + completion(Timeline(entries: [entry], policy: .after(refresh))) + } +} + +struct TodayWidget: Widget { + let kind = "TaskViewTodayWidget" + + var body: some WidgetConfiguration { + StaticConfiguration(kind: kind, provider: TodayProvider()) { entry in + TodayWidgetView(entry: entry) + .containerBackground(for: .widget) { + Color(uiColor: .systemBackground) + } + } + .configurationDisplayName("TaskView") + .description("Today's tasks / Задачи на сегодня") + .supportedFamilies([.systemSmall, .systemMedium, .systemLarge]) + .contentMarginsDisabled() + } +} + +struct TodayWidgetView: View { + @Environment(\.widgetFamily) private var family + + let entry: TodayEntry + + private var strings: WidgetStrings { + WidgetStrings.forLocale(entry.snapshot?.locale) + } + + private var maxSlots: Int { + switch family { + case .systemLarge: return 8 + case .systemSmall: return 4 + default: return 3 + } + } + + var body: some View { + TaskListTodayView( + snapshot: entry.snapshot, + strings: strings, + maxSlots: maxSlots, + compact: family == .systemSmall + ) + } +} + +struct TaskListTodayView: View { + let snapshot: WidgetSnapshot? + let strings: WidgetStrings + let maxSlots: Int + let compact: Bool + + private var horizontalPadding: CGFloat { compact ? 12 : 16 } + private var rowVerticalPadding: CGFloat { compact ? 6 : 10 } + private var dividerInset: CGFloat { compact ? 38 : 48 } + + private var isUpcoming: Bool { + snapshot?.isUpcoming ?? false + } + + private var visibleTasks: [WidgetSnapshotTask] { + guard let snapshot else { return [] } + let count = snapshot.activeCount > maxSlots ? maxSlots - 1 : maxSlots + return Array(snapshot.tasks.prefix(count)) + } + + private var hiddenCount: Int { + guard let snapshot else { return 0 } + return max(0, snapshot.activeCount - visibleTasks.count) + } + + var body: some View { + VStack(spacing: 0) { + HStack(spacing: compact ? 6 : 8) { + WidgetLogoView(size: compact ? 16 : 20) + + Text(isUpcoming ? strings.upcoming : strings.today) + .font(compact ? .footnote.weight(.semibold) : .headline) + + Spacer() + + Text("\(snapshot?.activeCount ?? 0)") + .font(.caption2.weight(.bold)) + .contentTransition(.numericText(countsDown: true)) + .foregroundStyle(.white) + .padding(.horizontal, compact ? 6 : 8) + .padding(.vertical, compact ? 2 : 3) + .background(WidgetPalette.accent, in: Capsule()) + } + .padding(.horizontal, horizontalPadding) + .padding(.top, 12) + .padding(.bottom, compact ? 8 : 12) + .background(WidgetPalette.headerBackground) + + if !visibleTasks.isEmpty { + VStack(spacing: 0) { + ForEach(Array(visibleTasks.enumerated()), id: \.element.id) { index, task in + VStack(spacing: 0) { + if index > 0 { + Divider() + .padding(.leading, dividerInset) + } + + TaskRowView(task: task, strings: strings, compact: compact, showDate: isUpcoming) + .padding(.horizontal, horizontalPadding) + .padding(.vertical, rowVerticalPadding) + } + .transition(.opacity.combined(with: .move(edge: .trailing))) + } + + if hiddenCount > 0 { + Divider() + .padding(.leading, dividerInset) + + Text(strings.more(hiddenCount)) + .font(compact ? .caption2 : .footnote) + .foregroundStyle(.secondary) + .frame(maxWidth: .infinity, alignment: .leading) + .padding(.leading, dividerInset) + .padding(.trailing, horizontalPadding) + .padding(.vertical, rowVerticalPadding) + } + } + Spacer(minLength: 0) + } else { + Spacer() + Text(snapshot == nil ? strings.openApp : strings.empty) + .font(compact ? .caption : .footnote) + .foregroundStyle(.secondary) + .frame(maxWidth: .infinity, alignment: .center) + .padding(.horizontal, horizontalPadding) + Spacer() + } + } + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading) + .widgetURL(compact ? URL(string: "taskview://open") : nil) + } +} + +struct TaskRowView: View { + let task: WidgetSnapshotTask + let strings: WidgetStrings + let compact: Bool + let showDate: Bool + + private var destination: URL { + task.deepLinkURL ?? URL(string: "taskview://open")! + } + + var body: some View { + if compact { + if #available(iOS 18.0, *) { + Button(intent: OpenTaskIntent(urlString: destination.absoluteString)) { + row + } + .buttonStyle(.plain) + } else { + row + } + } else { + Link(destination: destination) { + row + } + } + } + + private var row: some View { + HStack(spacing: compact ? 4 : 6) { + Circle() + .strokeBorder(WidgetPalette.priority(task.priority), lineWidth: compact ? 1.2 : 1.5) + .frame(width: compact ? 14 : 18, height: compact ? 14 : 18) + .padding(4) + + content + } + } + + private var content: some View { + HStack(spacing: compact ? 8 : 10) { + Text(task.title) + .font(compact ? .caption : .subheadline) + .foregroundStyle(.primary) + .lineLimit(1) + + Spacer(minLength: 4) + + if showDate { + if let endDate = task.shortEndDate { + Text(endDate) + .font(compact ? .caption2 : .caption) + .foregroundStyle(.secondary) + } + } else if task.overdue { + Text(compact ? "!" : strings.overdue) + .font(compact ? .caption.weight(.bold) : .caption.weight(.medium)) + .foregroundStyle(WidgetPalette.overdue) + } else if let endTime = task.shortEndTime { + Text(endTime) + .font(compact ? .caption2 : .caption) + .foregroundStyle(.secondary) + } + } + } +} + +struct WidgetLogoView: View { + let size: CGFloat + + var body: some View { + Image("WidgetLogo") + .resizable() + .frame(width: size, height: size) + .clipShape(RoundedRectangle(cornerRadius: size * 0.28)) + } +} + +enum WidgetPalette { + static let accent = Color(red: 0.086, green: 0.639, blue: 0.290) + static let overdue = Color(red: 1.0, green: 0.090, blue: 0.267) + static let headerBackground = Color(uiColor: .secondarySystemBackground).opacity(0.7) + + static func priority(_ priority: Int) -> Color { + switch priority { + case 3: return Color(red: 1.0, green: 0.090, blue: 0.267) + case 2: return Color(red: 1.0, green: 0.569, blue: 0.0) + default: return Color(red: 0.220, green: 0.839, blue: 0.506) + } + } +} diff --git a/web/ios/App/TaskViewWidget/WidgetSnapshotModel.swift b/web/ios/App/TaskViewWidget/WidgetSnapshotModel.swift new file mode 100644 index 0000000..ca80087 --- /dev/null +++ b/web/ios/App/TaskViewWidget/WidgetSnapshotModel.swift @@ -0,0 +1,94 @@ +import Foundation + +struct WidgetSnapshotTask: Decodable, Identifiable { + let id: Int + let title: String + let priority: Int + let overdue: Bool + let endTime: String? + let endDate: String? + let path: String + + var deepLinkURL: URL? { + guard let encoded = path.addingPercentEncoding(withAllowedCharacters: .alphanumerics) else { return nil } + return URL(string: "taskview://open?path=\(encoded)") + } + + var shortEndTime: String? { + guard let endTime, endTime.count >= 5 else { return endTime } + return String(endTime.prefix(5)) + } + + var shortEndDate: String? { + guard let endDate else { return nil } + let parts = endDate.split(separator: "-") + guard parts.count == 3 else { return endDate } + return "\(parts[2]).\(parts[1])" + } +} + +struct WidgetSnapshot: Decodable { + let v: Int + let generatedAt: String + let locale: String + let orgSlug: String? + let mode: String? + let todayCount: Int + let overdueCount: Int + let upcomingCount: Int? + let tasks: [WidgetSnapshotTask] + + var isUpcoming: Bool { + mode == "upcoming" + } + + var activeCount: Int { + isUpcoming ? (upcomingCount ?? tasks.count) : todayCount + } + + static let appGroup = "group.com.handscream.taskview.app" + static let snapshotKey = "widgetSnapshot" + + static func load() -> WidgetSnapshot? { + guard let defaults = UserDefaults(suiteName: appGroup), + let raw = defaults.string(forKey: snapshotKey), + let data = raw.data(using: .utf8) else { return nil } + return try? JSONDecoder().decode(WidgetSnapshot.self, from: data) + } +} + +struct WidgetStrings { + let today: String + let upcoming: String + let empty: String + let overdue: String + let openApp: String + let moreFormat: String + + static let ru = WidgetStrings( + today: "Сегодня", + upcoming: "Ближайшие", + empty: "Нет задач на сегодня", + overdue: "Просрочено", + openApp: "Откройте TaskView", + moreFormat: "и ещё %d" + ) + + static let en = WidgetStrings( + today: "Today", + upcoming: "Upcoming", + empty: "No tasks for today", + overdue: "Overdue", + openApp: "Open TaskView", + moreFormat: "+%d more" + ) + + func more(_ count: Int) -> String { + String(format: moreFormat, count) + } + + static func forLocale(_ locale: String?) -> WidgetStrings { + let resolved = locale ?? Locale.preferredLanguages.first ?? "ru" + return resolved.hasPrefix("ru") ? ru : en + } +} diff --git a/web/package.json b/web/package.json index 0dd6bca..26eb52f 100644 --- a/web/package.json +++ b/web/package.json @@ -2,10 +2,10 @@ "name": "web-nuxt-ui", "private": true, "type": "module", - "version": "1.49.1", + "version": "1.49.2", "scripts": { "dev": "vite", - "build:packages": "pnpm --filter taskview-db-schemas build && pnpm --filter taskview-api build", + "build:packages": "pnpm --filter taskview-db-schemas build && pnpm --filter taskview-api build && pnpm --filter capacitor-widget-bridge build", "build": "pnpm run build:packages && pnpm run typecheck && vite build", "build-only": "vite build", "preview": "vite preview", @@ -58,6 +58,7 @@ "@vueuse/core": "^14.1.0", "arktype": "2.1.20", "axios": "1.13.5", + "capacitor-widget-bridge": "workspace:^", "centrifuge": "^5.5.3", "chart.js": "^4.5.1", "chartjs-plugin-annotation": "^3.1.0", diff --git a/web/src/composables/useLogout.ts b/web/src/composables/useLogout.ts index b7cce0e..d5778b7 100644 --- a/web/src/composables/useLogout.ts +++ b/web/src/composables/useLogout.ts @@ -1,3 +1,5 @@ +import { Capacitor } from '@capacitor/core' +import { WidgetBridge } from 'capacitor-widget-bridge' import $api from '@/helpers/axios' import { $ls, $tvApi } from '@/plugins/axios' import { usePushNotifications } from '@/composables/usePushNotifications' @@ -18,6 +20,11 @@ export async function useLogout() { if (result) { reset() + if (Capacitor.isNativePlatform()) { + await WidgetBridge.clearSnapshot().catch((err) => { + console.error('[Logout] Failed to clear widget snapshot:', err) + }) + } await $ls.invalidateTokens() return true } diff --git a/web/src/composables/useUpdater.ts b/web/src/composables/useUpdater.ts index 56875cf..80d7987 100644 --- a/web/src/composables/useUpdater.ts +++ b/web/src/composables/useUpdater.ts @@ -22,8 +22,6 @@ function isNewerVersion(server: string, current: string): boolean { } export async function useUpdater(canUpdate: boolean = false) { - // console.log(canUpdate); - // return; try { const BRANCH_MODE: 'prod' | 'dev' = (await $ls.getValue('update_loading')) === 'dev' ? 'dev' : 'prod' @@ -42,9 +40,11 @@ export async function useUpdater(canUpdate: boolean = false) { return } - console.log('currentVersion', currentVersion, 'updateInfo', last) + const effectiveVersion = currentVersion || APP_VERSION - if (!currentVersion || isNewerVersion(last.version, currentVersion) || (version && version?.version !== last.version)) { + console.log('currentVersion', effectiveVersion, 'updateInfo', last) + + if (isNewerVersion(last.version, effectiveVersion) || (version && version?.version !== last.version)) { console.log('[Update] Downloading new version:', last.version) version = await CapacitorUpdater.download({ diff --git a/web/src/composables/useWidgetDeepLink.ts b/web/src/composables/useWidgetDeepLink.ts new file mode 100644 index 0000000..0d81c62 --- /dev/null +++ b/web/src/composables/useWidgetDeepLink.ts @@ -0,0 +1,67 @@ +import { onScopeDispose, ref, watch } from 'vue' +import { useRouter } from 'vue-router' +import { Capacitor } from '@capacitor/core' +import { App } from '@capacitor/app' +import { useOrganizationStore } from '@/stores/organization.store' +import { useGoalsStore } from '@/stores/goals.store' + +const OPEN_PREFIX = 'taskview://open' + +let launchUrlHandled = false + +export function useWidgetDeepLink() { + if (!Capacitor.isNativePlatform()) return + + const router = useRouter() + const orgStore = useOrganizationStore() + const goalsStore = useGoalsStore() + + const pendingPath = ref(null) + + const extractPath = (url: string): string | null => { + if (!url.startsWith(OPEN_PREFIX)) return null + const match = url.match(/[?&]path=([^&]+)/) + return match ? decodeURIComponent(match[1]) : null + } + + const navigate = (url: string) => { + const path = extractPath(url) + console.log('[WidgetDeepLink] url:', url, '-> path:', path) + if (path) pendingPath.value = path + } + + watch( + () => [pendingPath.value, orgStore.initialized, goalsStore.initialized] as const, + ([path, orgReady, goalsReady]) => { + if (!path) return + console.log('[WidgetDeepLink] pending:', path, 'orgReady:', orgReady, 'goalsReady:', goalsReady) + if (!orgReady || !goalsReady) return + pendingPath.value = null + // If a task is already open, replace the history entry instead of pushing — + // otherwise consecutive widget taps stack task routes and closing one + // reopens the previous task. + const hasOpenTask = Boolean(router.currentRoute.value.params.taskId) + if (hasOpenTask) { + router.replace(path) + } else { + router.push(path) + } + }, + { immediate: true }, + ) + + let remove: (() => void) | undefined + App.addListener('appUrlOpen', ({ url }) => navigate(url)).then((handle) => { + remove = () => handle.remove() + }) + + if (!launchUrlHandled) { + launchUrlHandled = true + App.getLaunchUrl().then((result) => { + console.log('[WidgetDeepLink] launchUrl:', result?.url ?? 'none') + if (result?.url) navigate(result.url) + }) + } + + onScopeDispose(() => remove?.()) +} diff --git a/web/src/composables/useWidgetSnapshot.ts b/web/src/composables/useWidgetSnapshot.ts new file mode 100644 index 0000000..6233bd8 --- /dev/null +++ b/web/src/composables/useWidgetSnapshot.ts @@ -0,0 +1,74 @@ +import { watch } from 'vue' +import { useDebounceFn, useDateFormat } from '@vueuse/core' +import { Capacitor } from '@capacitor/core' +import { useI18n } from 'vue-i18n' +import { ALL_TASKS_LIST_ID } from 'taskview-api' +import { WidgetBridge, type WidgetSnapshot, type WidgetSnapshotTask } from 'capacitor-widget-bridge' +import { useBaseScreenStore } from '@/stores/base-screen.store' +import { useOrganizationStore } from '@/stores/organization.store' +import { useRefreshOnResume } from '@/composables/useRefreshOnResume' +import type { TaskItem } from '@/types/tasks.types' + +const MAX_WIDGET_TASKS = 10 + +export function useWidgetSnapshot() { + if (!Capacitor.isNativePlatform()) return + + const baseScreenStore = useBaseScreenStore() + const orgStore = useOrganizationStore() + const { locale } = useI18n() + + const todayYmd = () => useDateFormat(new Date(), 'YYYY-MM-DD').value + + const isOverdue = (task: TaskItem) => { + if (!task.endDate) return false + return useDateFormat(new Date(task.endDate), 'YYYY-MM-DD').value < todayYmd() + } + + const toSnapshotTask = (task: TaskItem): WidgetSnapshotTask => ({ + id: task.id, + title: task.description, + priority: task.priorityId, + overdue: isOverdue(task), + endTime: task.endTime, + endDate: task.endDate ? useDateFormat(new Date(task.endDate), 'YYYY-MM-DD').value : null, + path: `/${orgStore.currentOrgSlug}/${task.goalId}/${task.goalListId ?? ALL_TASKS_LIST_ID}/${task.id}`, + }) + + const buildSnapshot = (): WidgetSnapshot => { + const openToday = baseScreenStore.tasksToday.filter((task) => !task.complete) + const openUpcoming = baseScreenStore.tasksUpcoming + .filter((task) => !task.complete) + .sort((a, b) => new Date(a.endDate ?? 0).getTime() - new Date(b.endDate ?? 0).getTime()) + const mode = openToday.length === 0 && openUpcoming.length > 0 ? 'upcoming' : 'today' + const shownTasks = mode === 'today' ? openToday : openUpcoming + return { + v: 3, + generatedAt: new Date().toISOString(), + locale: locale.value, + orgSlug: orgStore.currentOrgSlug || null, + mode, + todayCount: openToday.length, + overdueCount: openToday.filter(isOverdue).length, + upcomingCount: openUpcoming.length, + tasks: shownTasks.slice(0, MAX_WIDGET_TASKS).map(toSnapshotTask), + } + } + + const writeSnapshot = useDebounceFn(async () => { + if (!baseScreenStore.wasCalled) return + await WidgetBridge.setSnapshot({ snapshot: JSON.stringify(buildSnapshot()) }).catch((err) => { + console.error('[WidgetSnapshot] Failed to write snapshot:', err) + }) + }, 500) + + watch( + () => [baseScreenStore.tasksToday, baseScreenStore.tasksUpcoming, orgStore.currentOrgSlug, locale.value] as const, + () => writeSnapshot(), + { deep: true }, + ) + + useRefreshOnResume(() => { + if (baseScreenStore.wasCalled) baseScreenStore.fetchAllState() + }) +} diff --git a/web/src/layouts/UserLayout.vue b/web/src/layouts/UserLayout.vue index 3d608bf..d1ee855 100644 --- a/web/src/layouts/UserLayout.vue +++ b/web/src/layouts/UserLayout.vue @@ -34,6 +34,8 @@ import DashboardSidebarSecond from '@/components/sidebars/DashboardSidebarSecond import SearchAll from '@/components/features/main/screen-main/parts/SearchAll.vue' import { useCentrifugo } from '@/composables/useCentrifugo' import { usePushNotifications } from '@/composables/usePushNotifications' +import { useWidgetSnapshot } from '@/composables/useWidgetSnapshot' +import { useWidgetDeepLink } from '@/composables/useWidgetDeepLink' import { useGoalsStore } from '@/stores/goals.store' import { useOrganizationStore } from '@/stores/organization.store' import { useTimeTrackingStore } from '@/stores/time-tracking.store' @@ -49,6 +51,9 @@ const orgStore = useOrganizationStore() const timeTrackingStore = useTimeTrackingStore() const uiPrefsStore = useUiPreferencesStore() +useWidgetSnapshot() +useWidgetDeepLink() + watch( () => timeTrackingStore.lastError, (err) => {