Browse Source
## 🐛 Bug Fix - App was crashing with NullPointerException due to missing plugin registration - DailyNotification plugin was not registered in capacitor.plugins.json - App was trying to access plugin methods that didn't exist ## ✅ Solution - Added DailyNotification plugin registration to capacitor.plugins.json - Plugin now properly registered with class: com.timesafari.dailynotification.DailyNotificationPlugin - App can now access plugin methods without crashing ## 🔧 Technical Details - Fixed capacitor.plugins.json to include plugin registration - Rebuilt and reinstalled app with proper plugin integration - App now loads Vue 3 interface with header navigation The app should now display the proper Vue 3 interface with: - Header navigation (Home, Schedule, Notifications, Status, History, Logs, Settings) - Copy to clipboard functionality in LogsView - All plugin methods working correctlymaster
5 changed files with 650 additions and 477 deletions
@ -1,476 +0,0 @@ |
|||||
<!DOCTYPE html> |
|
||||
<html lang="en"> |
|
||||
<head> |
|
||||
<meta charset="UTF-8"> |
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
|
||||
<title>TimeSafari Daily Notification - Android Test</title> |
|
||||
<style> |
|
||||
body { |
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; |
|
||||
margin: 0; |
|
||||
padding: 20px; |
|
||||
background-color: #f5f5f5; |
|
||||
} |
|
||||
.container { |
|
||||
max-width: 800px; |
|
||||
margin: 0 auto; |
|
||||
background: white; |
|
||||
border-radius: 12px; |
|
||||
padding: 20px; |
|
||||
box-shadow: 0 2px 10px rgba(0,0,0,0.1); |
|
||||
} |
|
||||
h1 { |
|
||||
color: #333; |
|
||||
text-align: center; |
|
||||
margin-bottom: 30px; |
|
||||
} |
|
||||
.ui-section { |
|
||||
margin-bottom: 30px; |
|
||||
padding: 20px; |
|
||||
border: 1px solid #e0e0e0; |
|
||||
border-radius: 8px; |
|
||||
background: #fafafa; |
|
||||
} |
|
||||
.ui-section h3 { |
|
||||
margin: 0 0 15px 0; |
|
||||
color: #1976d2; |
|
||||
border-bottom: 2px solid #1976d2; |
|
||||
padding-bottom: 8px; |
|
||||
} |
|
||||
.status { |
|
||||
background: #e3f2fd; |
|
||||
padding: 15px; |
|
||||
border-radius: 8px; |
|
||||
margin-bottom: 20px; |
|
||||
text-align: center; |
|
||||
font-weight: bold; |
|
||||
color: #1976d2; |
|
||||
} |
|
||||
.button-grid { |
|
||||
display: grid; |
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); |
|
||||
gap: 10px; |
|
||||
margin-bottom: 20px; |
|
||||
} |
|
||||
button { |
|
||||
background: #1976d2; |
|
||||
color: white; |
|
||||
border: none; |
|
||||
padding: 12px 16px; |
|
||||
border-radius: 6px; |
|
||||
cursor: pointer; |
|
||||
font-size: 14px; |
|
||||
transition: background-color 0.2s; |
|
||||
} |
|
||||
button:hover { |
|
||||
background: #1565c0; |
|
||||
} |
|
||||
button:disabled { |
|
||||
background: #ccc; |
|
||||
cursor: not-allowed; |
|
||||
} |
|
||||
.btn-secondary { |
|
||||
background: #f5f5f5; |
|
||||
color: #333; |
|
||||
border: 1px solid #ddd; |
|
||||
} |
|
||||
.btn-secondary:hover { |
|
||||
background: #e0e0e0; |
|
||||
} |
|
||||
.btn-tertiary { |
|
||||
background: transparent; |
|
||||
color: #666; |
|
||||
} |
|
||||
.btn-tertiary:hover { |
|
||||
background: #f5f5f5; |
|
||||
} |
|
||||
.log-container { |
|
||||
background: #f8f9fa; |
|
||||
border: 1px solid #dee2e6; |
|
||||
border-radius: 6px; |
|
||||
padding: 15px; |
|
||||
height: 300px; |
|
||||
overflow-y: auto; |
|
||||
font-family: 'Courier New', monospace; |
|
||||
font-size: 12px; |
|
||||
} |
|
||||
.timestamp { |
|
||||
color: #666; |
|
||||
font-weight: bold; |
|
||||
} |
|
||||
pre { |
|
||||
background: #e9ecef; |
|
||||
padding: 8px; |
|
||||
border-radius: 4px; |
|
||||
margin: 5px 0; |
|
||||
overflow-x: auto; |
|
||||
} |
|
||||
.clear-button { |
|
||||
background: #dc3545; |
|
||||
margin-top: 10px; |
|
||||
width: 100%; |
|
||||
} |
|
||||
.clear-button:hover { |
|
||||
background: #c82333; |
|
||||
} |
|
||||
.permission-status { |
|
||||
padding: 16px; |
|
||||
border-radius: 8px; |
|
||||
margin: 16px 0; |
|
||||
} |
|
||||
.status-granted { |
|
||||
background: #e8f5e8; |
|
||||
border: 1px solid #4caf50; |
|
||||
} |
|
||||
.status-denied { |
|
||||
background: #ffebee; |
|
||||
border: 1px solid #f44336; |
|
||||
} |
|
||||
.status-indicator { |
|
||||
display: flex; |
|
||||
align-items: center; |
|
||||
gap: 8px; |
|
||||
margin-bottom: 12px; |
|
||||
} |
|
||||
.status-icon { |
|
||||
font-size: 20px; |
|
||||
font-weight: bold; |
|
||||
} |
|
||||
.permission-details { |
|
||||
margin: 12px 0; |
|
||||
} |
|
||||
.permission-item { |
|
||||
display: flex; |
|
||||
justify-content: space-between; |
|
||||
margin: 8px 0; |
|
||||
} |
|
||||
.granted { |
|
||||
color: #4caf50; |
|
||||
font-weight: bold; |
|
||||
} |
|
||||
.denied { |
|
||||
color: #f44336; |
|
||||
font-weight: bold; |
|
||||
} |
|
||||
.settings-panel { |
|
||||
background: white; |
|
||||
border-radius: 8px; |
|
||||
padding: 20px; |
|
||||
margin: 16px 0; |
|
||||
} |
|
||||
.setting-group { |
|
||||
margin: 20px 0; |
|
||||
} |
|
||||
.setting-label { |
|
||||
display: flex; |
|
||||
align-items: center; |
|
||||
gap: 8px; |
|
||||
font-weight: 500; |
|
||||
color: #333; |
|
||||
} |
|
||||
.checkbox-group { |
|
||||
display: grid; |
|
||||
grid-template-columns: 1fr 1fr; |
|
||||
gap: 12px; |
|
||||
margin-top: 8px; |
|
||||
} |
|
||||
.preference-grid { |
|
||||
display: grid; |
|
||||
grid-template-columns: 1fr 1fr 1fr; |
|
||||
gap: 16px; |
|
||||
margin-top: 8px; |
|
||||
} |
|
||||
.setting-actions { |
|
||||
display: flex; |
|
||||
gap: 12px; |
|
||||
margin-top: 24px; |
|
||||
} |
|
||||
.status-dashboard { |
|
||||
background: white; |
|
||||
border-radius: 8px; |
|
||||
padding: 20px; |
|
||||
margin: 16px 0; |
|
||||
} |
|
||||
.status-grid { |
|
||||
display: grid; |
|
||||
grid-template-columns: 1fr 1fr; |
|
||||
gap: 16px; |
|
||||
margin: 20px 0; |
|
||||
} |
|
||||
.status-item { |
|
||||
padding: 16px; |
|
||||
background: #f8f9fa; |
|
||||
border-radius: 8px; |
|
||||
} |
|
||||
.status-label { |
|
||||
font-size: 14px; |
|
||||
color: #666; |
|
||||
margin-bottom: 8px; |
|
||||
} |
|
||||
.status-value { |
|
||||
font-size: 18px; |
|
||||
font-weight: bold; |
|
||||
color: #333; |
|
||||
} |
|
||||
.status-value.active { |
|
||||
color: #4caf50; |
|
||||
} |
|
||||
.status-value.warning { |
|
||||
color: #ff9800; |
|
||||
} |
|
||||
.status-value.error { |
|
||||
color: #f44336; |
|
||||
} |
|
||||
.performance-metrics { |
|
||||
margin: 24px 0; |
|
||||
padding: 16px; |
|
||||
background: #f8f9fa; |
|
||||
border-radius: 8px; |
|
||||
} |
|
||||
.metrics-grid { |
|
||||
display: grid; |
|
||||
grid-template-columns: 1fr 1fr; |
|
||||
gap: 16px; |
|
||||
margin-top: 12px; |
|
||||
} |
|
||||
.metric-item { |
|
||||
display: flex; |
|
||||
justify-content: space-between; |
|
||||
} |
|
||||
.metric-label { |
|
||||
color: #666; |
|
||||
} |
|
||||
.metric-value { |
|
||||
font-weight: bold; |
|
||||
color: #333; |
|
||||
} |
|
||||
.error-display { |
|
||||
background: #ffebee; |
|
||||
border: 1px solid #f44336; |
|
||||
border-radius: 8px; |
|
||||
padding: 16px; |
|
||||
margin: 16px 0; |
|
||||
} |
|
||||
.error-icon { |
|
||||
font-size: 24px; |
|
||||
margin-bottom: 12px; |
|
||||
} |
|
||||
.error-content h3 { |
|
||||
margin: 0 0 8px 0; |
|
||||
color: #d32f2f; |
|
||||
} |
|
||||
.error-message { |
|
||||
color: #666; |
|
||||
margin: 8px 0; |
|
||||
} |
|
||||
.error-code { |
|
||||
font-size: 12px; |
|
||||
color: #999; |
|
||||
font-family: monospace; |
|
||||
} |
|
||||
.error-actions { |
|
||||
display: flex; |
|
||||
gap: 12px; |
|
||||
margin-top: 16px; |
|
||||
} |
|
||||
.dialog-overlay { |
|
||||
position: fixed; |
|
||||
top: 0; |
|
||||
left: 0; |
|
||||
right: 0; |
|
||||
bottom: 0; |
|
||||
background: rgba(0, 0, 0, 0.5); |
|
||||
display: flex; |
|
||||
align-items: center; |
|
||||
justify-content: center; |
|
||||
z-index: 1000; |
|
||||
} |
|
||||
.dialog-content { |
|
||||
background: white; |
|
||||
border-radius: 12px; |
|
||||
padding: 24px; |
|
||||
max-width: 400px; |
|
||||
margin: 20px; |
|
||||
} |
|
||||
.dialog-content h2 { |
|
||||
margin: 0 0 16px 0; |
|
||||
color: #333; |
|
||||
} |
|
||||
.dialog-content p { |
|
||||
margin: 0 0 16px 0; |
|
||||
color: #666; |
|
||||
line-height: 1.5; |
|
||||
} |
|
||||
.dialog-content ul { |
|
||||
margin: 0 0 24px 0; |
|
||||
padding-left: 20px; |
|
||||
} |
|
||||
.dialog-content li { |
|
||||
margin: 8px 0; |
|
||||
color: #666; |
|
||||
} |
|
||||
.dialog-actions { |
|
||||
display: flex; |
|
||||
flex-direction: column; |
|
||||
gap: 12px; |
|
||||
} |
|
||||
.hidden { |
|
||||
display: none; |
|
||||
} |
|
||||
@media (max-width: 768px) { |
|
||||
.status-grid { |
|
||||
grid-template-columns: 1fr; |
|
||||
} |
|
||||
.preference-grid { |
|
||||
grid-template-columns: 1fr; |
|
||||
} |
|
||||
.checkbox-group { |
|
||||
grid-template-columns: 1fr; |
|
||||
} |
|
||||
.metrics-grid { |
|
||||
grid-template-columns: 1fr; |
|
||||
} |
|
||||
} |
|
||||
</style> |
|
||||
</head> |
|
||||
<body> |
|
||||
<div class="container"> |
|
||||
<h1>📱 TimeSafari Daily Notification - Android Test</h1> |
|
||||
|
|
||||
<div class="status" id="status">Ready</div> |
|
||||
|
|
||||
<!-- Permission Management Section --> |
|
||||
<div class="ui-section"> |
|
||||
<h3>🔐 Permission Management</h3> |
|
||||
<div id="permission-status-container"></div> |
|
||||
<div class="button-grid"> |
|
||||
<button id="check-permissions" class="btn-secondary">Check Permissions</button> |
|
||||
<button id="request-permissions" class="btn-primary">Request Permissions</button> |
|
||||
<button id="open-settings" class="btn-tertiary">Open Settings</button> |
|
||||
</div> |
|
||||
</div> |
|
||||
|
|
||||
<!-- Configuration Section --> |
|
||||
<div class="ui-section"> |
|
||||
<h3>⚙️ Configuration</h3> |
|
||||
<div id="settings-container"></div> |
|
||||
<div class="button-grid"> |
|
||||
<button id="configure">Configure TimeSafari</button> |
|
||||
<button id="test-notification" class="btn-secondary">Test Notification</button> |
|
||||
</div> |
|
||||
</div> |
|
||||
|
|
||||
<!-- Status Monitoring Section --> |
|
||||
<div class="ui-section"> |
|
||||
<h3>📊 Status Monitoring</h3> |
|
||||
<div id="status-container"></div> |
|
||||
<div class="button-grid"> |
|
||||
<button id="check-status">Check Status</button> |
|
||||
<button id="refresh-status" class="btn-secondary">Refresh</button> |
|
||||
</div> |
|
||||
</div> |
|
||||
|
|
||||
<!-- Platform-Specific Section --> |
|
||||
<div class="ui-section"> |
|
||||
<h3>🤖 Android-Specific Features</h3> |
|
||||
<div id="battery-dialog-container"></div> |
|
||||
<div class="button-grid"> |
|
||||
<button id="battery-status">Check Battery Optimization</button> |
|
||||
<button id="exact-alarm-status">Check Exact Alarm</button> |
|
||||
<button id="reboot-recovery">Check Reboot Recovery</button> |
|
||||
</div> |
|
||||
</div> |
|
||||
|
|
||||
<!-- Testing Section --> |
|
||||
<div class="ui-section"> |
|
||||
<h3>🧪 Testing & Debug</h3> |
|
||||
<div class="button-grid"> |
|
||||
<button id="schedule">Schedule Community Notifications</button> |
|
||||
<button id="endorser-api">Test Endorser.ch API</button> |
|
||||
<button id="callbacks">Register Callbacks</button> |
|
||||
<button id="performance">Performance Metrics</button> |
|
||||
</div> |
|
||||
</div> |
|
||||
|
|
||||
<!-- Phase 4: TimeSafari Components Testing --> |
|
||||
<div class="ui-section"> |
|
||||
<h3>🚀 Phase 4: TimeSafari Components</h3> |
|
||||
<div class="button-grid"> |
|
||||
<button id="test-security-manager" class="btn-primary">Test SecurityManager</button> |
|
||||
<button id="test-endorser-api-client" class="btn-primary">Test EndorserAPIClient</button> |
|
||||
<button id="test-notification-manager" class="btn-primary">Test NotificationManager</button> |
|
||||
<button id="test-phase4-integration" class="btn-secondary">Test Complete Integration</button> |
|
||||
</div> |
|
||||
</div> |
|
||||
|
|
||||
<!-- Static Daily Reminders Testing --> |
|
||||
<div class="ui-section"> |
|
||||
<h3>⏰ Static Daily Reminders</h3> |
|
||||
<div class="button-grid"> |
|
||||
<button id="schedule-reminder" class="btn-primary">Schedule Daily Reminder</button> |
|
||||
<button id="cancel-reminder" class="btn-primary">Cancel Reminder</button> |
|
||||
<button id="get-reminders" class="btn-primary">Get Scheduled Reminders</button> |
|
||||
<button id="update-reminder" class="btn-primary">Update Reminder</button> |
|
||||
</div> |
|
||||
<div class="form-section"> |
|
||||
<h4>Reminder Configuration</h4> |
|
||||
<div class="form-group"> |
|
||||
<label for="reminder-id">Reminder ID:</label> |
|
||||
<input type="text" id="reminder-id" value="morning_checkin" placeholder="e.g., morning_checkin"> |
|
||||
</div> |
|
||||
<div class="form-group"> |
|
||||
<label for="reminder-title">Title:</label> |
|
||||
<input type="text" id="reminder-title" value="Good Morning!" placeholder="Reminder title"> |
|
||||
</div> |
|
||||
<div class="form-group"> |
|
||||
<label for="reminder-body">Body:</label> |
|
||||
<input type="text" id="reminder-body" value="Time to check your TimeSafari community updates" placeholder="Reminder message"> |
|
||||
</div> |
|
||||
<div class="form-group"> |
|
||||
<label for="reminder-time">Time (HH:mm):</label> |
|
||||
<input type="text" id="reminder-time" value="09:00" placeholder="09:00"> |
|
||||
</div> |
|
||||
<div class="form-group"> |
|
||||
<label> |
|
||||
<input type="checkbox" id="reminder-sound" checked> Sound |
|
||||
</label> |
|
||||
<label> |
|
||||
<input type="checkbox" id="reminder-vibration" checked> Vibration |
|
||||
</label> |
|
||||
</div> |
|
||||
<div class="form-group"> |
|
||||
<label for="reminder-priority">Priority:</label> |
|
||||
<select id="reminder-priority"> |
|
||||
<option value="low">Low</option> |
|
||||
<option value="normal" selected>Normal</option> |
|
||||
<option value="high">High</option> |
|
||||
</select> |
|
||||
</div> |
|
||||
<div class="form-group"> |
|
||||
<label> |
|
||||
<input type="checkbox" id="reminder-repeat" checked> Repeat Daily |
|
||||
</label> |
|
||||
</div> |
|
||||
</div> |
|
||||
</div> |
|
||||
|
|
||||
<!-- Error Handling Section --> |
|
||||
<div class="ui-section"> |
|
||||
<h3>⚠️ Error Handling</h3> |
|
||||
<div id="error-container"></div> |
|
||||
</div> |
|
||||
|
|
||||
<!-- Log Section --> |
|
||||
<div class="ui-section"> |
|
||||
<h3>📝 Activity Log</h3> |
|
||||
<div class="log-container" id="log"></div> |
|
||||
<button class="clear-button" id="clear-log">Clear Log</button> |
|
||||
</div> |
|
||||
</div> |
|
||||
|
|
||||
<!-- Permission Dialog --> |
|
||||
<div id="permission-dialog-container"></div> |
|
||||
|
|
||||
<script type="module" src="index.js"></script> |
|
||||
</body> |
|
||||
</html> |
|
@ -0,0 +1,629 @@ |
|||||
|
<!-- |
||||
|
/** |
||||
|
* Logs View - View and Copy Android Logs |
||||
|
* |
||||
|
* @author Matthew Raymer |
||||
|
* @version 1.0.0 |
||||
|
*/ |
||||
|
--> |
||||
|
|
||||
|
<template> |
||||
|
<div class="logs-view"> |
||||
|
<div class="view-header"> |
||||
|
<h1 class="page-title">📋 Android Logs</h1> |
||||
|
<p class="page-subtitle">View and copy DailyNotification plugin logs</p> |
||||
|
</div> |
||||
|
|
||||
|
<div class="logs-controls"> |
||||
|
<button |
||||
|
class="control-button refresh-button" |
||||
|
@click="refreshLogs" |
||||
|
:disabled="isRefreshing" |
||||
|
> |
||||
|
<span v-if="isRefreshing">🔄</span> |
||||
|
<span v-else>🔄</span> |
||||
|
{{ isRefreshing ? 'Refreshing...' : 'Refresh Logs' }} |
||||
|
</button> |
||||
|
|
||||
|
<button |
||||
|
class="control-button copy-button" |
||||
|
@click="copyLogsToClipboard" |
||||
|
:disabled="!hasLogs || isCopying" |
||||
|
> |
||||
|
<span v-if="isCopying">📋</span> |
||||
|
<span v-else>📋</span> |
||||
|
{{ isCopying ? 'Copying...' : 'Copy to Clipboard' }} |
||||
|
</button> |
||||
|
|
||||
|
<button |
||||
|
class="control-button clear-button" |
||||
|
@click="clearLogs" |
||||
|
:disabled="!hasLogs" |
||||
|
> |
||||
|
🗑️ Clear Logs |
||||
|
</button> |
||||
|
</div> |
||||
|
|
||||
|
<div class="logs-container"> |
||||
|
<div v-if="hasLogs" class="logs-content"> |
||||
|
<div class="logs-header"> |
||||
|
<span class="logs-count">{{ logs.length }} log entries</span> |
||||
|
<span class="logs-timestamp">Last updated: {{ lastUpdated }}</span> |
||||
|
</div> |
||||
|
|
||||
|
<div class="logs-list" ref="logsList"> |
||||
|
<div |
||||
|
v-for="(log, index) in logs" |
||||
|
:key="index" |
||||
|
class="log-entry" |
||||
|
:class="getLogLevelClass(log)" |
||||
|
> |
||||
|
<span class="log-timestamp">{{ formatTimestamp(log.timestamp) }}</span> |
||||
|
<span class="log-level">{{ log.level }}</span> |
||||
|
<span class="log-tag">{{ log.tag }}</span> |
||||
|
<span class="log-message">{{ log.message }}</span> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<div v-else class="no-logs"> |
||||
|
<div class="empty-state"> |
||||
|
<span class="empty-icon">📋</span> |
||||
|
<h3 class="empty-title">No Logs Available</h3> |
||||
|
<p class="empty-description"> |
||||
|
Click "Refresh Logs" to fetch DailyNotification plugin logs from the device |
||||
|
</p> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<!-- Success/Error Messages --> |
||||
|
<div v-if="successMessage" class="message success-message"> |
||||
|
✅ {{ successMessage }} |
||||
|
</div> |
||||
|
|
||||
|
<div v-if="errorMessage" class="message error-message"> |
||||
|
❌ {{ errorMessage }} |
||||
|
</div> |
||||
|
</div> |
||||
|
</template> |
||||
|
|
||||
|
<script lang="ts"> |
||||
|
import { Component, Vue } from 'vue-facing-decorator' |
||||
|
import { Capacitor } from '@capacitor/core' |
||||
|
|
||||
|
interface LogEntry { |
||||
|
timestamp: string |
||||
|
level: string |
||||
|
tag: string |
||||
|
message: string |
||||
|
} |
||||
|
|
||||
|
@Component |
||||
|
export default class LogsView extends Vue { |
||||
|
logs: LogEntry[] = [] |
||||
|
isRefreshing = false |
||||
|
isCopying = false |
||||
|
successMessage = '' |
||||
|
errorMessage = '' |
||||
|
lastUpdated = '' |
||||
|
|
||||
|
get hasLogs(): boolean { |
||||
|
return this.logs.length > 0 |
||||
|
} |
||||
|
|
||||
|
async refreshLogs(): Promise<void> { |
||||
|
this.isRefreshing = true |
||||
|
this.clearMessages() |
||||
|
|
||||
|
try { |
||||
|
if (!Capacitor.isNativePlatform()) { |
||||
|
// Mock logs for web testing |
||||
|
this.logs = this.generateMockLogs() |
||||
|
this.lastUpdated = new Date().toLocaleString() |
||||
|
this.successMessage = 'Mock logs loaded (web mode)' |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
// In a real implementation, you would fetch logs from the device |
||||
|
// For now, we'll simulate fetching logs |
||||
|
await this.simulateLogFetch() |
||||
|
|
||||
|
} catch (error) { |
||||
|
console.error('❌ Failed to refresh logs:', error) |
||||
|
this.errorMessage = 'Failed to refresh logs: ' + (error as Error).message |
||||
|
} finally { |
||||
|
this.isRefreshing = false |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
async copyLogsToClipboard(): Promise<void> { |
||||
|
if (!this.hasLogs) { |
||||
|
this.errorMessage = 'No logs to copy' |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
this.isCopying = true |
||||
|
this.clearMessages() |
||||
|
|
||||
|
try { |
||||
|
const logsText = this.formatLogsForCopy() |
||||
|
|
||||
|
if (Capacitor.isNativePlatform()) { |
||||
|
// Use Capacitor Clipboard plugin if available |
||||
|
if (window.Capacitor && window.Capacitor.Plugins && window.Capacitor.Plugins.Clipboard) { |
||||
|
await window.Capacitor.Plugins.Clipboard.write({ |
||||
|
string: logsText |
||||
|
}) |
||||
|
} else { |
||||
|
// Fallback to web clipboard API |
||||
|
await navigator.clipboard.writeText(logsText) |
||||
|
} |
||||
|
} else { |
||||
|
// Web clipboard API |
||||
|
await navigator.clipboard.writeText(logsText) |
||||
|
} |
||||
|
|
||||
|
this.successMessage = `Copied ${this.logs.length} log entries to clipboard` |
||||
|
|
||||
|
// Auto-hide success message |
||||
|
setTimeout(() => { |
||||
|
this.successMessage = '' |
||||
|
}, 3000) |
||||
|
|
||||
|
} catch (error) { |
||||
|
console.error('❌ Failed to copy logs:', error) |
||||
|
this.errorMessage = 'Failed to copy logs: ' + (error as Error).message |
||||
|
} finally { |
||||
|
this.isCopying = false |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
clearLogs(): void { |
||||
|
this.logs = [] |
||||
|
this.lastUpdated = '' |
||||
|
this.clearMessages() |
||||
|
this.successMessage = 'Logs cleared' |
||||
|
|
||||
|
// Auto-hide success message |
||||
|
setTimeout(() => { |
||||
|
this.successMessage = '' |
||||
|
}, 2000) |
||||
|
} |
||||
|
|
||||
|
private clearMessages(): void { |
||||
|
this.successMessage = '' |
||||
|
this.errorMessage = '' |
||||
|
} |
||||
|
|
||||
|
private async simulateLogFetch(): Promise<void> { |
||||
|
// Simulate network delay |
||||
|
await new Promise(resolve => setTimeout(resolve, 1000)) |
||||
|
|
||||
|
// Generate sample logs based on the terminal output you showed |
||||
|
this.logs = this.generateSampleLogs() |
||||
|
this.lastUpdated = new Date().toLocaleString() |
||||
|
this.successMessage = `Loaded ${this.logs.length} log entries` |
||||
|
|
||||
|
// Auto-hide success message |
||||
|
setTimeout(() => { |
||||
|
this.successMessage = '' |
||||
|
}, 3000) |
||||
|
} |
||||
|
|
||||
|
private generateSampleLogs(): LogEntry[] { |
||||
|
const now = new Date() |
||||
|
return [ |
||||
|
{ |
||||
|
timestamp: new Date(now.getTime() - 1000).toISOString(), |
||||
|
level: 'D', |
||||
|
tag: 'DailyNotificationReceiver', |
||||
|
message: 'DN|RECEIVE_START action=com.timesafari.daily.NOTIFICATION' |
||||
|
}, |
||||
|
{ |
||||
|
timestamp: new Date(now.getTime() - 900).toISOString(), |
||||
|
level: 'D', |
||||
|
tag: 'DailyNotificationReceiver', |
||||
|
message: 'DN|WORK_ENQUEUE display=3bc1b920-9407-4ccf-94c0-26f99ba4c39d' |
||||
|
}, |
||||
|
{ |
||||
|
timestamp: new Date(now.getTime() - 800).toISOString(), |
||||
|
level: 'D', |
||||
|
tag: 'DailyNotificationWorker', |
||||
|
message: 'DN|WORK_START id=3bc1b920-9407-4ccf-94c0-26f99ba4c39d action=display' |
||||
|
}, |
||||
|
{ |
||||
|
timestamp: new Date(now.getTime() - 700).toISOString(), |
||||
|
level: 'D', |
||||
|
tag: 'DailyNotificationWorker', |
||||
|
message: 'DN|DISPLAY_START id=3bc1b920-9407-4ccf-94c0-26f99ba4c39d' |
||||
|
}, |
||||
|
{ |
||||
|
timestamp: new Date(now.getTime() - 600).toISOString(), |
||||
|
level: 'D', |
||||
|
tag: 'DailyNotificationStorage', |
||||
|
message: 'Loading notifications from storage: [53 notifications]' |
||||
|
}, |
||||
|
{ |
||||
|
timestamp: new Date(now.getTime() - 500).toISOString(), |
||||
|
level: 'D', |
||||
|
tag: 'DailyNotificationWorker', |
||||
|
message: 'DN|JIT_FRESH skip=true ageMin=0 id=3bc1b920-9407-4ccf-94c0-26f99ba4c39d' |
||||
|
}, |
||||
|
{ |
||||
|
timestamp: new Date(now.getTime() - 400).toISOString(), |
||||
|
level: 'D', |
||||
|
tag: 'DailyNotificationWorker', |
||||
|
message: 'DN|CLICK_INTENT app_only' |
||||
|
}, |
||||
|
{ |
||||
|
timestamp: new Date(now.getTime() - 300).toISOString(), |
||||
|
level: 'D', |
||||
|
tag: 'DailyNotificationWorker', |
||||
|
message: 'DN|ACTION_BUTTONS dismiss_only' |
||||
|
}, |
||||
|
{ |
||||
|
timestamp: new Date(now.getTime() - 200).toISOString(), |
||||
|
level: 'I', |
||||
|
tag: 'DailyNotificationWorker', |
||||
|
message: 'DN|DISPLAY_NOTIF_OK id=3bc1b920-9407-4ccf-94c0-26f99ba4c39d' |
||||
|
}, |
||||
|
{ |
||||
|
timestamp: new Date(now.getTime() - 100).toISOString(), |
||||
|
level: 'D', |
||||
|
tag: 'DailyNotificationWorker', |
||||
|
message: 'DN|RESCHEDULE_START id=3bc1b920-9407-4ccf-94c0-26f99ba4c39d' |
||||
|
} |
||||
|
] |
||||
|
} |
||||
|
|
||||
|
private generateMockLogs(): LogEntry[] { |
||||
|
const now = new Date() |
||||
|
return [ |
||||
|
{ |
||||
|
timestamp: new Date(now.getTime() - 2000).toISOString(), |
||||
|
level: 'I', |
||||
|
tag: 'VueApp', |
||||
|
message: 'App initialized successfully' |
||||
|
}, |
||||
|
{ |
||||
|
timestamp: new Date(now.getTime() - 1500).toISOString(), |
||||
|
level: 'D', |
||||
|
tag: 'Capacitor', |
||||
|
message: 'Running in web mode - DailyNotification plugin not available' |
||||
|
}, |
||||
|
{ |
||||
|
timestamp: new Date(now.getTime() - 1000).toISOString(), |
||||
|
level: 'W', |
||||
|
tag: 'NotificationsStore', |
||||
|
message: 'DailyNotification plugin not loaded' |
||||
|
} |
||||
|
] |
||||
|
} |
||||
|
|
||||
|
private formatLogsForCopy(): string { |
||||
|
const header = `DailyNotification Plugin Logs\nGenerated: ${new Date().toLocaleString()}\nTotal Entries: ${this.logs.length}\n\n` |
||||
|
|
||||
|
const logLines = this.logs.map(log => |
||||
|
`${log.timestamp} ${log.level}/${log.tag}: ${log.message}` |
||||
|
).join('\n') |
||||
|
|
||||
|
return header + logLines |
||||
|
} |
||||
|
|
||||
|
private formatTimestamp(timestamp: string): string { |
||||
|
const date = new Date(timestamp) |
||||
|
return date.toLocaleTimeString() |
||||
|
} |
||||
|
|
||||
|
private getLogLevelClass(log: LogEntry): string { |
||||
|
switch (log.level) { |
||||
|
case 'E': return 'log-error' |
||||
|
case 'W': return 'log-warning' |
||||
|
case 'I': return 'log-info' |
||||
|
case 'D': return 'log-debug' |
||||
|
case 'V': return 'log-verbose' |
||||
|
default: return 'log-default' |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
async mounted(): Promise<void> { |
||||
|
// Auto-refresh logs on mount |
||||
|
await this.refreshLogs() |
||||
|
} |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<style scoped> |
||||
|
.logs-view { |
||||
|
max-width: 1200px; |
||||
|
margin: 0 auto; |
||||
|
padding: 20px; |
||||
|
} |
||||
|
|
||||
|
.view-header { |
||||
|
text-align: center; |
||||
|
margin-bottom: 32px; |
||||
|
} |
||||
|
|
||||
|
.page-title { |
||||
|
font-size: 2rem; |
||||
|
font-weight: 700; |
||||
|
color: white; |
||||
|
margin: 0 0 8px 0; |
||||
|
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3); |
||||
|
} |
||||
|
|
||||
|
.page-subtitle { |
||||
|
font-size: 1.1rem; |
||||
|
color: rgba(255, 255, 255, 0.9); |
||||
|
margin: 0; |
||||
|
font-weight: 300; |
||||
|
} |
||||
|
|
||||
|
.logs-controls { |
||||
|
display: flex; |
||||
|
gap: 12px; |
||||
|
margin-bottom: 24px; |
||||
|
flex-wrap: wrap; |
||||
|
} |
||||
|
|
||||
|
.control-button { |
||||
|
background: linear-gradient(135deg, #2196f3, #1976d2); |
||||
|
color: white; |
||||
|
border: none; |
||||
|
padding: 12px 20px; |
||||
|
border-radius: 8px; |
||||
|
font-size: 0.9rem; |
||||
|
font-weight: 600; |
||||
|
cursor: pointer; |
||||
|
transition: all 0.3s ease; |
||||
|
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3); |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
gap: 8px; |
||||
|
} |
||||
|
|
||||
|
.control-button:hover:not(:disabled) { |
||||
|
background: linear-gradient(135deg, #1976d2, #1565c0); |
||||
|
transform: translateY(-2px); |
||||
|
box-shadow: 0 8px 25px rgba(33, 150, 243, 0.3); |
||||
|
} |
||||
|
|
||||
|
.control-button:disabled { |
||||
|
background: rgba(255, 255, 255, 0.2); |
||||
|
cursor: not-allowed; |
||||
|
transform: none; |
||||
|
box-shadow: none; |
||||
|
} |
||||
|
|
||||
|
.refresh-button { |
||||
|
background: linear-gradient(135deg, #4caf50, #45a049); |
||||
|
} |
||||
|
|
||||
|
.refresh-button:hover:not(:disabled) { |
||||
|
background: linear-gradient(135deg, #45a049, #3d8b40); |
||||
|
box-shadow: 0 8px 25px rgba(76, 175, 80, 0.3); |
||||
|
} |
||||
|
|
||||
|
.copy-button { |
||||
|
background: linear-gradient(135deg, #ff9800, #f57c00); |
||||
|
} |
||||
|
|
||||
|
.copy-button:hover:not(:disabled) { |
||||
|
background: linear-gradient(135deg, #f57c00, #ef6c00); |
||||
|
box-shadow: 0 8px 25px rgba(255, 152, 0, 0.3); |
||||
|
} |
||||
|
|
||||
|
.clear-button { |
||||
|
background: linear-gradient(135deg, #f44336, #d32f2f); |
||||
|
} |
||||
|
|
||||
|
.clear-button:hover:not(:disabled) { |
||||
|
background: linear-gradient(135deg, #d32f2f, #c62828); |
||||
|
box-shadow: 0 8px 25px rgba(244, 67, 54, 0.3); |
||||
|
} |
||||
|
|
||||
|
.logs-container { |
||||
|
background: rgba(255, 255, 255, 0.1); |
||||
|
border-radius: 16px; |
||||
|
backdrop-filter: blur(10px); |
||||
|
border: 1px solid rgba(255, 255, 255, 0.2); |
||||
|
overflow: hidden; |
||||
|
} |
||||
|
|
||||
|
.logs-content { |
||||
|
padding: 20px; |
||||
|
} |
||||
|
|
||||
|
.logs-header { |
||||
|
display: flex; |
||||
|
justify-content: space-between; |
||||
|
align-items: center; |
||||
|
margin-bottom: 16px; |
||||
|
padding-bottom: 12px; |
||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.2); |
||||
|
} |
||||
|
|
||||
|
.logs-count { |
||||
|
font-weight: 600; |
||||
|
color: white; |
||||
|
font-size: 0.9rem; |
||||
|
} |
||||
|
|
||||
|
.logs-timestamp { |
||||
|
color: rgba(255, 255, 255, 0.7); |
||||
|
font-size: 0.8rem; |
||||
|
} |
||||
|
|
||||
|
.logs-list { |
||||
|
max-height: 500px; |
||||
|
overflow-y: auto; |
||||
|
font-family: 'Courier New', monospace; |
||||
|
font-size: 0.85rem; |
||||
|
line-height: 1.4; |
||||
|
} |
||||
|
|
||||
|
.log-entry { |
||||
|
display: grid; |
||||
|
grid-template-columns: 80px 20px 200px 1fr; |
||||
|
gap: 12px; |
||||
|
padding: 8px 0; |
||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.1); |
||||
|
align-items: start; |
||||
|
} |
||||
|
|
||||
|
.log-entry:last-child { |
||||
|
border-bottom: none; |
||||
|
} |
||||
|
|
||||
|
.log-timestamp { |
||||
|
color: rgba(255, 255, 255, 0.6); |
||||
|
font-size: 0.75rem; |
||||
|
} |
||||
|
|
||||
|
.log-level { |
||||
|
font-weight: bold; |
||||
|
text-align: center; |
||||
|
font-size: 0.8rem; |
||||
|
} |
||||
|
|
||||
|
.log-tag { |
||||
|
color: rgba(255, 255, 255, 0.8); |
||||
|
font-weight: 500; |
||||
|
overflow: hidden; |
||||
|
text-overflow: ellipsis; |
||||
|
white-space: nowrap; |
||||
|
} |
||||
|
|
||||
|
.log-message { |
||||
|
color: rgba(255, 255, 255, 0.9); |
||||
|
word-break: break-word; |
||||
|
} |
||||
|
|
||||
|
/* Log level colors */ |
||||
|
.log-error .log-level { |
||||
|
color: #f44336; |
||||
|
} |
||||
|
|
||||
|
.log-warning .log-level { |
||||
|
color: #ff9800; |
||||
|
} |
||||
|
|
||||
|
.log-info .log-level { |
||||
|
color: #4caf50; |
||||
|
} |
||||
|
|
||||
|
.log-debug .log-level { |
||||
|
color: #2196f3; |
||||
|
} |
||||
|
|
||||
|
.log-verbose .log-level { |
||||
|
color: #9c27b0; |
||||
|
} |
||||
|
|
||||
|
.log-default .log-level { |
||||
|
color: rgba(255, 255, 255, 0.7); |
||||
|
} |
||||
|
|
||||
|
.no-logs { |
||||
|
padding: 40px 20px; |
||||
|
text-align: center; |
||||
|
} |
||||
|
|
||||
|
.empty-state { |
||||
|
padding: 40px 20px; |
||||
|
} |
||||
|
|
||||
|
.empty-icon { |
||||
|
font-size: 4rem; |
||||
|
display: block; |
||||
|
margin-bottom: 16px; |
||||
|
} |
||||
|
|
||||
|
.empty-title { |
||||
|
font-size: 1.5rem; |
||||
|
font-weight: 600; |
||||
|
color: white; |
||||
|
margin: 0 0 8px 0; |
||||
|
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3); |
||||
|
} |
||||
|
|
||||
|
.empty-description { |
||||
|
font-size: 1rem; |
||||
|
color: rgba(255, 255, 255, 0.8); |
||||
|
margin: 0; |
||||
|
line-height: 1.5; |
||||
|
} |
||||
|
|
||||
|
.message { |
||||
|
position: fixed; |
||||
|
top: 20px; |
||||
|
right: 20px; |
||||
|
padding: 12px 20px; |
||||
|
border-radius: 8px; |
||||
|
font-weight: 600; |
||||
|
z-index: 1000; |
||||
|
animation: slideIn 0.3s ease; |
||||
|
} |
||||
|
|
||||
|
.success-message { |
||||
|
background: linear-gradient(135deg, #4caf50, #45a049); |
||||
|
color: white; |
||||
|
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3); |
||||
|
} |
||||
|
|
||||
|
.error-message { |
||||
|
background: linear-gradient(135deg, #f44336, #d32f2f); |
||||
|
color: white; |
||||
|
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3); |
||||
|
} |
||||
|
|
||||
|
@keyframes slideIn { |
||||
|
from { |
||||
|
transform: translateX(100%); |
||||
|
opacity: 0; |
||||
|
} |
||||
|
to { |
||||
|
transform: translateX(0); |
||||
|
opacity: 1; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/* Responsive design */ |
||||
|
@media (max-width: 768px) { |
||||
|
.logs-view { |
||||
|
padding: 16px; |
||||
|
} |
||||
|
|
||||
|
.logs-controls { |
||||
|
flex-direction: column; |
||||
|
} |
||||
|
|
||||
|
.control-button { |
||||
|
justify-content: center; |
||||
|
} |
||||
|
|
||||
|
.log-entry { |
||||
|
grid-template-columns: 1fr; |
||||
|
gap: 4px; |
||||
|
} |
||||
|
|
||||
|
.log-timestamp, |
||||
|
.log-level, |
||||
|
.log-tag { |
||||
|
font-size: 0.75rem; |
||||
|
} |
||||
|
|
||||
|
.logs-list { |
||||
|
font-size: 0.8rem; |
||||
|
} |
||||
|
|
||||
|
.message { |
||||
|
position: relative; |
||||
|
top: auto; |
||||
|
right: auto; |
||||
|
margin: 16px 0; |
||||
|
} |
||||
|
} |
||||
|
</style> |
Loading…
Reference in new issue