mirror of
https://github.com/Gimanh/taskview-community.git
synced 2026-09-11 21:38:56 +00:00
Merge pull request #80 from Gimanh/feat/mobile-widgets
feat: mobile widgets
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
android/build/
|
||||
android/.gradle/
|
||||
android/local.properties
|
||||
.swiftpm/
|
||||
ios/.build/
|
||||
DerivedData/
|
||||
@@ -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")
|
||||
]
|
||||
)
|
||||
@@ -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"
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android" />
|
||||
+48
@@ -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);
|
||||
}
|
||||
}
|
||||
+44
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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<void>
|
||||
clearSnapshot(): Promise<void>
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import { registerPlugin } from '@capacitor/core'
|
||||
import type { WidgetBridgePlugin } from './definitions'
|
||||
|
||||
export const WidgetBridge = registerPlugin<WidgetBridgePlugin>('WidgetBridge')
|
||||
|
||||
export type {
|
||||
WidgetBridgePlugin,
|
||||
WidgetSnapshot,
|
||||
WidgetSnapshotMode,
|
||||
WidgetSnapshotTask,
|
||||
SetSnapshotOptions,
|
||||
} from './definitions'
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
@@ -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')
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -29,6 +29,24 @@
|
||||
|
||||
</activity>
|
||||
|
||||
<receiver
|
||||
android:name=".widget.TodayWidgetProvider"
|
||||
android:exported="false"
|
||||
android:label="@string/widget_today_title">
|
||||
<intent-filter>
|
||||
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
|
||||
<action android:name="tech.taskview.widget.UPDATE" />
|
||||
</intent-filter>
|
||||
<meta-data
|
||||
android:name="android.appwidget.provider"
|
||||
android:resource="@xml/widget_today_info" />
|
||||
</receiver>
|
||||
|
||||
<service
|
||||
android:name=".widget.TodayWidgetService"
|
||||
android:permission="android.permission.BIND_REMOTEVIEWS"
|
||||
android:exported="false" />
|
||||
|
||||
<provider
|
||||
android:name="androidx.core.content.FileProvider"
|
||||
android:authorities="${applicationId}.fileprovider"
|
||||
|
||||
+141
@@ -0,0 +1,141 @@
|
||||
package com.handscream.taskview.app.widget;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.view.View;
|
||||
import android.widget.RemoteViews;
|
||||
import android.widget.RemoteViewsService;
|
||||
|
||||
import com.handscream.taskview.app.R;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import tech.taskview.plugins.widgetbridge.WidgetBridgePlugin;
|
||||
|
||||
public class TodayWidgetFactory implements RemoteViewsService.RemoteViewsFactory {
|
||||
|
||||
private final Context context;
|
||||
private final List<JSONObject> 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;
|
||||
}
|
||||
}
|
||||
+90
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
+12
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
<solid android:color="@color/widget_accent" />
|
||||
<corners android:radius="999dp" />
|
||||
</shape>
|
||||
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
<solid android:color="@color/widget_background" />
|
||||
<corners android:radius="16dp" />
|
||||
</shape>
|
||||
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="oval">
|
||||
<solid android:color="@android:color/transparent" />
|
||||
<stroke
|
||||
android:width="1.5dp"
|
||||
android:color="@color/widget_priority_high" />
|
||||
</shape>
|
||||
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="oval">
|
||||
<solid android:color="@android:color/transparent" />
|
||||
<stroke
|
||||
android:width="1.5dp"
|
||||
android:color="@color/widget_priority_low" />
|
||||
</shape>
|
||||
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="oval">
|
||||
<solid android:color="@android:color/transparent" />
|
||||
<stroke
|
||||
android:width="1.5dp"
|
||||
android:color="@color/widget_priority_medium" />
|
||||
</shape>
|
||||
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
<solid android:color="@color/widget_header_background" />
|
||||
<corners
|
||||
android:topLeftRadius="16dp"
|
||||
android:topRightRadius="16dp" />
|
||||
</shape>
|
||||
@@ -0,0 +1,76 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
android:background="@drawable/widget_bg">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/widget_today_header"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center_vertical"
|
||||
android:background="@drawable/widget_header_bg"
|
||||
android:paddingStart="16dp"
|
||||
android:paddingEnd="16dp"
|
||||
android:paddingTop="12dp"
|
||||
android:paddingBottom="12dp">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/widget_today_logo"
|
||||
android:layout_width="20dp"
|
||||
android:layout_height="20dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:src="@mipmap/ic_launcher_round"
|
||||
android:importantForAccessibility="no" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/widget_today_title"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="@string/widget_today_title"
|
||||
android:textColor="@color/widget_text_primary"
|
||||
android:textSize="16sp"
|
||||
android:textStyle="bold"
|
||||
android:maxLines="1" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/widget_today_count"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@drawable/widget_badge_bg"
|
||||
android:paddingStart="8dp"
|
||||
android:paddingEnd="8dp"
|
||||
android:paddingTop="3dp"
|
||||
android:paddingBottom="3dp"
|
||||
android:textColor="@android:color/white"
|
||||
android:textSize="12sp"
|
||||
android:textStyle="bold" />
|
||||
</LinearLayout>
|
||||
|
||||
<FrameLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1">
|
||||
|
||||
<ListView
|
||||
android:id="@+id/widget_today_list"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:divider="@color/widget_divider"
|
||||
android:dividerHeight="0.5dp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/widget_today_empty"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:gravity="center"
|
||||
android:paddingStart="16dp"
|
||||
android:paddingEnd="16dp"
|
||||
android:text="@string/widget_today_empty"
|
||||
android:textColor="@color/widget_text_secondary"
|
||||
android:textSize="13sp" />
|
||||
</FrameLayout>
|
||||
</LinearLayout>
|
||||
@@ -0,0 +1,39 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/widget_item_root"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center_vertical"
|
||||
android:paddingStart="8dp"
|
||||
android:paddingEnd="16dp"
|
||||
android:paddingTop="4dp"
|
||||
android:paddingBottom="4dp">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/widget_item_checkbox"
|
||||
android:layout_width="32dp"
|
||||
android:layout_height="32dp"
|
||||
android:padding="8dp"
|
||||
android:src="@drawable/widget_checkbox_low"
|
||||
android:importantForAccessibility="no" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/widget_item_title"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:layout_marginStart="2dp"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
android:textColor="@color/widget_text_primary"
|
||||
android:textSize="14sp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/widget_item_meta"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
android:textColor="@color/widget_text_secondary"
|
||||
android:textSize="11sp" />
|
||||
</LinearLayout>
|
||||
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="widget_background">#18181B</color>
|
||||
<color name="widget_header_background">#27272A</color>
|
||||
<color name="widget_divider">#3F3F46</color>
|
||||
<color name="widget_text_primary">#FAFAFA</color>
|
||||
<color name="widget_text_secondary">#A1A1AA</color>
|
||||
</resources>
|
||||
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="widget_today_title">Сегодня</string>
|
||||
<string name="widget_upcoming_title">Ближайшие</string>
|
||||
<string name="widget_today_empty">Нет задач на сегодня</string>
|
||||
<string name="widget_overdue">Просрочено</string>
|
||||
<string name="widget_today_description">Ваши задачи на сегодня</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="widget_today_title">Today</string>
|
||||
<string name="widget_upcoming_title">Upcoming</string>
|
||||
<string name="widget_today_empty">No tasks for today</string>
|
||||
<string name="widget_overdue">Overdue</string>
|
||||
<string name="widget_today_description">Your tasks for today at a glance</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="widget_background">#FFFFFF</color>
|
||||
<color name="widget_header_background">#F4F4F5</color>
|
||||
<color name="widget_divider">#E4E4E7</color>
|
||||
<color name="widget_text_primary">#18181B</color>
|
||||
<color name="widget_text_secondary">#71717A</color>
|
||||
<color name="widget_accent">#16A34A</color>
|
||||
<color name="widget_overdue">#FF1744</color>
|
||||
<color name="widget_priority_low">#38D681</color>
|
||||
<color name="widget_priority_medium">#FF9100</color>
|
||||
<color name="widget_priority_high">#FF1744</color>
|
||||
</resources>
|
||||
@@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:minWidth="250dp"
|
||||
android:minHeight="110dp"
|
||||
android:minResizeWidth="110dp"
|
||||
android:minResizeHeight="70dp"
|
||||
android:targetCellWidth="4"
|
||||
android:targetCellHeight="2"
|
||||
android:updatePeriodMillis="1800000"
|
||||
android:resizeMode="horizontal|vertical"
|
||||
android:widgetCategory="home_screen"
|
||||
android:initialLayout="@layout/widget_today"
|
||||
android:previewLayout="@layout/widget_today"
|
||||
android:description="@string/widget_today_description" />
|
||||
@@ -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')
|
||||
|
||||
@@ -25,6 +25,9 @@ const config: CapacitorConfig = {
|
||||
CapacitorHttp: {
|
||||
enabled: true,
|
||||
},
|
||||
WidgetBridge: {
|
||||
appGroup: 'group.com.handscream.taskview.app',
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -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 = "<group>"; };
|
||||
41EECE9262690D5E2EF23CE9 /* Assets.xcassets */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||
4822ABF5AC8CD3030A5CE020 /* TodayWidget.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TodayWidget.swift; sourceTree = "<group>"; };
|
||||
50379B222058CBB4000EE86E /* capacitor.config.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = capacitor.config.json; sourceTree = "<group>"; };
|
||||
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 = "<group>"; };
|
||||
@@ -28,9 +63,15 @@
|
||||
504EC3111FED79650016851F /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
|
||||
504EC3131FED79650016851F /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
50B271D01FEDC1A000F3C39B /* public */ = {isa = PBXFileReference; lastKnownFileType = folder; path = public; sourceTree = "<group>"; };
|
||||
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 = "<group>"; };
|
||||
AB10F00D00000000000000A1 /* OpenTaskIntent.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = OpenTaskIntent.swift; sourceTree = "<group>"; };
|
||||
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 = "<group>"; };
|
||||
AAA632B02F69F429007C705D /* App.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = App.entitlements; sourceTree = "<group>"; };
|
||||
AAA632B12F6BB598007C705D /* GoogleService-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = "<group>"; };
|
||||
EB7495A9A40AB2365CC0624E /* Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
F1EEEB83D95DC32C9728EAC4 /* TaskViewWidget.entitlements */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.entitlements; path = TaskViewWidget.entitlements; sourceTree = "<group>"; };
|
||||
/* 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 = "<group>";
|
||||
};
|
||||
504EC2FB1FED79650016851F = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@@ -52,6 +109,8 @@
|
||||
958DCC722DB07C7200EA8C5F /* debug.xcconfig */,
|
||||
504EC3061FED79650016851F /* App */,
|
||||
504EC3051FED79650016851F /* Products */,
|
||||
4961C8EBB434EDD15B55395B /* Frameworks */,
|
||||
79D12E01B1FBD77844324865 /* TaskViewWidget */,
|
||||
);
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
@@ -59,6 +118,7 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
504EC3041FED79650016851F /* App.app */,
|
||||
7F4057F5AC9B7A7AC7E70240 /* TaskViewWidget.appex */,
|
||||
);
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
@@ -79,9 +139,49 @@
|
||||
path = App;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
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 = "<group>";
|
||||
};
|
||||
7E3EA782F4406B2B6D7A0121 /* iOS */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
029A385E0A8D9B1FCBD9CC75 /* Foundation.framework */,
|
||||
);
|
||||
name = iOS;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* 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 */
|
||||
|
||||
@@ -4,5 +4,9 @@
|
||||
<dict>
|
||||
<key>aps-environment</key>
|
||||
<string>development</string>
|
||||
<key>com.apple.security.application-groups</key>
|
||||
<array>
|
||||
<string>group.com.handscream.taskview.app</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -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")
|
||||
]
|
||||
)
|
||||
]
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "logo.png",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 10 KiB |
@@ -0,0 +1,39 @@
|
||||
import Foundation
|
||||
import AppIntents
|
||||
#if !WIDGET_EXTENSION
|
||||
import UIKit
|
||||
#endif
|
||||
|
||||
@available(iOS 18.0, *)
|
||||
struct OpenTaskIntent: AppIntent {
|
||||
static let title: LocalizedStringResource = "Open Task"
|
||||
static let isDiscoverable = false
|
||||
static let openAppWhenRun = true
|
||||
|
||||
@Parameter(title: "URL")
|
||||
var urlString: String
|
||||
|
||||
init() {
|
||||
urlString = "taskview://open"
|
||||
}
|
||||
|
||||
init(urlString: String) {
|
||||
self.urlString = urlString
|
||||
}
|
||||
|
||||
private var resolvedURL: URL {
|
||||
URL(string: urlString) ?? URL(string: "taskview://open")!
|
||||
}
|
||||
|
||||
#if WIDGET_EXTENSION
|
||||
func perform() async throws -> some IntentResult & OpensIntent {
|
||||
.result(opensIntent: OpenURLIntent(resolvedURL))
|
||||
}
|
||||
#else
|
||||
@MainActor
|
||||
func perform() async throws -> some IntentResult {
|
||||
await UIApplication.shared.open(resolvedURL)
|
||||
return .result()
|
||||
}
|
||||
#endif
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.security.application-groups</key>
|
||||
<array>
|
||||
<string>group.com.handscream.taskview.app</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,9 @@
|
||||
import WidgetKit
|
||||
import SwiftUI
|
||||
|
||||
@main
|
||||
struct TaskViewWidgetBundle: WidgetBundle {
|
||||
var body: some Widget {
|
||||
TodayWidget()
|
||||
}
|
||||
}
|
||||
@@ -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<TodayEntry>) -> 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
+3
-2
@@ -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",
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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<string | null>(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?.())
|
||||
}
|
||||
@@ -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()
|
||||
})
|
||||
}
|
||||
@@ -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) => {
|
||||
|
||||
Reference in New Issue
Block a user