feat: 添加原生通知监听功能,自动同步通知和交易列表
This commit is contained in:
@@ -17,6 +17,17 @@ android {
|
|||||||
ignoreAssetsPattern '!.svn:!.git:!.ds_store:!*.scc:.*:!CVS:!thumbs.db:!picasa.ini:!*~'
|
ignoreAssetsPattern '!.svn:!.git:!.ds_store:!*.scc:.*:!CVS:!thumbs.db:!picasa.ini:!*~'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// <20><>ʽͳһ Java / Kotlin <20>ı<EFBFBD><C4B1><EFBFBD>Ŀ<EFBFBD><C4BF><EFBFBD>汾<EFBFBD><E6B1BE><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> Capacitor <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>һ<EFBFBD>£<EFBFBD>Java 21<32><31>
|
||||||
|
compileOptions {
|
||||||
|
sourceCompatibility JavaVersion.VERSION_21
|
||||||
|
targetCompatibility JavaVersion.VERSION_21
|
||||||
|
}
|
||||||
|
|
||||||
|
kotlinOptions {
|
||||||
|
jvmTarget = '21'
|
||||||
|
}
|
||||||
|
|
||||||
buildTypes {
|
buildTypes {
|
||||||
release {
|
release {
|
||||||
minifyEnabled false
|
minifyEnabled false
|
||||||
|
|||||||
@@ -2,5 +2,9 @@
|
|||||||
{
|
{
|
||||||
"pkg": "@capacitor-community/sqlite",
|
"pkg": "@capacitor-community/sqlite",
|
||||||
"classpath": "com.getcapacitor.community.database.sqlite.CapacitorSQLitePlugin"
|
"classpath": "com.getcapacitor.community.database.sqlite.CapacitorSQLitePlugin"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"pkg": "@capacitor/app",
|
||||||
|
"classpath": "com.capacitorjs.plugins.app.AppPlugin"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,17 +1,46 @@
|
|||||||
package com.echo.app.notification
|
package com.echo.app.notification
|
||||||
|
|
||||||
|
import android.content.BroadcastReceiver
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.IntentFilter
|
||||||
import androidx.core.app.NotificationManagerCompat
|
import androidx.core.app.NotificationManagerCompat
|
||||||
import com.getcapacitor.JSArray
|
import com.getcapacitor.JSArray
|
||||||
import com.getcapacitor.JSObject
|
import com.getcapacitor.JSObject
|
||||||
import com.getcapacitor.Plugin
|
import com.getcapacitor.Plugin
|
||||||
import com.getcapacitor.PluginCall
|
import com.getcapacitor.PluginCall
|
||||||
import com.getcapacitor.annotation.CapacitorPlugin
|
|
||||||
import com.getcapacitor.PluginMethod
|
import com.getcapacitor.PluginMethod
|
||||||
|
import com.getcapacitor.annotation.CapacitorPlugin
|
||||||
|
|
||||||
// Capacitor <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ǰ<EFBFBD>˱<EFBFBD>¶֪ͨȨ<D6AA><EFBFBD><DEBC><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ж<EFBFBD>д<EFBFBD><EFBFBD>??
|
// Capacitor <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ǰ<EFBFBD>˱<EFBFBD>¶֪ͨȨ<D6AA><EFBFBD><DEBC>顢<EFBFBD><EFBFBD><EFBFBD>ж<EFBFBD>ȡ<EFBFBD><EFBFBD><EFBFBD>Լ<EFBFBD>֪ͨ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>¼<EFBFBD>
|
||||||
@CapacitorPlugin(name = "NotificationBridge")
|
@CapacitorPlugin(name = "NotificationBridge")
|
||||||
class NotificationBridgePlugin : Plugin() {
|
class NotificationBridgePlugin : Plugin() {
|
||||||
|
|
||||||
|
private var notificationPostedReceiver: BroadcastReceiver? = null
|
||||||
|
|
||||||
|
override fun load() {
|
||||||
|
super.load()
|
||||||
|
|
||||||
|
// <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> NotificationBridgeService <20><>Ӧ<EFBFBD><D3A6><EFBFBD>ڹ㲥<DAB9><E3B2A5><EFBFBD><EFBFBD>ת<EFBFBD><D7AA>Ϊ JS <20>¼<EFBFBD>
|
||||||
|
val filter = IntentFilter("com.echo.app.NOTIFICATION_POSTED")
|
||||||
|
notificationPostedReceiver = object : BroadcastReceiver() {
|
||||||
|
override fun onReceive(context: Context, intent: Intent) {
|
||||||
|
val payload = JSObject()
|
||||||
|
payload.put("id", intent.getStringExtra("id"))
|
||||||
|
notifyListeners("notificationPosted", payload)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
context.registerReceiver(notificationPostedReceiver, filter)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun handleOnDestroy() {
|
||||||
|
notificationPostedReceiver?.let { receiver ->
|
||||||
|
context.unregisterReceiver(receiver)
|
||||||
|
}
|
||||||
|
notificationPostedReceiver = null
|
||||||
|
super.handleOnDestroy()
|
||||||
|
}
|
||||||
|
|
||||||
@PluginMethod
|
@PluginMethod
|
||||||
fun hasPermission(call: PluginCall) {
|
fun hasPermission(call: PluginCall) {
|
||||||
val enabled = NotificationManagerCompat.getEnabledListenerPackages(context)
|
val enabled = NotificationManagerCompat.getEnabledListenerPackages(context)
|
||||||
@@ -23,7 +52,7 @@ class NotificationBridgePlugin : Plugin() {
|
|||||||
|
|
||||||
@PluginMethod
|
@PluginMethod
|
||||||
fun requestPermission(call: PluginCall) {
|
fun requestPermission(call: PluginCall) {
|
||||||
// <20><><EFBFBD><EFBFBD>ֻ<EFBFBD><EFBFBD><EFBFBD>ص<EFBFBD>ǰ״̬<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ת<EFBFBD><EFBFBD><EFBFBD><EFBFBD>ҳ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> App <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>û<EFBFBD>
|
// <20><>ǰ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>״̬<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>û<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ǰ<EFBFBD><EFBFBD><EFBFBD><EFBFBD>ת<EFBFBD><EFBFBD>ϵͳ<EFBFBD><EFBFBD><EFBFBD><EFBFBD>
|
||||||
hasPermission(call)
|
hasPermission(call)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package com.echo.app.notification
|
package com.echo.app.notification
|
||||||
|
|
||||||
import android.app.Notification
|
import android.app.Notification
|
||||||
|
import android.content.Intent
|
||||||
import android.service.notification.NotificationListenerService
|
import android.service.notification.NotificationListenerService
|
||||||
import android.service.notification.StatusBarNotification
|
import android.service.notification.StatusBarNotification
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
@@ -9,7 +10,7 @@ import java.util.Locale
|
|||||||
import java.util.TimeZone
|
import java.util.TimeZone
|
||||||
import org.json.JSONObject
|
import org.json.JSONObject
|
||||||
|
|
||||||
// Android ֪ͨ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ϵͳ֪ͨת<EFBFBD><EFBFBD><EFBFBD><EFBFBD> Echo <20><><EFBFBD><EFBFBD><EFBFBD>ѵ<EFBFBD> JSON <20><>д<EFBFBD>뱾<EFBFBD>ض<EFBFBD><D8B6><EFBFBD>
|
// Android notification listener: persist notifications to local storage and notify app via broadcast
|
||||||
class NotificationBridgeService : NotificationListenerService() {
|
class NotificationBridgeService : NotificationListenerService() {
|
||||||
override fun onNotificationPosted(sbn: StatusBarNotification) {
|
override fun onNotificationPosted(sbn: StatusBarNotification) {
|
||||||
val notification = sbn.notification ?: return
|
val notification = sbn.notification ?: return
|
||||||
@@ -34,14 +35,22 @@ class NotificationBridgeService : NotificationListenerService() {
|
|||||||
appendValue(subText)
|
appendValue(subText)
|
||||||
lines?.forEach { appendValue(it) }
|
lines?.forEach { appendValue(it) }
|
||||||
|
|
||||||
|
val id = sbn.key ?: sbn.id.toString()
|
||||||
val payload = JSONObject()
|
val payload = JSONObject()
|
||||||
payload.put("id", sbn.key ?: sbn.id.toString())
|
payload.put("id", id)
|
||||||
payload.put("channel", title ?: sbn.packageName)
|
payload.put("channel", title ?: sbn.packageName)
|
||||||
payload.put("text", if (builder.isNotEmpty()) builder.toString() else text.orEmpty())
|
payload.put("text", if (builder.isNotEmpty()) builder.toString() else text.orEmpty())
|
||||||
payload.put("createdAt", isoNow())
|
payload.put("createdAt", isoNow())
|
||||||
payload.put("packageName", sbn.packageName)
|
payload.put("packageName", sbn.packageName)
|
||||||
|
|
||||||
|
// Persist notification in local queue for JS side to fetch via NotificationBridge
|
||||||
NotificationStorage.add(applicationContext, payload)
|
NotificationStorage.add(applicationContext, payload)
|
||||||
|
|
||||||
|
// Broadcast an in-app event so the Capacitor plugin can notify JS listeners
|
||||||
|
val intent = Intent("com.echo.app.NOTIFICATION_POSTED")
|
||||||
|
intent.setPackage(packageName)
|
||||||
|
intent.putExtra("id", id)
|
||||||
|
sendBroadcast(intent)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onNotificationRemoved(sbn: StatusBarNotification) {
|
override fun onNotificationRemoved(sbn: StatusBarNotification) {
|
||||||
|
|||||||
1161
package-lock.json
generated
1161
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -10,6 +10,8 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@capacitor-community/sqlite": "^5.7.2",
|
"@capacitor-community/sqlite": "^5.7.2",
|
||||||
|
"@capacitor/android": "^5.7.8",
|
||||||
|
"@capacitor/app": "^5.0.0",
|
||||||
"@capacitor/core": "^5.7.8",
|
"@capacitor/core": "^5.7.8",
|
||||||
"@phosphor-icons/web": "^2.1.2",
|
"@phosphor-icons/web": "^2.1.2",
|
||||||
"jeep-sqlite": "^2.7.1",
|
"jeep-sqlite": "^2.7.1",
|
||||||
@@ -21,10 +23,12 @@
|
|||||||
"vue-router": "^4.6.3"
|
"vue-router": "^4.6.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@capacitor/cli": "^7.4.4",
|
||||||
"@vitejs/plugin-vue": "^6.0.1",
|
"@vitejs/plugin-vue": "^6.0.1",
|
||||||
"autoprefixer": "^10.4.17",
|
"autoprefixer": "^10.4.17",
|
||||||
"postcss": "^8.4.35",
|
"postcss": "^8.4.35",
|
||||||
"tailwindcss": "^3.4.1",
|
"tailwindcss": "^3.4.1",
|
||||||
|
"typescript": "^5.9.3",
|
||||||
"vite": "^7.2.4"
|
"vite": "^7.2.4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ const ruleSet = ref([])
|
|||||||
const nativeBridgeReady = isNativeNotificationBridgeAvailable()
|
const nativeBridgeReady = isNativeNotificationBridgeAvailable()
|
||||||
let permissionChecked = false
|
let permissionChecked = false
|
||||||
let bootstrapped = false
|
let bootstrapped = false
|
||||||
|
let nativeNotificationListener = null
|
||||||
|
|
||||||
const ensureRulesReady = async () => {
|
const ensureRulesReady = async () => {
|
||||||
if (ruleSet.value.length) return ruleSet.value
|
if (ruleSet.value.length) return ruleSet.value
|
||||||
@@ -119,6 +120,19 @@ export const useTransactionEntry = () => {
|
|||||||
if (!bootstrapped) {
|
if (!bootstrapped) {
|
||||||
bootstrapped = true
|
bootstrapped = true
|
||||||
syncNotifications()
|
syncNotifications()
|
||||||
|
|
||||||
|
// 原生环境:监听 NotificationBridgePlugin 推送的 notificationPosted 事件,做到“通知一到就同步一次”
|
||||||
|
if (nativeBridgeReady && !nativeNotificationListener && typeof NotificationBridge?.addListener === 'function') {
|
||||||
|
NotificationBridge.addListener('notificationPosted', async () => {
|
||||||
|
await syncNotifications()
|
||||||
|
})
|
||||||
|
.then((handle) => {
|
||||||
|
nativeNotificationListener = handle
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.warn('[notifications] 无法注册 notificationPosted 监听', error)
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -1,15 +1,39 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { computed } from 'vue'
|
import { computed, onMounted, onBeforeUnmount } from 'vue'
|
||||||
import { storeToRefs } from 'pinia'
|
import { storeToRefs } from 'pinia'
|
||||||
|
import { App } from '@capacitor/app'
|
||||||
import { useTransactionStore } from '../stores/transactions'
|
import { useTransactionStore } from '../stores/transactions'
|
||||||
import { useTransactionEntry } from '../composables/useTransactionEntry'
|
import { useTransactionEntry } from '../composables/useTransactionEntry'
|
||||||
|
|
||||||
const transactionStore = useTransactionStore()
|
const transactionStore = useTransactionStore()
|
||||||
const { totalIncome, totalExpense, todaysIncome, todaysExpense, latestTransactions } =
|
const { totalIncome, totalExpense, todaysIncome, todaysExpense, latestTransactions } =
|
||||||
storeToRefs(transactionStore)
|
storeToRefs(transactionStore)
|
||||||
const { notifications, confirmNotification, dismissNotification, processingId } =
|
const { notifications, confirmNotification, dismissNotification, processingId, syncNotifications } =
|
||||||
useTransactionEntry()
|
useTransactionEntry()
|
||||||
|
|
||||||
|
let appStateListener = null
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
try {
|
||||||
|
// App 从后台回到前台时,自动同步一次通知和交易列表
|
||||||
|
appStateListener = await App.addListener('appStateChange', async ({ isActive }) => {
|
||||||
|
if (isActive) {
|
||||||
|
await syncNotifications()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
// Web 环境或插件不可用时忽略
|
||||||
|
console.warn('[notifications] appStateChange listener failed', error)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
if (appStateListener && typeof appStateListener.remove === 'function') {
|
||||||
|
appStateListener.remove()
|
||||||
|
}
|
||||||
|
appStateListener = null
|
||||||
|
})
|
||||||
|
|
||||||
const emit = defineEmits(['changeTab'])
|
const emit = defineEmits(['changeTab'])
|
||||||
const monthlyBudget = 12000
|
const monthlyBudget = 12000
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user