Compare commits
5 Commits
49fd1dfedf
...
54478b1c97
Author | SHA1 | Date |
---|---|---|
|
54478b1c97 | 1 week ago |
|
a625adecf4 | 1 week ago |
|
425189d933 | 1 week ago |
|
ed8db53612 | 1 week ago |
|
6213235a16 | 1 week ago |
35 changed files with 8166 additions and 5620 deletions
@ -0,0 +1,20 @@ |
|||||
|
/* eslint-env node */ |
||||
|
require('@rushstack/eslint-patch/modern-module-resolution') |
||||
|
|
||||
|
module.exports = { |
||||
|
root: true, |
||||
|
'extends': [ |
||||
|
'plugin:vue/vue3-essential', |
||||
|
'eslint:recommended', |
||||
|
'@vue/eslint-config-typescript' |
||||
|
], |
||||
|
parserOptions: { |
||||
|
ecmaVersion: 'latest' |
||||
|
}, |
||||
|
rules: { |
||||
|
'vue/multi-word-component-names': 'off', |
||||
|
'@typescript-eslint/no-unused-vars': ['error', { 'argsIgnorePattern': '^_' }], |
||||
|
'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off', |
||||
|
'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off' |
||||
|
} |
||||
|
} |
@ -0,0 +1,329 @@ |
|||||
|
# Daily Notification Test App - Vue 3 |
||||
|
|
||||
|
A modern Vue 3 + Vite + Capacitor test application for the Daily Notification Plugin, built with vue-facing-decorator for TypeScript class-based components. |
||||
|
|
||||
|
## 🚀 Features |
||||
|
|
||||
|
- **Vue 3** with Composition API and TypeScript |
||||
|
- **Vite** for fast development and building |
||||
|
- **vue-facing-decorator** for class-based components |
||||
|
- **Pinia** for state management |
||||
|
- **Vue Router** for navigation |
||||
|
- **Capacitor** for native Android functionality |
||||
|
- **Modern UI** with glassmorphism design |
||||
|
- **Responsive** design for mobile and desktop |
||||
|
|
||||
|
## 📱 App Structure |
||||
|
|
||||
|
``` |
||||
|
src/ |
||||
|
├── components/ # Reusable Vue components |
||||
|
│ ├── cards/ # Card components (ActionCard, StatusCard, etc.) |
||||
|
│ ├── items/ # List item components |
||||
|
│ ├── layout/ # Layout components (Header, Footer) |
||||
|
│ └── ui/ # UI components (Loading, Error, etc.) |
||||
|
├── stores/ # Pinia stores |
||||
|
│ ├── app.ts # Global app state |
||||
|
│ └── notifications.ts # Notification management |
||||
|
├── views/ # Page components |
||||
|
│ ├── HomeView.vue # Dashboard |
||||
|
│ ├── ScheduleView.vue # Schedule notifications |
||||
|
│ ├── NotificationsView.vue # Manage notifications |
||||
|
│ ├── StatusView.vue # System status |
||||
|
│ ├── HistoryView.vue # Notification history |
||||
|
│ └── SettingsView.vue # App settings |
||||
|
├── router/ # Vue Router configuration |
||||
|
├── types/ # TypeScript type definitions |
||||
|
└── utils/ # Utility functions |
||||
|
``` |
||||
|
|
||||
|
## 🛠️ Development |
||||
|
|
||||
|
### Prerequisites |
||||
|
|
||||
|
- Node.js 18+ |
||||
|
- npm or yarn |
||||
|
- Android Studio (for Android development) |
||||
|
- Capacitor CLI |
||||
|
|
||||
|
### Installation |
||||
|
|
||||
|
```bash |
||||
|
# Install dependencies |
||||
|
npm install |
||||
|
|
||||
|
# Start development server |
||||
|
npm run dev |
||||
|
|
||||
|
# Build for production |
||||
|
npm run build |
||||
|
|
||||
|
# Type checking |
||||
|
npm run type-check |
||||
|
|
||||
|
# Linting |
||||
|
npm run lint |
||||
|
``` |
||||
|
|
||||
|
### Android Development |
||||
|
|
||||
|
```bash |
||||
|
# Sync with Capacitor |
||||
|
npm run sync |
||||
|
|
||||
|
# Open Android Studio |
||||
|
npm run open |
||||
|
|
||||
|
# Run on Android device/emulator |
||||
|
npm run android |
||||
|
``` |
||||
|
|
||||
|
## 🎨 UI Components |
||||
|
|
||||
|
### Class-Based Components |
||||
|
|
||||
|
All components use vue-facing-decorator for TypeScript class-based syntax: |
||||
|
|
||||
|
```typescript |
||||
|
@Component({ |
||||
|
components: { |
||||
|
ChildComponent |
||||
|
} |
||||
|
}) |
||||
|
export default class MyComponent extends Vue { |
||||
|
@Prop() title!: string |
||||
|
|
||||
|
private data = ref('') |
||||
|
|
||||
|
get computedValue(): string { |
||||
|
return this.data.value.toUpperCase() |
||||
|
} |
||||
|
|
||||
|
private handleClick(): void { |
||||
|
// Handle click |
||||
|
} |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
### State Management |
||||
|
|
||||
|
Uses Pinia stores for reactive state management: |
||||
|
|
||||
|
```typescript |
||||
|
// stores/notifications.ts |
||||
|
export const useNotificationsStore = defineStore('notifications', () => { |
||||
|
const scheduledNotifications = ref<ScheduledNotification[]>([]) |
||||
|
|
||||
|
async function scheduleNotification(options: ScheduleOptions): Promise<void> { |
||||
|
// Schedule logic |
||||
|
} |
||||
|
|
||||
|
return { |
||||
|
scheduledNotifications, |
||||
|
scheduleNotification |
||||
|
} |
||||
|
}) |
||||
|
``` |
||||
|
|
||||
|
## 📊 Features |
||||
|
|
||||
|
### Dashboard (Home) |
||||
|
- Quick action cards |
||||
|
- System status overview |
||||
|
- Next scheduled notification |
||||
|
- Recent activity feed |
||||
|
|
||||
|
### Schedule Notifications |
||||
|
- Time picker |
||||
|
- Title and message inputs |
||||
|
- Sound and priority options |
||||
|
- URL support for deep linking |
||||
|
- Quick schedule presets |
||||
|
|
||||
|
### Notification Management |
||||
|
- View all scheduled notifications |
||||
|
- Cancel notifications |
||||
|
- Status indicators |
||||
|
|
||||
|
### System Status |
||||
|
- Permission checks |
||||
|
- Channel status |
||||
|
- Exact alarm settings |
||||
|
- Platform information |
||||
|
- Test notification functionality |
||||
|
|
||||
|
### History |
||||
|
- Delivered notification history |
||||
|
- Click and dismiss tracking |
||||
|
- Time-based filtering |
||||
|
|
||||
|
## 🔧 Configuration |
||||
|
|
||||
|
### Capacitor Config |
||||
|
|
||||
|
```typescript |
||||
|
// capacitor.config.ts |
||||
|
const config: CapacitorConfig = { |
||||
|
appId: 'com.timesafari.dailynotification.androidtest', |
||||
|
appName: 'Daily Notification Test - Vue 3', |
||||
|
webDir: 'dist', |
||||
|
plugins: { |
||||
|
DailyNotification: { |
||||
|
storage: 'shared', |
||||
|
ttlSeconds: 1800, |
||||
|
prefetchLeadMinutes: 15, |
||||
|
enableETagSupport: true, |
||||
|
enableErrorHandling: true, |
||||
|
enablePerformanceOptimization: true |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
### Vite Config |
||||
|
|
||||
|
```typescript |
||||
|
// vite.config.ts |
||||
|
export default defineConfig({ |
||||
|
plugins: [vue()], |
||||
|
build: { |
||||
|
outDir: 'dist', |
||||
|
sourcemap: true |
||||
|
}, |
||||
|
resolve: { |
||||
|
alias: { |
||||
|
'@': resolve(__dirname, 'src') |
||||
|
} |
||||
|
} |
||||
|
}) |
||||
|
``` |
||||
|
|
||||
|
## 🎯 Testing the Plugin |
||||
|
|
||||
|
1. **Start Development Server** |
||||
|
```bash |
||||
|
npm run dev |
||||
|
``` |
||||
|
|
||||
|
2. **Build and Sync** |
||||
|
```bash |
||||
|
npm run build |
||||
|
npm run sync |
||||
|
``` |
||||
|
|
||||
|
3. **Run on Android** |
||||
|
```bash |
||||
|
npm run android |
||||
|
``` |
||||
|
|
||||
|
4. **Test Features** |
||||
|
- Schedule notifications |
||||
|
- Check system status |
||||
|
- View notification history |
||||
|
- Test clickable notifications |
||||
|
|
||||
|
## 📱 Native Features |
||||
|
|
||||
|
- **Notification Scheduling** - Schedule daily notifications |
||||
|
- **Permission Management** - Check and request permissions |
||||
|
- **Status Monitoring** - Real-time system status |
||||
|
- **Deep Linking** - URL support in notifications |
||||
|
- **Background Processing** - WorkManager integration |
||||
|
|
||||
|
## 🎨 Design System |
||||
|
|
||||
|
### Colors |
||||
|
- Primary: Linear gradient (purple to blue) |
||||
|
- Success: #4caf50 |
||||
|
- Warning: #ff9800 |
||||
|
- Error: #f44336 |
||||
|
- Info: #2196f3 |
||||
|
|
||||
|
### Typography |
||||
|
- Headers: Bold, white with text-shadow |
||||
|
- Body: Regular, rgba white |
||||
|
- Code: Courier New monospace |
||||
|
|
||||
|
### Components |
||||
|
- Glassmorphism design with backdrop-filter |
||||
|
- Rounded corners (8px, 12px, 16px) |
||||
|
- Smooth transitions and hover effects |
||||
|
- Responsive grid layouts |
||||
|
|
||||
|
## 🚀 Production Build |
||||
|
|
||||
|
```bash |
||||
|
# Build for production |
||||
|
npm run build |
||||
|
|
||||
|
# Preview production build |
||||
|
npm run preview |
||||
|
|
||||
|
# Sync with Capacitor |
||||
|
npm run sync |
||||
|
|
||||
|
# Build Android APK |
||||
|
npm run android |
||||
|
``` |
||||
|
|
||||
|
## 📝 Scripts |
||||
|
|
||||
|
- `npm run dev` - Start development server |
||||
|
- `npm run build` - Build for production |
||||
|
- `npm run preview` - Preview production build |
||||
|
- `npm run android` - Run on Android |
||||
|
- `npm run sync` - Sync with Capacitor |
||||
|
- `npm run open` - Open Android Studio |
||||
|
- `npm run lint` - Run ESLint |
||||
|
- `npm run type-check` - TypeScript type checking |
||||
|
|
||||
|
## 🔍 Debugging |
||||
|
|
||||
|
### Vue DevTools |
||||
|
Install Vue DevTools browser extension for component inspection. |
||||
|
|
||||
|
### Capacitor Logs |
||||
|
```bash |
||||
|
# Android logs |
||||
|
adb logcat | grep -i "daily\|notification" |
||||
|
|
||||
|
# Capacitor logs |
||||
|
npx cap run android --livereload --external |
||||
|
``` |
||||
|
|
||||
|
### TypeScript |
||||
|
Enable strict mode in `tsconfig.json` for better type checking. |
||||
|
|
||||
|
## 📚 Dependencies |
||||
|
|
||||
|
### Core |
||||
|
- Vue 3.4+ |
||||
|
- Vite 5.0+ |
||||
|
- TypeScript 5.3+ |
||||
|
- Capacitor 5.0+ |
||||
|
|
||||
|
### UI & State |
||||
|
- vue-facing-decorator 3.0+ |
||||
|
- Pinia 2.1+ |
||||
|
- Vue Router 4.2+ |
||||
|
|
||||
|
### Development |
||||
|
- ESLint + TypeScript configs |
||||
|
- Vue TSC for type checking |
||||
|
- Modern module resolution |
||||
|
|
||||
|
## 🤝 Contributing |
||||
|
|
||||
|
1. Follow Vue 3 + TypeScript best practices |
||||
|
2. Use vue-facing-decorator for class components |
||||
|
3. Maintain responsive design |
||||
|
4. Add proper TypeScript types |
||||
|
5. Test on both web and Android |
||||
|
|
||||
|
## 📄 License |
||||
|
|
||||
|
MIT License - see LICENSE file for details. |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
**Built with ❤️ using Vue 3 + Vite + Capacitor** |
@ -0,0 +1,90 @@ |
|||||
|
/** |
||||
|
* Environment Type Declarations |
||||
|
* |
||||
|
* @author Matthew Raymer |
||||
|
* @version 1.0.0 |
||||
|
*/ |
||||
|
|
||||
|
/// <reference types="vite/client" />
|
||||
|
|
||||
|
declare module '*.vue' { |
||||
|
import type { DefineComponent } from 'vue' |
||||
|
const component: DefineComponent<{}, {}, any> |
||||
|
export default component |
||||
|
} |
||||
|
|
||||
|
// Capacitor plugin declarations
|
||||
|
declare global { |
||||
|
interface Window { |
||||
|
DailyNotification: { |
||||
|
scheduleDailyNotification: (options: { |
||||
|
time: string |
||||
|
title?: string |
||||
|
body?: string |
||||
|
sound?: boolean |
||||
|
priority?: string |
||||
|
url?: string |
||||
|
}) => Promise<void> |
||||
|
|
||||
|
scheduleDailyReminder: (options: { |
||||
|
id: string |
||||
|
title: string |
||||
|
body: string |
||||
|
time: string |
||||
|
sound?: boolean |
||||
|
vibration?: boolean |
||||
|
priority?: string |
||||
|
repeatDaily?: boolean |
||||
|
timezone?: string |
||||
|
}) => Promise<void> |
||||
|
|
||||
|
cancelDailyReminder: (options: { |
||||
|
reminderId: string |
||||
|
}) => Promise<void> |
||||
|
|
||||
|
updateDailyReminder: (options: { |
||||
|
reminderId: string |
||||
|
title?: string |
||||
|
body?: string |
||||
|
time?: string |
||||
|
sound?: boolean |
||||
|
vibration?: boolean |
||||
|
priority?: string |
||||
|
repeatDaily?: boolean |
||||
|
timezone?: string |
||||
|
}) => Promise<void> |
||||
|
|
||||
|
getLastNotification: () => Promise<{ |
||||
|
id: string |
||||
|
title: string |
||||
|
body: string |
||||
|
scheduledTime: number |
||||
|
deliveredAt: number |
||||
|
}> |
||||
|
|
||||
|
checkStatus: () => Promise<{ |
||||
|
canScheduleNow: boolean |
||||
|
postNotificationsGranted: boolean |
||||
|
channelEnabled: boolean |
||||
|
channelImportance: number |
||||
|
channelId: string |
||||
|
exactAlarmsGranted: boolean |
||||
|
exactAlarmsSupported: boolean |
||||
|
androidVersion: number |
||||
|
nextScheduledAt: number |
||||
|
}> |
||||
|
|
||||
|
checkChannelStatus: () => Promise<{ |
||||
|
enabled: boolean |
||||
|
importance: number |
||||
|
id: string |
||||
|
}> |
||||
|
|
||||
|
openChannelSettings: () => Promise<void> |
||||
|
|
||||
|
openExactAlarmSettings: () => Promise<void> |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
export {} |
@ -0,0 +1,73 @@ |
|||||
|
<!DOCTYPE html> |
||||
|
<html lang="en"> |
||||
|
<head> |
||||
|
<meta charset="UTF-8" /> |
||||
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" /> |
||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" /> |
||||
|
<title>Daily Notification Test - Vue 3</title> |
||||
|
|
||||
|
<!-- Capacitor meta tags --> |
||||
|
<meta name="color-scheme" content="light dark" /> |
||||
|
<meta name="format-detection" content="telephone=no" /> |
||||
|
<meta name="msapplication-tap-highlight" content="no" /> |
||||
|
|
||||
|
<!-- PWA meta tags --> |
||||
|
<meta name="theme-color" content="#1976d2" /> |
||||
|
<meta name="apple-mobile-web-app-capable" content="yes" /> |
||||
|
<meta name="apple-mobile-web-app-status-bar-style" content="default" /> |
||||
|
<meta name="apple-mobile-web-app-title" content="Daily Notification Test" /> |
||||
|
|
||||
|
<!-- Styles --> |
||||
|
<style> |
||||
|
body { |
||||
|
margin: 0; |
||||
|
padding: 0; |
||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', |
||||
|
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', |
||||
|
sans-serif; |
||||
|
-webkit-font-smoothing: antialiased; |
||||
|
-moz-osx-font-smoothing: grayscale; |
||||
|
background-color: #f5f5f5; |
||||
|
} |
||||
|
|
||||
|
#app { |
||||
|
min-height: 100vh; |
||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); |
||||
|
} |
||||
|
|
||||
|
/* Loading spinner */ |
||||
|
.loading { |
||||
|
display: flex; |
||||
|
justify-content: center; |
||||
|
align-items: center; |
||||
|
height: 100vh; |
||||
|
color: white; |
||||
|
font-size: 18px; |
||||
|
} |
||||
|
|
||||
|
.spinner { |
||||
|
width: 40px; |
||||
|
height: 40px; |
||||
|
border: 4px solid rgba(255, 255, 255, 0.3); |
||||
|
border-top: 4px solid white; |
||||
|
border-radius: 50%; |
||||
|
animation: spin 1s linear infinite; |
||||
|
margin-right: 16px; |
||||
|
} |
||||
|
|
||||
|
@keyframes spin { |
||||
|
0% { transform: rotate(0deg); } |
||||
|
100% { transform: rotate(360deg); } |
||||
|
} |
||||
|
</style> |
||||
|
</head> |
||||
|
<body> |
||||
|
<div id="app"> |
||||
|
<div class="loading"> |
||||
|
<div class="spinner"></div> |
||||
|
Loading Daily Notification Test App... |
||||
|
</div> |
||||
|
</div> |
||||
|
<script type="module" src="/src/main.ts"></script> |
||||
|
</body> |
||||
|
</html> |
File diff suppressed because it is too large
@ -1,29 +1,42 @@ |
|||||
{ |
{ |
||||
"name": "daily-notification-android-test", |
"name": "daily-notification-android-test", |
||||
"version": "1.0.0", |
"version": "1.0.0", |
||||
"description": "Minimal Android test app for Daily Notification Plugin", |
"description": "Vue 3 + Vite + Capacitor test app for Daily Notification Plugin", |
||||
"main": "index.js", |
"type": "module", |
||||
"scripts": { |
"scripts": { |
||||
"build": "webpack --mode=production", |
"dev": "vite", |
||||
"dev": "webpack serve --mode=development", |
"build": "vite build", |
||||
|
"preview": "vite preview", |
||||
"android": "npx cap run android", |
"android": "npx cap run android", |
||||
"sync": "npx cap sync android", |
"sync": "npx cap sync android", |
||||
"open": "npx cap open android" |
"open": "npx cap open android", |
||||
|
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore", |
||||
|
"type-check": "vue-tsc --noEmit" |
||||
}, |
}, |
||||
"keywords": ["capacitor", "android", "notifications", "test"], |
"keywords": ["capacitor", "android", "notifications", "test", "vue3", "vite", "typescript"], |
||||
"author": "Matthew Raymer", |
"author": "Matthew Raymer", |
||||
"license": "MIT", |
"license": "MIT", |
||||
"dependencies": { |
"dependencies": { |
||||
"@capacitor/core": "^5.0.0", |
"@capacitor/core": "^5.0.0", |
||||
"@capacitor/android": "^5.0.0", |
"@capacitor/android": "^5.0.0", |
||||
"@capacitor/cli": "^5.0.0" |
"@capacitor/cli": "^5.0.0", |
||||
|
"vue": "^3.4.0", |
||||
|
"vue-router": "^4.2.0", |
||||
|
"pinia": "^2.1.0", |
||||
|
"vue-facing-decorator": "^3.0.0" |
||||
}, |
}, |
||||
"devDependencies": { |
"devDependencies": { |
||||
"webpack": "^5.88.0", |
"@capacitor/cli": "^5.0.0", |
||||
"webpack-cli": "^5.1.0", |
"@types/node": "^20.0.0", |
||||
"webpack-dev-server": "^4.15.0", |
"@typescript-eslint/eslint-plugin": "^6.0.0", |
||||
"html-webpack-plugin": "^5.5.0", |
"@typescript-eslint/parser": "^6.0.0", |
||||
"typescript": "^5.0.0", |
"@vitejs/plugin-vue": "^4.5.0", |
||||
"ts-loader": "^9.4.0" |
"@vue/eslint-config-typescript": "^12.0.0", |
||||
|
"@vue/tsconfig": "^0.5.0", |
||||
|
"eslint": "^8.0.0", |
||||
|
"eslint-plugin-vue": "^9.0.0", |
||||
|
"typescript": "~5.3.0", |
||||
|
"vite": "^5.0.0", |
||||
|
"vue-tsc": "^1.8.0" |
||||
} |
} |
||||
} |
} |
@ -0,0 +1,147 @@ |
|||||
|
<!-- |
||||
|
/** |
||||
|
* Main App Component |
||||
|
* |
||||
|
* Vue 3 + vue-facing-decorator + TypeScript |
||||
|
* |
||||
|
* @author Matthew Raymer |
||||
|
* @version 1.0.0 |
||||
|
*/ |
||||
|
--> |
||||
|
|
||||
|
<template> |
||||
|
<div id="app" class="app-container"> |
||||
|
<!-- Header --> |
||||
|
<AppHeader /> |
||||
|
|
||||
|
<!-- Main Content --> |
||||
|
<main class="main-content"> |
||||
|
<router-view /> |
||||
|
</main> |
||||
|
|
||||
|
<!-- Footer --> |
||||
|
<AppFooter /> |
||||
|
|
||||
|
<!-- Global Loading Overlay --> |
||||
|
<LoadingOverlay v-if="isLoading" /> |
||||
|
|
||||
|
<!-- Global Error Dialog --> |
||||
|
<ErrorDialog |
||||
|
v-if="errorMessage" |
||||
|
:message="errorMessage" |
||||
|
@close="clearError" |
||||
|
/> |
||||
|
</div> |
||||
|
</template> |
||||
|
|
||||
|
<script lang="ts"> |
||||
|
import { Component, Vue } from 'vue-facing-decorator' |
||||
|
import { Capacitor } from '@capacitor/core' |
||||
|
import AppHeader from '@/components/layout/AppHeader.vue' |
||||
|
import AppFooter from '@/components/layout/AppFooter.vue' |
||||
|
import LoadingOverlay from '@/components/ui/LoadingOverlay.vue' |
||||
|
import ErrorDialog from '@/components/ui/ErrorDialog.vue' |
||||
|
import { useAppStore } from '@/stores/app' |
||||
|
|
||||
|
@Component({ |
||||
|
components: { |
||||
|
AppHeader, |
||||
|
AppFooter, |
||||
|
LoadingOverlay, |
||||
|
ErrorDialog |
||||
|
} |
||||
|
}) |
||||
|
export default class App extends Vue { |
||||
|
private appStore = useAppStore() |
||||
|
|
||||
|
get isLoading(): boolean { |
||||
|
return this.appStore.isLoading |
||||
|
} |
||||
|
|
||||
|
get errorMessage(): string | null { |
||||
|
return this.appStore.errorMessage |
||||
|
} |
||||
|
|
||||
|
mounted(): void { |
||||
|
this.initializeApp() |
||||
|
} |
||||
|
|
||||
|
private async initializeApp(): Promise<void> { |
||||
|
try { |
||||
|
this.appStore.setLoading(true) |
||||
|
|
||||
|
// Initialize Capacitor plugins |
||||
|
await this.initializeCapacitor() |
||||
|
|
||||
|
// Check notification permissions |
||||
|
await this.checkNotificationPermissions() |
||||
|
|
||||
|
console.log('✅ App initialized successfully') |
||||
|
|
||||
|
} catch (error) { |
||||
|
console.error('❌ App initialization failed:', error) |
||||
|
this.appStore.setError('Failed to initialize app: ' + (error as Error).message) |
||||
|
} finally { |
||||
|
this.appStore.setLoading(false) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private async initializeCapacitor(): Promise<void> { |
||||
|
if (Capacitor.isNativePlatform()) { |
||||
|
console.log('📱 Initializing Capacitor for native platform') |
||||
|
|
||||
|
// Check if DailyNotification plugin is available |
||||
|
if (!window.DailyNotification) { |
||||
|
console.warn('⚠️ DailyNotification plugin not loaded - buttons will show error messages') |
||||
|
this.appStore.setError('DailyNotification plugin not loaded. Please restart the app to enable full functionality.') |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
console.log('✅ DailyNotification plugin available') |
||||
|
} else { |
||||
|
console.log('🌐 Running in web mode - DailyNotification plugin not available') |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private async checkNotificationPermissions(): Promise<void> { |
||||
|
if (Capacitor.isNativePlatform() && window.DailyNotification) { |
||||
|
try { |
||||
|
const status = await window.DailyNotification.checkStatus() |
||||
|
console.log('📊 Notification Status:', status) |
||||
|
|
||||
|
if (!status.canScheduleNow) { |
||||
|
console.warn('⚠️ Notifications not fully configured') |
||||
|
} |
||||
|
} catch (error) { |
||||
|
console.warn('⚠️ Could not check notification status:', error) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
clearError(): void { |
||||
|
this.appStore.clearError() |
||||
|
} |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<style scoped> |
||||
|
.app-container { |
||||
|
min-height: 100vh; |
||||
|
display: flex; |
||||
|
flex-direction: column; |
||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); |
||||
|
} |
||||
|
|
||||
|
.main-content { |
||||
|
flex: 1; |
||||
|
padding: 16px; |
||||
|
overflow-y: auto; |
||||
|
} |
||||
|
|
||||
|
/* Responsive design */ |
||||
|
@media (max-width: 768px) { |
||||
|
.main-content { |
||||
|
padding: 8px; |
||||
|
} |
||||
|
} |
||||
|
</style> |
@ -0,0 +1,163 @@ |
|||||
|
<!-- |
||||
|
/** |
||||
|
* Action Card Component |
||||
|
* |
||||
|
* Reusable card component for quick actions |
||||
|
* |
||||
|
* @author Matthew Raymer |
||||
|
* @version 1.0.0 |
||||
|
*/ |
||||
|
--> |
||||
|
|
||||
|
<template> |
||||
|
<div |
||||
|
class="action-card" |
||||
|
:class="{ 'loading': loading, 'disabled': disabled }" |
||||
|
@click="handleClick" |
||||
|
> |
||||
|
<div class="card-content"> |
||||
|
<div class="icon-container"> |
||||
|
<span class="icon">{{ icon }}</span> |
||||
|
</div> |
||||
|
<div class="text-content"> |
||||
|
<h3 class="title">{{ title }}</h3> |
||||
|
<p class="description">{{ description }}</p> |
||||
|
</div> |
||||
|
<div class="action-indicator"> |
||||
|
<span v-if="loading" class="spinner"></span> |
||||
|
<span v-else class="arrow">→</span> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</template> |
||||
|
|
||||
|
<script lang="ts"> |
||||
|
import { Component, Vue, Prop } from 'vue-facing-decorator' |
||||
|
|
||||
|
@Component |
||||
|
export default class ActionCard extends Vue { |
||||
|
@Prop({ required: true }) icon!: string |
||||
|
@Prop({ required: true }) title!: string |
||||
|
@Prop({ required: true }) description!: string |
||||
|
@Prop({ default: false }) loading!: boolean |
||||
|
@Prop({ default: false }) disabled!: boolean |
||||
|
|
||||
|
handleClick(): void { |
||||
|
if (!this.loading && !this.disabled) { |
||||
|
this.$emit('click') |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<style scoped> |
||||
|
.action-card { |
||||
|
background: rgba(255, 255, 255, 0.1); |
||||
|
border-radius: 12px; |
||||
|
padding: 20px; |
||||
|
cursor: pointer; |
||||
|
transition: all 0.3s ease; |
||||
|
backdrop-filter: blur(10px); |
||||
|
border: 1px solid rgba(255, 255, 255, 0.2); |
||||
|
} |
||||
|
|
||||
|
.action-card:hover:not(.disabled) { |
||||
|
background: rgba(255, 255, 255, 0.15); |
||||
|
transform: translateY(-2px); |
||||
|
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.2); |
||||
|
} |
||||
|
|
||||
|
.action-card:active:not(.disabled) { |
||||
|
transform: translateY(0); |
||||
|
} |
||||
|
|
||||
|
.action-card.loading { |
||||
|
cursor: wait; |
||||
|
opacity: 0.7; |
||||
|
} |
||||
|
|
||||
|
.action-card.disabled { |
||||
|
cursor: not-allowed; |
||||
|
opacity: 0.5; |
||||
|
} |
||||
|
|
||||
|
.card-content { |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
gap: 16px; |
||||
|
} |
||||
|
|
||||
|
.icon-container { |
||||
|
flex-shrink: 0; |
||||
|
} |
||||
|
|
||||
|
.icon { |
||||
|
font-size: 2rem; |
||||
|
display: block; |
||||
|
} |
||||
|
|
||||
|
.text-content { |
||||
|
flex: 1; |
||||
|
min-width: 0; |
||||
|
} |
||||
|
|
||||
|
.title { |
||||
|
font-size: 1.1rem; |
||||
|
font-weight: 600; |
||||
|
color: white; |
||||
|
margin: 0 0 4px 0; |
||||
|
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3); |
||||
|
} |
||||
|
|
||||
|
.description { |
||||
|
font-size: 0.9rem; |
||||
|
color: rgba(255, 255, 255, 0.8); |
||||
|
margin: 0; |
||||
|
line-height: 1.4; |
||||
|
} |
||||
|
|
||||
|
.action-indicator { |
||||
|
flex-shrink: 0; |
||||
|
color: rgba(255, 255, 255, 0.7); |
||||
|
font-size: 1.2rem; |
||||
|
font-weight: bold; |
||||
|
} |
||||
|
|
||||
|
.spinner { |
||||
|
display: inline-block; |
||||
|
width: 16px; |
||||
|
height: 16px; |
||||
|
border: 2px solid rgba(255, 255, 255, 0.3); |
||||
|
border-top: 2px solid white; |
||||
|
border-radius: 50%; |
||||
|
animation: spin 1s linear infinite; |
||||
|
} |
||||
|
|
||||
|
@keyframes spin { |
||||
|
0% { transform: rotate(0deg); } |
||||
|
100% { transform: rotate(360deg); } |
||||
|
} |
||||
|
|
||||
|
/* Responsive design */ |
||||
|
@media (max-width: 768px) { |
||||
|
.action-card { |
||||
|
padding: 16px; |
||||
|
} |
||||
|
|
||||
|
.card-content { |
||||
|
gap: 12px; |
||||
|
} |
||||
|
|
||||
|
.icon { |
||||
|
font-size: 1.5rem; |
||||
|
} |
||||
|
|
||||
|
.title { |
||||
|
font-size: 1rem; |
||||
|
} |
||||
|
|
||||
|
.description { |
||||
|
font-size: 0.85rem; |
||||
|
} |
||||
|
} |
||||
|
</style> |
@ -0,0 +1,95 @@ |
|||||
|
<!-- |
||||
|
/** |
||||
|
* Info Card Component |
||||
|
* |
||||
|
* Simple information display card |
||||
|
* |
||||
|
* @author Matthew Raymer |
||||
|
* @version 1.0.0 |
||||
|
*/ |
||||
|
--> |
||||
|
|
||||
|
<template> |
||||
|
<div class="info-card"> |
||||
|
<div class="info-header"> |
||||
|
<span class="info-icon">{{ icon }}</span> |
||||
|
<h3 class="info-title">{{ title }}</h3> |
||||
|
</div> |
||||
|
<div class="info-content"> |
||||
|
<p class="info-value">{{ value }}</p> |
||||
|
</div> |
||||
|
</div> |
||||
|
</template> |
||||
|
|
||||
|
<script lang="ts"> |
||||
|
import { Component, Vue, Prop } from 'vue-facing-decorator' |
||||
|
|
||||
|
@Component |
||||
|
export default class InfoCard extends Vue { |
||||
|
@Prop({ required: true }) title!: string |
||||
|
@Prop({ required: true }) value!: string |
||||
|
@Prop({ required: true }) icon!: string |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<style scoped> |
||||
|
.info-card { |
||||
|
background: rgba(255, 255, 255, 0.1); |
||||
|
border-radius: 12px; |
||||
|
padding: 16px; |
||||
|
backdrop-filter: blur(10px); |
||||
|
border: 1px solid rgba(255, 255, 255, 0.2); |
||||
|
transition: all 0.3s ease; |
||||
|
} |
||||
|
|
||||
|
.info-card:hover { |
||||
|
background: rgba(255, 255, 255, 0.15); |
||||
|
} |
||||
|
|
||||
|
.info-header { |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
gap: 8px; |
||||
|
margin-bottom: 12px; |
||||
|
} |
||||
|
|
||||
|
.info-icon { |
||||
|
font-size: 1.2rem; |
||||
|
} |
||||
|
|
||||
|
.info-title { |
||||
|
font-size: 0.9rem; |
||||
|
font-weight: 600; |
||||
|
color: rgba(255, 255, 255, 0.9); |
||||
|
margin: 0; |
||||
|
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3); |
||||
|
} |
||||
|
|
||||
|
.info-content { |
||||
|
margin: 0; |
||||
|
} |
||||
|
|
||||
|
.info-value { |
||||
|
font-size: 1rem; |
||||
|
font-weight: 500; |
||||
|
color: white; |
||||
|
margin: 0; |
||||
|
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3); |
||||
|
word-break: break-word; |
||||
|
} |
||||
|
|
||||
|
/* Responsive design */ |
||||
|
@media (max-width: 768px) { |
||||
|
.info-card { |
||||
|
padding: 12px; |
||||
|
} |
||||
|
|
||||
|
.info-title { |
||||
|
font-size: 0.85rem; |
||||
|
} |
||||
|
|
||||
|
.info-value { |
||||
|
font-size: 0.9rem; |
||||
|
} |
||||
|
} |
||||
|
</style> |
@ -0,0 +1,181 @@ |
|||||
|
<!-- |
||||
|
/** |
||||
|
* Notification Card Component |
||||
|
* |
||||
|
* Displays notification information |
||||
|
* |
||||
|
* @author Matthew Raymer |
||||
|
* @version 1.0.0 |
||||
|
*/ |
||||
|
--> |
||||
|
|
||||
|
<template> |
||||
|
<div class="notification-card"> |
||||
|
<div class="notification-header"> |
||||
|
<h3 class="notification-title">{{ notification.title }}</h3> |
||||
|
<div class="notification-status" :class="statusClass"> |
||||
|
{{ statusText }} |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<div class="notification-content"> |
||||
|
<p class="notification-body">{{ notification.body }}</p> |
||||
|
</div> |
||||
|
|
||||
|
<div class="notification-footer"> |
||||
|
<div class="notification-time"> |
||||
|
<span class="time-label">Scheduled:</span> |
||||
|
<span class="time-value">{{ formatScheduledTime }}</span> |
||||
|
</div> |
||||
|
|
||||
|
<div v-if="notification.deliveredAt" class="notification-delivered"> |
||||
|
<span class="delivered-label">Delivered:</span> |
||||
|
<span class="delivered-value">{{ formatDeliveredTime }}</span> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</template> |
||||
|
|
||||
|
<script lang="ts"> |
||||
|
import { Component, Vue, Prop } from 'vue-facing-decorator' |
||||
|
import type { ScheduledNotification } from '@/stores/notifications' |
||||
|
|
||||
|
@Component |
||||
|
export default class NotificationCard extends Vue { |
||||
|
@Prop({ required: true }) notification!: ScheduledNotification |
||||
|
|
||||
|
get statusClass(): string { |
||||
|
return `status-${this.notification.status}` |
||||
|
} |
||||
|
|
||||
|
get statusText(): string { |
||||
|
const statusMap = { |
||||
|
scheduled: 'Scheduled', |
||||
|
delivered: 'Delivered', |
||||
|
cancelled: 'Cancelled' |
||||
|
} |
||||
|
return statusMap[this.notification.status] |
||||
|
} |
||||
|
|
||||
|
get formatScheduledTime(): string { |
||||
|
const date = new Date(this.notification.scheduledTime) |
||||
|
return date.toLocaleString() |
||||
|
} |
||||
|
|
||||
|
get formatDeliveredTime(): string { |
||||
|
if (!this.notification.deliveredAt) return 'Not delivered' |
||||
|
const date = new Date(this.notification.deliveredAt) |
||||
|
return date.toLocaleString() |
||||
|
} |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<style scoped> |
||||
|
.notification-card { |
||||
|
background: rgba(255, 255, 255, 0.1); |
||||
|
border-radius: 12px; |
||||
|
padding: 16px; |
||||
|
backdrop-filter: blur(10px); |
||||
|
border: 1px solid rgba(255, 255, 255, 0.2); |
||||
|
} |
||||
|
|
||||
|
.notification-header { |
||||
|
display: flex; |
||||
|
justify-content: space-between; |
||||
|
align-items: flex-start; |
||||
|
margin-bottom: 12px; |
||||
|
} |
||||
|
|
||||
|
.notification-title { |
||||
|
font-size: 1.1rem; |
||||
|
font-weight: 600; |
||||
|
color: white; |
||||
|
margin: 0; |
||||
|
flex: 1; |
||||
|
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3); |
||||
|
} |
||||
|
|
||||
|
.notification-status { |
||||
|
padding: 4px 8px; |
||||
|
border-radius: 12px; |
||||
|
font-size: 0.75rem; |
||||
|
font-weight: 600; |
||||
|
text-transform: uppercase; |
||||
|
letter-spacing: 0.5px; |
||||
|
} |
||||
|
|
||||
|
.status-scheduled { |
||||
|
background: rgba(33, 150, 243, 0.2); |
||||
|
color: #2196f3; |
||||
|
border: 1px solid rgba(33, 150, 243, 0.3); |
||||
|
} |
||||
|
|
||||
|
.status-delivered { |
||||
|
background: rgba(76, 175, 80, 0.2); |
||||
|
color: #4caf50; |
||||
|
border: 1px solid rgba(76, 175, 80, 0.3); |
||||
|
} |
||||
|
|
||||
|
.status-cancelled { |
||||
|
background: rgba(158, 158, 158, 0.2); |
||||
|
color: #9e9e9e; |
||||
|
border: 1px solid rgba(158, 158, 158, 0.3); |
||||
|
} |
||||
|
|
||||
|
.notification-content { |
||||
|
margin-bottom: 16px; |
||||
|
} |
||||
|
|
||||
|
.notification-body { |
||||
|
color: rgba(255, 255, 255, 0.9); |
||||
|
font-size: 0.95rem; |
||||
|
line-height: 1.5; |
||||
|
margin: 0; |
||||
|
} |
||||
|
|
||||
|
.notification-footer { |
||||
|
display: flex; |
||||
|
flex-direction: column; |
||||
|
gap: 8px; |
||||
|
padding-top: 12px; |
||||
|
border-top: 1px solid rgba(255, 255, 255, 0.1); |
||||
|
} |
||||
|
|
||||
|
.notification-time, |
||||
|
.notification-delivered { |
||||
|
display: flex; |
||||
|
justify-content: space-between; |
||||
|
align-items: center; |
||||
|
} |
||||
|
|
||||
|
.time-label, |
||||
|
.delivered-label { |
||||
|
font-size: 0.8rem; |
||||
|
color: rgba(255, 255, 255, 0.7); |
||||
|
font-weight: 500; |
||||
|
} |
||||
|
|
||||
|
.time-value, |
||||
|
.delivered-value { |
||||
|
font-size: 0.8rem; |
||||
|
color: rgba(255, 255, 255, 0.9); |
||||
|
font-family: 'Courier New', monospace; |
||||
|
} |
||||
|
|
||||
|
/* Responsive design */ |
||||
|
@media (max-width: 768px) { |
||||
|
.notification-card { |
||||
|
padding: 12px; |
||||
|
} |
||||
|
|
||||
|
.notification-header { |
||||
|
flex-direction: column; |
||||
|
align-items: flex-start; |
||||
|
gap: 8px; |
||||
|
} |
||||
|
|
||||
|
.notification-footer { |
||||
|
gap: 6px; |
||||
|
} |
||||
|
} |
||||
|
</style> |
@ -0,0 +1,264 @@ |
|||||
|
<!-- |
||||
|
/** |
||||
|
* Status Card Component |
||||
|
* |
||||
|
* Displays notification system status with visual indicators |
||||
|
* |
||||
|
* @author Matthew Raymer |
||||
|
* @version 1.0.0 |
||||
|
*/ |
||||
|
--> |
||||
|
|
||||
|
<template> |
||||
|
<div class="status-card"> |
||||
|
<div class="status-header"> |
||||
|
<h3 class="status-title">System Status</h3> |
||||
|
<div class="status-indicator" :class="statusClass"> |
||||
|
<span class="status-dot"></span> |
||||
|
<span class="status-text">{{ statusText }}</span> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<div v-if="status" class="status-details"> |
||||
|
<div class="status-grid"> |
||||
|
<StatusItem |
||||
|
label="Notifications" |
||||
|
:value="status.postNotificationsGranted ? 'Granted' : 'Denied'" |
||||
|
:status="status.postNotificationsGranted ? 'success' : 'error'" |
||||
|
/> |
||||
|
<StatusItem |
||||
|
label="Channel" |
||||
|
:value="channelStatusText" |
||||
|
:status="status.channelEnabled ? 'success' : 'warning'" |
||||
|
/> |
||||
|
<StatusItem |
||||
|
label="Exact Alarms" |
||||
|
:value="status.exactAlarmsGranted ? 'Granted' : 'Denied'" |
||||
|
:status="status.exactAlarmsGranted ? 'success' : 'error'" |
||||
|
/> |
||||
|
<StatusItem |
||||
|
label="Android Version" |
||||
|
:value="`API ${status.androidVersion}`" |
||||
|
status="info" |
||||
|
/> |
||||
|
</div> |
||||
|
|
||||
|
<div v-if="status.nextScheduledAt > 0" class="next-scheduled"> |
||||
|
<h4 class="next-title">Next Scheduled</h4> |
||||
|
<p class="next-time">{{ formatNextScheduledTime }}</p> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<div v-else class="no-status"> |
||||
|
<p class="no-status-text">Status not available</p> |
||||
|
<button class="refresh-button" @click="refreshStatus"> |
||||
|
🔄 Refresh |
||||
|
</button> |
||||
|
</div> |
||||
|
</div> |
||||
|
</template> |
||||
|
|
||||
|
<script lang="ts"> |
||||
|
import { Component, Vue, Prop } from 'vue-facing-decorator' |
||||
|
import type { NotificationStatus } from '@/stores/app' |
||||
|
import StatusItem from '@/components/items/StatusItem.vue' |
||||
|
|
||||
|
@Component({ |
||||
|
components: { |
||||
|
StatusItem |
||||
|
} |
||||
|
}) |
||||
|
export default class StatusCard extends Vue { |
||||
|
@Prop() status!: NotificationStatus | null |
||||
|
|
||||
|
get statusClass(): string { |
||||
|
if (!this.status) return 'unknown' |
||||
|
if (this.status.canScheduleNow) return 'ready' |
||||
|
return 'not-ready' |
||||
|
} |
||||
|
|
||||
|
get statusText(): string { |
||||
|
if (!this.status) return 'Unknown' |
||||
|
if (this.status.canScheduleNow) return 'Ready' |
||||
|
return 'Not Ready' |
||||
|
} |
||||
|
|
||||
|
get channelStatusText(): string { |
||||
|
if (!this.status) return 'Unknown' |
||||
|
if (!this.status.channelEnabled) return 'Disabled' |
||||
|
|
||||
|
const importanceMap: Record<number, string> = { |
||||
|
0: 'None', |
||||
|
1: 'Min', |
||||
|
2: 'Low', |
||||
|
3: 'Default', |
||||
|
4: 'High', |
||||
|
5: 'Max' |
||||
|
} |
||||
|
|
||||
|
return importanceMap[this.status.channelImportance] || 'Unknown' |
||||
|
} |
||||
|
|
||||
|
get formatNextScheduledTime(): string { |
||||
|
if (!this.status || this.status.nextScheduledAt <= 0) { |
||||
|
return 'No notifications scheduled' |
||||
|
} |
||||
|
|
||||
|
const date = new Date(this.status.nextScheduledAt) |
||||
|
const now = new Date() |
||||
|
const diffMs = date.getTime() - now.getTime() |
||||
|
|
||||
|
if (diffMs < 0) { |
||||
|
return 'Past due' |
||||
|
} |
||||
|
|
||||
|
const diffHours = Math.floor(diffMs / (1000 * 60 * 60)) |
||||
|
const diffMinutes = Math.floor((diffMs % (1000 * 60 * 60)) / (1000 * 60)) |
||||
|
|
||||
|
if (diffHours > 24) { |
||||
|
const diffDays = Math.floor(diffHours / 24) |
||||
|
return `In ${diffDays} day${diffDays > 1 ? 's' : ''}` |
||||
|
} else if (diffHours > 0) { |
||||
|
return `In ${diffHours}h ${diffMinutes}m` |
||||
|
} else { |
||||
|
return `In ${diffMinutes} minutes` |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
refreshStatus(): void { |
||||
|
this.$emit('refresh') |
||||
|
} |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<style scoped> |
||||
|
.status-card { |
||||
|
background: rgba(255, 255, 255, 0.1); |
||||
|
border-radius: 12px; |
||||
|
padding: 20px; |
||||
|
backdrop-filter: blur(10px); |
||||
|
border: 1px solid rgba(255, 255, 255, 0.2); |
||||
|
} |
||||
|
|
||||
|
.status-header { |
||||
|
display: flex; |
||||
|
justify-content: space-between; |
||||
|
align-items: center; |
||||
|
margin-bottom: 16px; |
||||
|
} |
||||
|
|
||||
|
.status-title { |
||||
|
font-size: 1.2rem; |
||||
|
font-weight: 600; |
||||
|
color: white; |
||||
|
margin: 0; |
||||
|
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3); |
||||
|
} |
||||
|
|
||||
|
.status-indicator { |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
gap: 8px; |
||||
|
padding: 6px 12px; |
||||
|
border-radius: 20px; |
||||
|
font-size: 0.9rem; |
||||
|
font-weight: 500; |
||||
|
} |
||||
|
|
||||
|
.status-indicator.ready { |
||||
|
background: rgba(76, 175, 80, 0.2); |
||||
|
color: #4caf50; |
||||
|
border: 1px solid rgba(76, 175, 80, 0.3); |
||||
|
} |
||||
|
|
||||
|
.status-indicator.not-ready { |
||||
|
background: rgba(244, 67, 54, 0.2); |
||||
|
color: #f44336; |
||||
|
border: 1px solid rgba(244, 67, 54, 0.3); |
||||
|
} |
||||
|
|
||||
|
.status-indicator.unknown { |
||||
|
background: rgba(158, 158, 158, 0.2); |
||||
|
color: #9e9e9e; |
||||
|
border: 1px solid rgba(158, 158, 158, 0.3); |
||||
|
} |
||||
|
|
||||
|
.status-dot { |
||||
|
width: 8px; |
||||
|
height: 8px; |
||||
|
border-radius: 50%; |
||||
|
background: currentColor; |
||||
|
} |
||||
|
|
||||
|
.status-details { |
||||
|
space-y: 16px; |
||||
|
} |
||||
|
|
||||
|
.status-grid { |
||||
|
display: grid; |
||||
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); |
||||
|
gap: 12px; |
||||
|
margin-bottom: 16px; |
||||
|
} |
||||
|
|
||||
|
.next-scheduled { |
||||
|
padding-top: 16px; |
||||
|
border-top: 1px solid rgba(255, 255, 255, 0.1); |
||||
|
} |
||||
|
|
||||
|
.next-title { |
||||
|
font-size: 1rem; |
||||
|
font-weight: 600; |
||||
|
color: white; |
||||
|
margin: 0 0 8px 0; |
||||
|
} |
||||
|
|
||||
|
.next-time { |
||||
|
font-size: 0.9rem; |
||||
|
color: rgba(255, 255, 255, 0.8); |
||||
|
margin: 0; |
||||
|
font-family: 'Courier New', monospace; |
||||
|
} |
||||
|
|
||||
|
.no-status { |
||||
|
text-align: center; |
||||
|
padding: 20px; |
||||
|
} |
||||
|
|
||||
|
.no-status-text { |
||||
|
color: rgba(255, 255, 255, 0.7); |
||||
|
margin: 0 0 16px 0; |
||||
|
} |
||||
|
|
||||
|
.refresh-button { |
||||
|
background: rgba(255, 255, 255, 0.1); |
||||
|
border: 1px solid rgba(255, 255, 255, 0.2); |
||||
|
color: white; |
||||
|
padding: 8px 16px; |
||||
|
border-radius: 8px; |
||||
|
cursor: pointer; |
||||
|
font-size: 0.9rem; |
||||
|
transition: all 0.3s ease; |
||||
|
} |
||||
|
|
||||
|
.refresh-button:hover { |
||||
|
background: rgba(255, 255, 255, 0.15); |
||||
|
} |
||||
|
|
||||
|
/* Responsive design */ |
||||
|
@media (max-width: 768px) { |
||||
|
.status-card { |
||||
|
padding: 16px; |
||||
|
} |
||||
|
|
||||
|
.status-header { |
||||
|
flex-direction: column; |
||||
|
align-items: flex-start; |
||||
|
gap: 12px; |
||||
|
} |
||||
|
|
||||
|
.status-grid { |
||||
|
grid-template-columns: 1fr; |
||||
|
} |
||||
|
} |
||||
|
</style> |
@ -0,0 +1,141 @@ |
|||||
|
<!-- |
||||
|
/** |
||||
|
* Activity Item Component |
||||
|
* |
||||
|
* Individual activity history item |
||||
|
* |
||||
|
* @author Matthew Raymer |
||||
|
* @version 1.0.0 |
||||
|
*/ |
||||
|
--> |
||||
|
|
||||
|
<template> |
||||
|
<div class="activity-item"> |
||||
|
<div class="activity-icon"> |
||||
|
<span>{{ activityIcon }}</span> |
||||
|
</div> |
||||
|
<div class="activity-content"> |
||||
|
<div class="activity-title">{{ activity.title }}</div> |
||||
|
<div class="activity-time">{{ formatActivityTime }}</div> |
||||
|
</div> |
||||
|
<div class="activity-status" :class="statusClass"> |
||||
|
<span class="status-dot"></span> |
||||
|
</div> |
||||
|
</div> |
||||
|
</template> |
||||
|
|
||||
|
<script lang="ts"> |
||||
|
import { Component, Vue, Prop } from 'vue-facing-decorator' |
||||
|
import type { NotificationHistory } from '@/stores/notifications' |
||||
|
|
||||
|
@Component |
||||
|
export default class ActivityItem extends Vue { |
||||
|
@Prop({ required: true }) activity!: NotificationHistory |
||||
|
|
||||
|
get activityIcon(): string { |
||||
|
if (this.activity.clicked) return '👆' |
||||
|
if (this.activity.dismissed) return '❌' |
||||
|
return '📱' |
||||
|
} |
||||
|
|
||||
|
get statusClass(): string { |
||||
|
if (this.activity.clicked) return 'clicked' |
||||
|
if (this.activity.dismissed) return 'dismissed' |
||||
|
return 'delivered' |
||||
|
} |
||||
|
|
||||
|
get formatActivityTime(): string { |
||||
|
const date = new Date(this.activity.deliveredAt) |
||||
|
const now = new Date() |
||||
|
const diffMs = now.getTime() - date.getTime() |
||||
|
|
||||
|
if (diffMs < 60000) { // Less than 1 minute |
||||
|
return 'Just now' |
||||
|
} else if (diffMs < 3600000) { // Less than 1 hour |
||||
|
const minutes = Math.floor(diffMs / 60000) |
||||
|
return `${minutes}m ago` |
||||
|
} else if (diffMs < 86400000) { // Less than 1 day |
||||
|
const hours = Math.floor(diffMs / 3600000) |
||||
|
return `${hours}h ago` |
||||
|
} else { |
||||
|
return date.toLocaleDateString() |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<style scoped> |
||||
|
.activity-item { |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
gap: 12px; |
||||
|
padding: 12px; |
||||
|
background: rgba(255, 255, 255, 0.05); |
||||
|
border-radius: 8px; |
||||
|
border: 1px solid rgba(255, 255, 255, 0.1); |
||||
|
transition: all 0.3s ease; |
||||
|
} |
||||
|
|
||||
|
.activity-item:hover { |
||||
|
background: rgba(255, 255, 255, 0.08); |
||||
|
} |
||||
|
|
||||
|
.activity-icon { |
||||
|
flex-shrink: 0; |
||||
|
width: 32px; |
||||
|
height: 32px; |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
justify-content: center; |
||||
|
background: rgba(255, 255, 255, 0.1); |
||||
|
border-radius: 50%; |
||||
|
font-size: 1.2rem; |
||||
|
} |
||||
|
|
||||
|
.activity-content { |
||||
|
flex: 1; |
||||
|
min-width: 0; |
||||
|
} |
||||
|
|
||||
|
.activity-title { |
||||
|
font-size: 0.9rem; |
||||
|
font-weight: 500; |
||||
|
color: white; |
||||
|
margin: 0 0 4px 0; |
||||
|
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3); |
||||
|
overflow: hidden; |
||||
|
text-overflow: ellipsis; |
||||
|
white-space: nowrap; |
||||
|
} |
||||
|
|
||||
|
.activity-time { |
||||
|
font-size: 0.8rem; |
||||
|
color: rgba(255, 255, 255, 0.7); |
||||
|
margin: 0; |
||||
|
} |
||||
|
|
||||
|
.activity-status { |
||||
|
flex-shrink: 0; |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
gap: 4px; |
||||
|
} |
||||
|
|
||||
|
.status-dot { |
||||
|
width: 8px; |
||||
|
height: 8px; |
||||
|
border-radius: 50%; |
||||
|
} |
||||
|
|
||||
|
.status-clicked .status-dot { |
||||
|
background: #4caf50; |
||||
|
} |
||||
|
|
||||
|
.status-dismissed .status-dot { |
||||
|
background: #f44336; |
||||
|
} |
||||
|
|
||||
|
.status-delivered .status-dot { |
||||
|
background: #2196f3; |
||||
|
} |
||||
|
</style> |
@ -0,0 +1,170 @@ |
|||||
|
<!-- |
||||
|
/** |
||||
|
* History Item Component |
||||
|
* |
||||
|
* Individual notification history item |
||||
|
* |
||||
|
* @author Matthew Raymer |
||||
|
* @version 1.0.0 |
||||
|
*/ |
||||
|
--> |
||||
|
|
||||
|
<template> |
||||
|
<div class="history-item"> |
||||
|
<div class="history-icon"> |
||||
|
<span>{{ historyIcon }}</span> |
||||
|
</div> |
||||
|
<div class="history-content"> |
||||
|
<div class="history-title">{{ history.title }}</div> |
||||
|
<div class="history-body">{{ history.body }}</div> |
||||
|
<div class="history-time">{{ formatHistoryTime }}</div> |
||||
|
</div> |
||||
|
<div class="history-actions"> |
||||
|
<span class="action-indicator" :class="actionClass"> |
||||
|
{{ actionText }} |
||||
|
</span> |
||||
|
</div> |
||||
|
</div> |
||||
|
</template> |
||||
|
|
||||
|
<script lang="ts"> |
||||
|
import { Component, Vue, Prop } from 'vue-facing-decorator' |
||||
|
import type { NotificationHistory } from '@/stores/notifications' |
||||
|
|
||||
|
@Component |
||||
|
export default class HistoryItem extends Vue { |
||||
|
@Prop({ required: true }) history!: NotificationHistory |
||||
|
|
||||
|
get historyIcon(): string { |
||||
|
if (this.history.clicked) return '👆' |
||||
|
if (this.history.dismissed) return '❌' |
||||
|
return '📱' |
||||
|
} |
||||
|
|
||||
|
get actionClass(): string { |
||||
|
if (this.history.clicked) return 'clicked' |
||||
|
if (this.history.dismissed) return 'dismissed' |
||||
|
return 'delivered' |
||||
|
} |
||||
|
|
||||
|
get actionText(): string { |
||||
|
if (this.history.clicked) return 'Clicked' |
||||
|
if (this.history.dismissed) return 'Dismissed' |
||||
|
return 'Delivered' |
||||
|
} |
||||
|
|
||||
|
get formatHistoryTime(): string { |
||||
|
const date = new Date(this.history.deliveredAt) |
||||
|
return date.toLocaleString() |
||||
|
} |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<style scoped> |
||||
|
.history-item { |
||||
|
display: flex; |
||||
|
align-items: flex-start; |
||||
|
gap: 16px; |
||||
|
padding: 16px; |
||||
|
background: rgba(255, 255, 255, 0.1); |
||||
|
border-radius: 12px; |
||||
|
backdrop-filter: blur(10px); |
||||
|
border: 1px solid rgba(255, 255, 255, 0.2); |
||||
|
transition: all 0.3s ease; |
||||
|
} |
||||
|
|
||||
|
.history-item:hover { |
||||
|
background: rgba(255, 255, 255, 0.15); |
||||
|
} |
||||
|
|
||||
|
.history-icon { |
||||
|
flex-shrink: 0; |
||||
|
width: 40px; |
||||
|
height: 40px; |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
justify-content: center; |
||||
|
background: rgba(255, 255, 255, 0.1); |
||||
|
border-radius: 50%; |
||||
|
font-size: 1.5rem; |
||||
|
} |
||||
|
|
||||
|
.history-content { |
||||
|
flex: 1; |
||||
|
min-width: 0; |
||||
|
} |
||||
|
|
||||
|
.history-title { |
||||
|
font-size: 1rem; |
||||
|
font-weight: 600; |
||||
|
color: white; |
||||
|
margin: 0 0 4px 0; |
||||
|
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3); |
||||
|
} |
||||
|
|
||||
|
.history-body { |
||||
|
font-size: 0.9rem; |
||||
|
color: rgba(255, 255, 255, 0.8); |
||||
|
margin: 0 0 8px 0; |
||||
|
line-height: 1.4; |
||||
|
} |
||||
|
|
||||
|
.history-time { |
||||
|
font-size: 0.8rem; |
||||
|
color: rgba(255, 255, 255, 0.6); |
||||
|
font-family: 'Courier New', monospace; |
||||
|
} |
||||
|
|
||||
|
.history-actions { |
||||
|
flex-shrink: 0; |
||||
|
} |
||||
|
|
||||
|
.action-indicator { |
||||
|
padding: 4px 8px; |
||||
|
border-radius: 12px; |
||||
|
font-size: 0.75rem; |
||||
|
font-weight: 600; |
||||
|
text-transform: uppercase; |
||||
|
letter-spacing: 0.5px; |
||||
|
} |
||||
|
|
||||
|
.action-indicator.clicked { |
||||
|
background: rgba(76, 175, 80, 0.2); |
||||
|
color: #4caf50; |
||||
|
border: 1px solid rgba(76, 175, 80, 0.3); |
||||
|
} |
||||
|
|
||||
|
.action-indicator.dismissed { |
||||
|
background: rgba(244, 67, 54, 0.2); |
||||
|
color: #f44336; |
||||
|
border: 1px solid rgba(244, 67, 54, 0.3); |
||||
|
} |
||||
|
|
||||
|
.action-indicator.delivered { |
||||
|
background: rgba(33, 150, 243, 0.2); |
||||
|
color: #2196f3; |
||||
|
border: 1px solid rgba(33, 150, 243, 0.3); |
||||
|
} |
||||
|
|
||||
|
/* Responsive design */ |
||||
|
@media (max-width: 768px) { |
||||
|
.history-item { |
||||
|
padding: 12px; |
||||
|
gap: 12px; |
||||
|
} |
||||
|
|
||||
|
.history-icon { |
||||
|
width: 32px; |
||||
|
height: 32px; |
||||
|
font-size: 1.2rem; |
||||
|
} |
||||
|
|
||||
|
.history-title { |
||||
|
font-size: 0.9rem; |
||||
|
} |
||||
|
|
||||
|
.history-body { |
||||
|
font-size: 0.85rem; |
||||
|
} |
||||
|
} |
||||
|
</style> |
@ -0,0 +1,99 @@ |
|||||
|
<!-- |
||||
|
/** |
||||
|
* Status Item Component |
||||
|
* |
||||
|
* Individual status display item |
||||
|
* |
||||
|
* @author Matthew Raymer |
||||
|
* @version 1.0.0 |
||||
|
*/ |
||||
|
--> |
||||
|
|
||||
|
<template> |
||||
|
<div class="status-item"> |
||||
|
<div class="status-label">{{ label }}</div> |
||||
|
<div class="status-value" :class="statusClass"> |
||||
|
<span class="status-dot"></span> |
||||
|
{{ value }} |
||||
|
</div> |
||||
|
</div> |
||||
|
</template> |
||||
|
|
||||
|
<script lang="ts"> |
||||
|
import { Component, Vue, Prop } from 'vue-facing-decorator' |
||||
|
|
||||
|
@Component |
||||
|
export default class StatusItem extends Vue { |
||||
|
@Prop({ required: true }) label!: string |
||||
|
@Prop({ required: true }) value!: string |
||||
|
@Prop({ default: 'info' }) status!: 'success' | 'warning' | 'error' | 'info' |
||||
|
|
||||
|
get statusClass(): string { |
||||
|
return `status-${this.status}` |
||||
|
} |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<style scoped> |
||||
|
.status-item { |
||||
|
display: flex; |
||||
|
justify-content: space-between; |
||||
|
align-items: center; |
||||
|
padding: 8px 12px; |
||||
|
background: rgba(255, 255, 255, 0.05); |
||||
|
border-radius: 8px; |
||||
|
border: 1px solid rgba(255, 255, 255, 0.1); |
||||
|
} |
||||
|
|
||||
|
.status-label { |
||||
|
font-size: 0.9rem; |
||||
|
color: rgba(255, 255, 255, 0.8); |
||||
|
font-weight: 500; |
||||
|
} |
||||
|
|
||||
|
.status-value { |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
gap: 6px; |
||||
|
font-size: 0.85rem; |
||||
|
font-weight: 600; |
||||
|
} |
||||
|
|
||||
|
.status-dot { |
||||
|
width: 6px; |
||||
|
height: 6px; |
||||
|
border-radius: 50%; |
||||
|
} |
||||
|
|
||||
|
.status-success { |
||||
|
color: #4caf50; |
||||
|
} |
||||
|
|
||||
|
.status-success .status-dot { |
||||
|
background: #4caf50; |
||||
|
} |
||||
|
|
||||
|
.status-warning { |
||||
|
color: #ff9800; |
||||
|
} |
||||
|
|
||||
|
.status-warning .status-dot { |
||||
|
background: #ff9800; |
||||
|
} |
||||
|
|
||||
|
.status-error { |
||||
|
color: #f44336; |
||||
|
} |
||||
|
|
||||
|
.status-error .status-dot { |
||||
|
background: #f44336; |
||||
|
} |
||||
|
|
||||
|
.status-info { |
||||
|
color: #2196f3; |
||||
|
} |
||||
|
|
||||
|
.status-info .status-dot { |
||||
|
background: #2196f3; |
||||
|
} |
||||
|
</style> |
@ -0,0 +1,75 @@ |
|||||
|
<!-- |
||||
|
/** |
||||
|
* App Footer Component |
||||
|
* |
||||
|
* Simple footer with app information |
||||
|
* |
||||
|
* @author Matthew Raymer |
||||
|
* @version 1.0.0 |
||||
|
*/ |
||||
|
--> |
||||
|
|
||||
|
<template> |
||||
|
<footer class="app-footer"> |
||||
|
<div class="footer-content"> |
||||
|
<div class="footer-left"> |
||||
|
<p class="footer-text"> |
||||
|
Daily Notification Test App v1.0.0 |
||||
|
</p> |
||||
|
</div> |
||||
|
<div class="footer-right"> |
||||
|
<p class="footer-text"> |
||||
|
Built with Vue 3 + Vite + Capacitor |
||||
|
</p> |
||||
|
</div> |
||||
|
</div> |
||||
|
</footer> |
||||
|
</template> |
||||
|
|
||||
|
<script lang="ts"> |
||||
|
import { Component, Vue } from 'vue-facing-decorator' |
||||
|
|
||||
|
@Component |
||||
|
export default class AppFooter extends Vue {} |
||||
|
</script> |
||||
|
|
||||
|
<style scoped> |
||||
|
.app-footer { |
||||
|
background: rgba(0, 0, 0, 0.2); |
||||
|
backdrop-filter: blur(10px); |
||||
|
border-top: 1px solid rgba(255, 255, 255, 0.1); |
||||
|
padding: 16px 20px; |
||||
|
margin-top: auto; |
||||
|
} |
||||
|
|
||||
|
.footer-content { |
||||
|
display: flex; |
||||
|
justify-content: space-between; |
||||
|
align-items: center; |
||||
|
max-width: 1200px; |
||||
|
margin: 0 auto; |
||||
|
} |
||||
|
|
||||
|
.footer-text { |
||||
|
font-size: 0.85rem; |
||||
|
color: rgba(255, 255, 255, 0.7); |
||||
|
margin: 0; |
||||
|
} |
||||
|
|
||||
|
/* Responsive design */ |
||||
|
@media (max-width: 768px) { |
||||
|
.app-footer { |
||||
|
padding: 12px 16px; |
||||
|
} |
||||
|
|
||||
|
.footer-content { |
||||
|
flex-direction: column; |
||||
|
gap: 8px; |
||||
|
text-align: center; |
||||
|
} |
||||
|
|
||||
|
.footer-text { |
||||
|
font-size: 0.8rem; |
||||
|
} |
||||
|
} |
||||
|
</style> |
@ -0,0 +1,239 @@ |
|||||
|
<!-- |
||||
|
/** |
||||
|
* App Header Component |
||||
|
* |
||||
|
* Navigation header with menu and status indicators |
||||
|
* |
||||
|
* @author Matthew Raymer |
||||
|
* @version 1.0.0 |
||||
|
*/ |
||||
|
--> |
||||
|
|
||||
|
<template> |
||||
|
<header class="app-header"> |
||||
|
<div class="header-content"> |
||||
|
<!-- Logo and Title --> |
||||
|
<div class="header-left"> |
||||
|
<div class="logo"> |
||||
|
<span class="logo-icon">🔔</span> |
||||
|
<span class="logo-text">Daily Notification Test</span> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<!-- Navigation --> |
||||
|
<nav class="header-nav"> |
||||
|
<router-link |
||||
|
v-for="item in navigationItems" |
||||
|
:key="item.name" |
||||
|
:to="item.path" |
||||
|
class="nav-item" |
||||
|
:class="{ 'active': $route.name === item.name }" |
||||
|
> |
||||
|
<span class="nav-icon">{{ item.icon }}</span> |
||||
|
<span class="nav-text">{{ item.label }}</span> |
||||
|
</router-link> |
||||
|
</nav> |
||||
|
|
||||
|
<!-- Status Indicator --> |
||||
|
<div class="header-right"> |
||||
|
<div class="status-indicator" :class="statusClass"> |
||||
|
<span class="status-dot"></span> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</header> |
||||
|
</template> |
||||
|
|
||||
|
<script lang="ts"> |
||||
|
import { Component, Vue } from 'vue-facing-decorator' |
||||
|
import { useAppStore } from '@/stores/app' |
||||
|
|
||||
|
interface NavigationItem { |
||||
|
name: string |
||||
|
path: string |
||||
|
label: string |
||||
|
icon: string |
||||
|
} |
||||
|
|
||||
|
@Component |
||||
|
export default class AppHeader extends Vue { |
||||
|
private appStore = useAppStore() |
||||
|
|
||||
|
navigationItems: NavigationItem[] = [ |
||||
|
{ name: 'Home', path: '/', label: 'Home', icon: '🏠' }, |
||||
|
{ name: 'Schedule', path: '/schedule', label: 'Schedule', icon: '📅' }, |
||||
|
{ name: 'Notifications', path: '/notifications', label: 'Notifications', icon: '📱' }, |
||||
|
{ name: 'Status', path: '/status', label: 'Status', icon: '📊' }, |
||||
|
{ name: 'History', path: '/history', label: 'History', icon: '📋' }, |
||||
|
{ name: 'Logs', path: '/logs', label: 'Logs', icon: '📜' } |
||||
|
] |
||||
|
|
||||
|
get statusClass(): string { |
||||
|
const status = this.appStore.notificationStatus |
||||
|
if (!status) return 'unknown' |
||||
|
if (status.canScheduleNow) return 'ready' |
||||
|
return 'not-ready' |
||||
|
} |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<style scoped> |
||||
|
.app-header { |
||||
|
background: rgba(255, 255, 255, 0.1); |
||||
|
backdrop-filter: blur(10px); |
||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.2); |
||||
|
padding: 12px 20px; |
||||
|
position: sticky; |
||||
|
top: 0; |
||||
|
z-index: 100; |
||||
|
} |
||||
|
|
||||
|
.header-content { |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
justify-content: space-between; |
||||
|
max-width: 1200px; |
||||
|
margin: 0 auto; |
||||
|
} |
||||
|
|
||||
|
.header-left { |
||||
|
flex-shrink: 0; |
||||
|
} |
||||
|
|
||||
|
.logo { |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
gap: 8px; |
||||
|
} |
||||
|
|
||||
|
.logo-icon { |
||||
|
font-size: 1.5rem; |
||||
|
} |
||||
|
|
||||
|
.logo-text { |
||||
|
font-size: 1.1rem; |
||||
|
font-weight: 600; |
||||
|
color: white; |
||||
|
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3); |
||||
|
} |
||||
|
|
||||
|
.header-nav { |
||||
|
display: flex; |
||||
|
gap: 8px; |
||||
|
flex: 1; |
||||
|
justify-content: center; |
||||
|
} |
||||
|
|
||||
|
.nav-item { |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
gap: 6px; |
||||
|
padding: 8px 12px; |
||||
|
border-radius: 8px; |
||||
|
text-decoration: none; |
||||
|
color: rgba(255, 255, 255, 0.8); |
||||
|
font-size: 0.9rem; |
||||
|
font-weight: 500; |
||||
|
transition: all 0.3s ease; |
||||
|
} |
||||
|
|
||||
|
.nav-item:hover { |
||||
|
background: rgba(255, 255, 255, 0.1); |
||||
|
color: white; |
||||
|
} |
||||
|
|
||||
|
.nav-item.active { |
||||
|
background: rgba(255, 255, 255, 0.2); |
||||
|
color: white; |
||||
|
} |
||||
|
|
||||
|
.nav-icon { |
||||
|
font-size: 1rem; |
||||
|
} |
||||
|
|
||||
|
.nav-text { |
||||
|
display: none; |
||||
|
} |
||||
|
|
||||
|
.header-right { |
||||
|
flex-shrink: 0; |
||||
|
} |
||||
|
|
||||
|
.status-indicator { |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
gap: 6px; |
||||
|
padding: 6px 10px; |
||||
|
border-radius: 16px; |
||||
|
font-size: 0.8rem; |
||||
|
} |
||||
|
|
||||
|
.status-indicator.ready { |
||||
|
background: rgba(76, 175, 80, 0.2); |
||||
|
border: 1px solid rgba(76, 175, 80, 0.3); |
||||
|
} |
||||
|
|
||||
|
.status-indicator.not-ready { |
||||
|
background: rgba(244, 67, 54, 0.2); |
||||
|
border: 1px solid rgba(244, 67, 54, 0.3); |
||||
|
} |
||||
|
|
||||
|
.status-indicator.unknown { |
||||
|
background: rgba(158, 158, 158, 0.2); |
||||
|
border: 1px solid rgba(158, 158, 158, 0.3); |
||||
|
} |
||||
|
|
||||
|
.status-dot { |
||||
|
width: 8px; |
||||
|
height: 8px; |
||||
|
border-radius: 50%; |
||||
|
background: currentColor; |
||||
|
} |
||||
|
|
||||
|
.status-indicator.ready .status-dot { |
||||
|
background: #4caf50; |
||||
|
} |
||||
|
|
||||
|
.status-indicator.not-ready .status-dot { |
||||
|
background: #f44336; |
||||
|
} |
||||
|
|
||||
|
.status-indicator.unknown .status-dot { |
||||
|
background: #9e9e9e; |
||||
|
} |
||||
|
|
||||
|
/* Responsive design */ |
||||
|
@media (min-width: 768px) { |
||||
|
.nav-text { |
||||
|
display: inline; |
||||
|
} |
||||
|
|
||||
|
.header-nav { |
||||
|
gap: 16px; |
||||
|
} |
||||
|
|
||||
|
.nav-item { |
||||
|
padding: 10px 16px; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@media (max-width: 767px) { |
||||
|
.header-content { |
||||
|
padding: 0 16px; |
||||
|
} |
||||
|
|
||||
|
.logo-text { |
||||
|
display: none; |
||||
|
} |
||||
|
|
||||
|
.header-nav { |
||||
|
gap: 4px; |
||||
|
} |
||||
|
|
||||
|
.nav-item { |
||||
|
padding: 6px 8px; |
||||
|
min-width: 44px; |
||||
|
justify-content: center; |
||||
|
} |
||||
|
} |
||||
|
</style> |
@ -0,0 +1,161 @@ |
|||||
|
<!-- |
||||
|
/** |
||||
|
* Error Dialog Component |
||||
|
* |
||||
|
* Global error display dialog |
||||
|
* |
||||
|
* @author Matthew Raymer |
||||
|
* @version 1.0.0 |
||||
|
*/ |
||||
|
--> |
||||
|
|
||||
|
<template> |
||||
|
<div class="error-dialog-overlay" @click="handleOverlayClick"> |
||||
|
<div class="error-dialog" @click.stop> |
||||
|
<div class="error-header"> |
||||
|
<span class="error-icon">⚠️</span> |
||||
|
<h3 class="error-title">Error</h3> |
||||
|
</div> |
||||
|
|
||||
|
<div class="error-content"> |
||||
|
<p class="error-message">{{ message }}</p> |
||||
|
</div> |
||||
|
|
||||
|
<div class="error-actions"> |
||||
|
<button class="error-button primary" @click="handleClose"> |
||||
|
OK |
||||
|
</button> |
||||
|
<button v-if="showRetry" class="error-button secondary" @click="handleRetry"> |
||||
|
Retry |
||||
|
</button> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</template> |
||||
|
|
||||
|
<script lang="ts"> |
||||
|
import { Component, Vue, Prop } from 'vue-facing-decorator' |
||||
|
|
||||
|
@Component |
||||
|
export default class ErrorDialog extends Vue { |
||||
|
@Prop({ required: true }) message!: string |
||||
|
@Prop({ default: false }) showRetry!: boolean |
||||
|
|
||||
|
handleClose(): void { |
||||
|
this.$emit('close') |
||||
|
} |
||||
|
|
||||
|
handleRetry(): void { |
||||
|
this.$emit('retry') |
||||
|
} |
||||
|
|
||||
|
handleOverlayClick(): void { |
||||
|
this.handleClose() |
||||
|
} |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<style scoped> |
||||
|
.error-dialog-overlay { |
||||
|
position: fixed; |
||||
|
top: 0; |
||||
|
left: 0; |
||||
|
right: 0; |
||||
|
bottom: 0; |
||||
|
background: rgba(0, 0, 0, 0.7); |
||||
|
display: flex; |
||||
|
justify-content: center; |
||||
|
align-items: center; |
||||
|
z-index: 9999; |
||||
|
backdrop-filter: blur(4px); |
||||
|
} |
||||
|
|
||||
|
.error-dialog { |
||||
|
background: rgba(255, 255, 255, 0.95); |
||||
|
border-radius: 16px; |
||||
|
padding: 24px; |
||||
|
max-width: 400px; |
||||
|
width: 90%; |
||||
|
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.3); |
||||
|
border: 1px solid rgba(255, 255, 255, 0.2); |
||||
|
} |
||||
|
|
||||
|
.error-header { |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
gap: 12px; |
||||
|
margin-bottom: 16px; |
||||
|
} |
||||
|
|
||||
|
.error-icon { |
||||
|
font-size: 1.5rem; |
||||
|
} |
||||
|
|
||||
|
.error-title { |
||||
|
font-size: 1.3rem; |
||||
|
font-weight: 600; |
||||
|
color: #d32f2f; |
||||
|
margin: 0; |
||||
|
} |
||||
|
|
||||
|
.error-content { |
||||
|
margin-bottom: 24px; |
||||
|
} |
||||
|
|
||||
|
.error-message { |
||||
|
color: #424242; |
||||
|
font-size: 1rem; |
||||
|
line-height: 1.5; |
||||
|
margin: 0; |
||||
|
} |
||||
|
|
||||
|
.error-actions { |
||||
|
display: flex; |
||||
|
gap: 12px; |
||||
|
justify-content: flex-end; |
||||
|
} |
||||
|
|
||||
|
.error-button { |
||||
|
padding: 10px 20px; |
||||
|
border-radius: 8px; |
||||
|
font-size: 0.9rem; |
||||
|
font-weight: 500; |
||||
|
cursor: pointer; |
||||
|
transition: all 0.3s ease; |
||||
|
border: none; |
||||
|
} |
||||
|
|
||||
|
.error-button.primary { |
||||
|
background: #d32f2f; |
||||
|
color: white; |
||||
|
} |
||||
|
|
||||
|
.error-button.primary:hover { |
||||
|
background: #b71c1c; |
||||
|
} |
||||
|
|
||||
|
.error-button.secondary { |
||||
|
background: rgba(0, 0, 0, 0.1); |
||||
|
color: #424242; |
||||
|
} |
||||
|
|
||||
|
.error-button.secondary:hover { |
||||
|
background: rgba(0, 0, 0, 0.15); |
||||
|
} |
||||
|
|
||||
|
/* Responsive design */ |
||||
|
@media (max-width: 480px) { |
||||
|
.error-dialog { |
||||
|
padding: 20px; |
||||
|
margin: 16px; |
||||
|
} |
||||
|
|
||||
|
.error-actions { |
||||
|
flex-direction: column; |
||||
|
} |
||||
|
|
||||
|
.error-button { |
||||
|
width: 100%; |
||||
|
} |
||||
|
} |
||||
|
</style> |
@ -0,0 +1,78 @@ |
|||||
|
<!-- |
||||
|
/** |
||||
|
* Loading Overlay Component |
||||
|
* |
||||
|
* Global loading indicator overlay |
||||
|
* |
||||
|
* @author Matthew Raymer |
||||
|
* @version 1.0.0 |
||||
|
*/ |
||||
|
--> |
||||
|
|
||||
|
<template> |
||||
|
<div class="loading-overlay"> |
||||
|
<div class="loading-content"> |
||||
|
<div class="spinner"></div> |
||||
|
<p class="loading-text">{{ message }}</p> |
||||
|
</div> |
||||
|
</div> |
||||
|
</template> |
||||
|
|
||||
|
<script lang="ts"> |
||||
|
import { Component, Vue, Prop } from 'vue-facing-decorator' |
||||
|
|
||||
|
@Component |
||||
|
export default class LoadingOverlay extends Vue { |
||||
|
@Prop({ default: 'Loading...' }) message!: string |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<style scoped> |
||||
|
.loading-overlay { |
||||
|
position: fixed; |
||||
|
top: 0; |
||||
|
left: 0; |
||||
|
right: 0; |
||||
|
bottom: 0; |
||||
|
background: rgba(0, 0, 0, 0.7); |
||||
|
display: flex; |
||||
|
justify-content: center; |
||||
|
align-items: center; |
||||
|
z-index: 9999; |
||||
|
backdrop-filter: blur(4px); |
||||
|
} |
||||
|
|
||||
|
.loading-content { |
||||
|
display: flex; |
||||
|
flex-direction: column; |
||||
|
align-items: center; |
||||
|
gap: 16px; |
||||
|
padding: 32px; |
||||
|
background: rgba(255, 255, 255, 0.1); |
||||
|
border-radius: 16px; |
||||
|
backdrop-filter: blur(10px); |
||||
|
border: 1px solid rgba(255, 255, 255, 0.2); |
||||
|
} |
||||
|
|
||||
|
.spinner { |
||||
|
width: 40px; |
||||
|
height: 40px; |
||||
|
border: 4px solid rgba(255, 255, 255, 0.3); |
||||
|
border-top: 4px solid white; |
||||
|
border-radius: 50%; |
||||
|
animation: spin 1s linear infinite; |
||||
|
} |
||||
|
|
||||
|
.loading-text { |
||||
|
color: white; |
||||
|
font-size: 1.1rem; |
||||
|
font-weight: 500; |
||||
|
margin: 0; |
||||
|
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3); |
||||
|
} |
||||
|
|
||||
|
@keyframes spin { |
||||
|
0% { transform: rotate(0deg); } |
||||
|
100% { transform: rotate(360deg); } |
||||
|
} |
||||
|
</style> |
@ -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> |
|
File diff suppressed because it is too large
@ -0,0 +1,42 @@ |
|||||
|
/** |
||||
|
* Main Application Entry Point |
||||
|
* |
||||
|
* Vue 3 + TypeScript + Capacitor + vue-facing-decorator setup |
||||
|
* |
||||
|
* @author Matthew Raymer |
||||
|
* @version 1.0.0 |
||||
|
*/ |
||||
|
|
||||
|
import { createApp } from 'vue' |
||||
|
import { createPinia } from 'pinia' |
||||
|
import { Capacitor } from '@capacitor/core' |
||||
|
import App from './App.vue' |
||||
|
import router from './router' |
||||
|
|
||||
|
// Create Vue app instance
|
||||
|
const app = createApp(App) |
||||
|
|
||||
|
// Configure Pinia for state management
|
||||
|
const pinia = createPinia() |
||||
|
app.use(pinia) |
||||
|
|
||||
|
// Configure Vue Router
|
||||
|
app.use(router) |
||||
|
|
||||
|
// Global error handler for Capacitor
|
||||
|
app.config.errorHandler = (err, _instance, info) => { |
||||
|
console.error('Vue Error:', err, info) |
||||
|
if (Capacitor.isNativePlatform()) { |
||||
|
// Log to native platform
|
||||
|
console.error('Native Platform Error:', err) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// Mount the app
|
||||
|
app.mount('#app') |
||||
|
|
||||
|
// Log platform information
|
||||
|
console.log('🚀 Daily Notification Test App Started') |
||||
|
console.log('📱 Platform:', Capacitor.getPlatform()) |
||||
|
console.log('🔧 Native Platform:', Capacitor.isNativePlatform()) |
||||
|
console.log('🌐 Web Platform:', Capacitor.isPluginAvailable('App')) |
@ -0,0 +1,109 @@ |
|||||
|
/** |
||||
|
* Vue Router Configuration |
||||
|
* |
||||
|
* @author Matthew Raymer |
||||
|
* @version 1.0.0 |
||||
|
*/ |
||||
|
|
||||
|
import { createRouter, createWebHistory } from 'vue-router' |
||||
|
import type { RouteRecordRaw } from 'vue-router' |
||||
|
|
||||
|
const routes: RouteRecordRaw[] = [ |
||||
|
{ |
||||
|
path: '/', |
||||
|
name: 'Home', |
||||
|
component: () => import('@/views/HomeView.vue'), |
||||
|
meta: { |
||||
|
title: 'Daily Notification Test', |
||||
|
requiresAuth: false |
||||
|
} |
||||
|
}, |
||||
|
{ |
||||
|
path: '/notifications', |
||||
|
name: 'Notifications', |
||||
|
component: () => import('@/views/NotificationsView.vue'), |
||||
|
meta: { |
||||
|
title: 'Notification Management', |
||||
|
requiresAuth: false |
||||
|
} |
||||
|
}, |
||||
|
{ |
||||
|
path: '/schedule', |
||||
|
name: 'Schedule', |
||||
|
component: () => import('@/views/ScheduleView.vue'), |
||||
|
meta: { |
||||
|
title: 'Schedule Notification', |
||||
|
requiresAuth: false |
||||
|
} |
||||
|
}, |
||||
|
{ |
||||
|
path: '/status', |
||||
|
name: 'Status', |
||||
|
component: () => import('@/views/StatusView.vue'), |
||||
|
meta: { |
||||
|
title: 'System Status', |
||||
|
requiresAuth: false |
||||
|
} |
||||
|
}, |
||||
|
{ |
||||
|
path: '/history', |
||||
|
name: 'History', |
||||
|
component: () => import('@/views/HistoryView.vue'), |
||||
|
meta: { |
||||
|
title: 'Notification History', |
||||
|
requiresAuth: false |
||||
|
} |
||||
|
}, |
||||
|
{ |
||||
|
path: '/logs', |
||||
|
name: 'Logs', |
||||
|
component: () => import('@/views/LogsView.vue'), |
||||
|
meta: { |
||||
|
title: 'Android Logs', |
||||
|
requiresAuth: false |
||||
|
} |
||||
|
}, |
||||
|
{ |
||||
|
path: '/settings', |
||||
|
name: 'Settings', |
||||
|
component: () => import('@/views/SettingsView.vue'), |
||||
|
meta: { |
||||
|
title: 'Settings', |
||||
|
requiresAuth: false |
||||
|
} |
||||
|
}, |
||||
|
{ |
||||
|
path: '/:pathMatch(.*)*', |
||||
|
name: 'NotFound', |
||||
|
component: () => import('@/views/NotFoundView.vue'), |
||||
|
meta: { |
||||
|
title: 'Page Not Found', |
||||
|
requiresAuth: false |
||||
|
} |
||||
|
} |
||||
|
] |
||||
|
|
||||
|
const router = createRouter({ |
||||
|
history: createWebHistory(), |
||||
|
routes |
||||
|
}) |
||||
|
|
||||
|
// Global navigation guards
|
||||
|
router.beforeEach((to, from, next) => { |
||||
|
// Set page title
|
||||
|
if (to.meta?.title) { |
||||
|
document.title = `${to.meta.title} - Daily Notification Test` |
||||
|
} |
||||
|
|
||||
|
// Add loading state
|
||||
|
console.log(`🔄 Navigating from ${String(from.name) || 'unknown'} to ${String(to.name) || 'unknown'}`) |
||||
|
|
||||
|
next() |
||||
|
}) |
||||
|
|
||||
|
router.afterEach((to) => { |
||||
|
// Clear any previous errors on successful navigation
|
||||
|
console.log(`✅ Navigation completed: ${String(to.name) || 'unknown'}`) |
||||
|
}) |
||||
|
|
||||
|
export default router |
@ -0,0 +1,108 @@ |
|||||
|
/** |
||||
|
* App Store - Global Application State |
||||
|
* |
||||
|
* Pinia store for managing global app state, loading, and errors |
||||
|
* |
||||
|
* @author Matthew Raymer |
||||
|
* @version 1.0.0 |
||||
|
*/ |
||||
|
|
||||
|
import { defineStore } from 'pinia' |
||||
|
import { ref, computed } from 'vue' |
||||
|
|
||||
|
export interface AppState { |
||||
|
isLoading: boolean |
||||
|
errorMessage: string | null |
||||
|
platform: string |
||||
|
isNative: boolean |
||||
|
notificationStatus: NotificationStatus | null |
||||
|
} |
||||
|
|
||||
|
export interface NotificationStatus { |
||||
|
canScheduleNow: boolean |
||||
|
postNotificationsGranted: boolean |
||||
|
channelEnabled: boolean |
||||
|
channelImportance: number |
||||
|
channelId: string |
||||
|
exactAlarmsGranted: boolean |
||||
|
exactAlarmsSupported: boolean |
||||
|
androidVersion: number |
||||
|
nextScheduledAt: number |
||||
|
} |
||||
|
|
||||
|
export const useAppStore = defineStore('app', () => { |
||||
|
// State
|
||||
|
const isLoading = ref(false) |
||||
|
const errorMessage = ref<string | null>(null) |
||||
|
const platform = ref('web') |
||||
|
const isNative = ref(false) |
||||
|
const notificationStatus = ref<NotificationStatus | null>(null) |
||||
|
|
||||
|
// Getters
|
||||
|
const hasError = computed(() => errorMessage.value !== null) |
||||
|
const isNotificationReady = computed(() => |
||||
|
notificationStatus.value?.canScheduleNow ?? false |
||||
|
) |
||||
|
|
||||
|
// Actions
|
||||
|
function setLoading(loading: boolean): void { |
||||
|
isLoading.value = loading |
||||
|
} |
||||
|
|
||||
|
function setError(message: string): void { |
||||
|
errorMessage.value = message |
||||
|
console.error('App Error:', message) |
||||
|
} |
||||
|
|
||||
|
function clearError(): void { |
||||
|
errorMessage.value = null |
||||
|
} |
||||
|
|
||||
|
function setPlatform(platformName: string, native: boolean): void { |
||||
|
platform.value = platformName |
||||
|
isNative.value = native |
||||
|
} |
||||
|
|
||||
|
function setNotificationStatus(status: NotificationStatus): void { |
||||
|
notificationStatus.value = status |
||||
|
} |
||||
|
|
||||
|
function updateNotificationStatus(status: Partial<NotificationStatus>): void { |
||||
|
if (notificationStatus.value) { |
||||
|
notificationStatus.value = { ...notificationStatus.value, ...status } |
||||
|
} else { |
||||
|
notificationStatus.value = status as NotificationStatus |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// Reset store
|
||||
|
function $reset(): void { |
||||
|
isLoading.value = false |
||||
|
errorMessage.value = null |
||||
|
platform.value = 'web' |
||||
|
isNative.value = false |
||||
|
notificationStatus.value = null |
||||
|
} |
||||
|
|
||||
|
return { |
||||
|
// State
|
||||
|
isLoading, |
||||
|
errorMessage, |
||||
|
platform, |
||||
|
isNative, |
||||
|
notificationStatus, |
||||
|
|
||||
|
// Getters
|
||||
|
hasError, |
||||
|
isNotificationReady, |
||||
|
|
||||
|
// Actions
|
||||
|
setLoading, |
||||
|
setError, |
||||
|
clearError, |
||||
|
setPlatform, |
||||
|
setNotificationStatus, |
||||
|
updateNotificationStatus, |
||||
|
$reset |
||||
|
} |
||||
|
}) |
@ -0,0 +1,299 @@ |
|||||
|
/** |
||||
|
* Notifications Store - Notification Management State |
||||
|
* |
||||
|
* Pinia store for managing notification scheduling, status, and history |
||||
|
* |
||||
|
* @author Matthew Raymer |
||||
|
* @version 1.0.0 |
||||
|
*/ |
||||
|
|
||||
|
import { defineStore } from 'pinia' |
||||
|
import { ref, computed } from 'vue' |
||||
|
import { Capacitor } from '@capacitor/core' |
||||
|
|
||||
|
export interface ScheduledNotification { |
||||
|
id: string |
||||
|
title: string |
||||
|
body: string |
||||
|
scheduledTime: number |
||||
|
deliveredAt?: number |
||||
|
status: 'scheduled' | 'delivered' | 'cancelled' |
||||
|
} |
||||
|
|
||||
|
export interface NotificationHistory { |
||||
|
id: string |
||||
|
title: string |
||||
|
body: string |
||||
|
scheduledTime: number |
||||
|
deliveredAt: number |
||||
|
clicked: boolean |
||||
|
dismissed: boolean |
||||
|
} |
||||
|
|
||||
|
export const useNotificationsStore = defineStore('notifications', () => { |
||||
|
// State
|
||||
|
const scheduledNotifications = ref<ScheduledNotification[]>([]) |
||||
|
const notificationHistory = ref<NotificationHistory[]>([]) |
||||
|
const isScheduling = ref(false) |
||||
|
const lastError = ref<string | null>(null) |
||||
|
|
||||
|
// Getters
|
||||
|
const hasScheduledNotifications = computed(() => |
||||
|
scheduledNotifications.value.length > 0 |
||||
|
) |
||||
|
|
||||
|
const nextNotification = computed(() => { |
||||
|
const future = scheduledNotifications.value |
||||
|
.filter(n => n.status === 'scheduled' && n.scheduledTime > Date.now()) |
||||
|
.sort((a, b) => a.scheduledTime - b.scheduledTime) |
||||
|
return future.length > 0 ? future[0] : null |
||||
|
}) |
||||
|
|
||||
|
const notificationCount = computed(() => |
||||
|
scheduledNotifications.value.length |
||||
|
) |
||||
|
|
||||
|
// Actions
|
||||
|
async function scheduleNotification(options: { |
||||
|
time: string |
||||
|
title?: string |
||||
|
body?: string |
||||
|
sound?: boolean |
||||
|
priority?: string |
||||
|
url?: string |
||||
|
}): Promise<void> { |
||||
|
if (!Capacitor.isNativePlatform()) { |
||||
|
throw new Error('DailyNotification plugin only available on native platforms') |
||||
|
} |
||||
|
|
||||
|
if (!window.DailyNotification) { |
||||
|
throw new Error('DailyNotification plugin not loaded. Please restart the app.') |
||||
|
} |
||||
|
|
||||
|
try { |
||||
|
isScheduling.value = true |
||||
|
lastError.value = null |
||||
|
|
||||
|
await window.DailyNotification.scheduleDailyNotification(options) |
||||
|
|
||||
|
// Add to local state (we'll get the actual ID from the plugin)
|
||||
|
const notification: ScheduledNotification = { |
||||
|
id: `temp-${Date.now()}`, |
||||
|
title: options.title || 'Daily Update', |
||||
|
body: options.body || 'Your daily notification is ready', |
||||
|
scheduledTime: parseTimeToTimestamp(options.time), |
||||
|
status: 'scheduled' |
||||
|
} |
||||
|
|
||||
|
scheduledNotifications.value.push(notification) |
||||
|
|
||||
|
console.log('✅ Notification scheduled successfully') |
||||
|
|
||||
|
} catch (error) { |
||||
|
const errorMessage = (error as Error).message |
||||
|
lastError.value = errorMessage |
||||
|
console.error('❌ Failed to schedule notification:', errorMessage) |
||||
|
throw error |
||||
|
} finally { |
||||
|
isScheduling.value = false |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
async function scheduleReminder(options: { |
||||
|
id: string |
||||
|
title: string |
||||
|
body: string |
||||
|
time: string |
||||
|
sound?: boolean |
||||
|
vibration?: boolean |
||||
|
priority?: string |
||||
|
repeatDaily?: boolean |
||||
|
timezone?: string |
||||
|
}): Promise<void> { |
||||
|
if (!Capacitor.isNativePlatform()) { |
||||
|
throw new Error('DailyNotification plugin only available on native platforms') |
||||
|
} |
||||
|
|
||||
|
if (!window.DailyNotification) { |
||||
|
throw new Error('DailyNotification plugin not loaded. Please restart the app.') |
||||
|
} |
||||
|
|
||||
|
try { |
||||
|
isScheduling.value = true |
||||
|
lastError.value = null |
||||
|
|
||||
|
await window.DailyNotification.scheduleDailyReminder(options) |
||||
|
|
||||
|
// Add to local state
|
||||
|
const notification: ScheduledNotification = { |
||||
|
id: options.id, |
||||
|
title: options.title, |
||||
|
body: options.body, |
||||
|
scheduledTime: parseTimeToTimestamp(options.time), |
||||
|
status: 'scheduled' |
||||
|
} |
||||
|
|
||||
|
scheduledNotifications.value.push(notification) |
||||
|
|
||||
|
console.log('✅ Reminder scheduled successfully') |
||||
|
|
||||
|
} catch (error) { |
||||
|
const errorMessage = (error as Error).message |
||||
|
lastError.value = errorMessage |
||||
|
console.error('❌ Failed to schedule reminder:', errorMessage) |
||||
|
throw error |
||||
|
} finally { |
||||
|
isScheduling.value = false |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
async function cancelReminder(reminderId: string): Promise<void> { |
||||
|
if (!Capacitor.isNativePlatform()) { |
||||
|
throw new Error('DailyNotification plugin only available on native platforms') |
||||
|
} |
||||
|
|
||||
|
if (!window.DailyNotification) { |
||||
|
throw new Error('DailyNotification plugin not loaded. Please restart the app.') |
||||
|
} |
||||
|
|
||||
|
try { |
||||
|
isScheduling.value = true |
||||
|
lastError.value = null |
||||
|
|
||||
|
await window.DailyNotification.cancelDailyReminder({ reminderId }) |
||||
|
|
||||
|
// Update local state
|
||||
|
const index = scheduledNotifications.value.findIndex(n => n.id === reminderId) |
||||
|
if (index !== -1) { |
||||
|
scheduledNotifications.value[index].status = 'cancelled' |
||||
|
} |
||||
|
|
||||
|
console.log('✅ Reminder cancelled successfully') |
||||
|
|
||||
|
} catch (error) { |
||||
|
const errorMessage = (error as Error).message |
||||
|
lastError.value = errorMessage |
||||
|
console.error('❌ Failed to cancel reminder:', errorMessage) |
||||
|
throw error |
||||
|
} finally { |
||||
|
isScheduling.value = false |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
async function checkStatus(): Promise<void> { |
||||
|
if (!Capacitor.isNativePlatform()) { |
||||
|
console.warn('DailyNotification plugin only available on native platforms') |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
if (!window.DailyNotification) { |
||||
|
console.warn('DailyNotification plugin not loaded') |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
try { |
||||
|
const status = await window.DailyNotification.checkStatus() |
||||
|
console.log('📊 Notification Status:', status) |
||||
|
|
||||
|
// Update app store with status
|
||||
|
const { useAppStore } = await import('@/stores/app') |
||||
|
const appStore = useAppStore() |
||||
|
appStore.setNotificationStatus(status) |
||||
|
|
||||
|
} catch (error) { |
||||
|
console.error('❌ Failed to check notification status:', error) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
async function getLastNotification(): Promise<void> { |
||||
|
if (!Capacitor.isNativePlatform()) { |
||||
|
console.warn('DailyNotification plugin only available on native platforms') |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
if (!window.DailyNotification) { |
||||
|
console.warn('DailyNotification plugin not loaded') |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
try { |
||||
|
const lastNotification = await window.DailyNotification.getLastNotification() |
||||
|
console.log('📱 Last Notification:', lastNotification) |
||||
|
|
||||
|
// Add to history
|
||||
|
const historyItem: NotificationHistory = { |
||||
|
id: lastNotification.id, |
||||
|
title: lastNotification.title, |
||||
|
body: lastNotification.body, |
||||
|
scheduledTime: lastNotification.scheduledTime, |
||||
|
deliveredAt: lastNotification.deliveredAt, |
||||
|
clicked: false, |
||||
|
dismissed: false |
||||
|
} |
||||
|
|
||||
|
notificationHistory.value.unshift(historyItem) |
||||
|
|
||||
|
// Keep only last 50 items
|
||||
|
if (notificationHistory.value.length > 50) { |
||||
|
notificationHistory.value = notificationHistory.value.slice(0, 50) |
||||
|
} |
||||
|
|
||||
|
} catch (error) { |
||||
|
console.error('❌ Failed to get last notification:', error) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
function clearError(): void { |
||||
|
lastError.value = null |
||||
|
} |
||||
|
|
||||
|
function clearHistory(): void { |
||||
|
notificationHistory.value = [] |
||||
|
} |
||||
|
|
||||
|
// Helper function to parse time string to timestamp
|
||||
|
function parseTimeToTimestamp(timeString: string): number { |
||||
|
const [hours, minutes] = timeString.split(':').map(Number) |
||||
|
const now = new Date() |
||||
|
const scheduled = new Date() |
||||
|
scheduled.setHours(hours, minutes, 0, 0) |
||||
|
|
||||
|
// If time has passed today, schedule for tomorrow
|
||||
|
if (scheduled <= now) { |
||||
|
scheduled.setDate(scheduled.getDate() + 1) |
||||
|
} |
||||
|
|
||||
|
return scheduled.getTime() |
||||
|
} |
||||
|
|
||||
|
// Reset store
|
||||
|
function $reset(): void { |
||||
|
scheduledNotifications.value = [] |
||||
|
notificationHistory.value = [] |
||||
|
isScheduling.value = false |
||||
|
lastError.value = null |
||||
|
} |
||||
|
|
||||
|
return { |
||||
|
// State
|
||||
|
scheduledNotifications, |
||||
|
notificationHistory, |
||||
|
isScheduling, |
||||
|
lastError, |
||||
|
|
||||
|
// Getters
|
||||
|
hasScheduledNotifications, |
||||
|
nextNotification, |
||||
|
notificationCount, |
||||
|
|
||||
|
// Actions
|
||||
|
scheduleNotification, |
||||
|
scheduleReminder, |
||||
|
cancelReminder, |
||||
|
checkStatus, |
||||
|
getLastNotification, |
||||
|
clearError, |
||||
|
clearHistory, |
||||
|
$reset |
||||
|
} |
||||
|
}) |
@ -0,0 +1,144 @@ |
|||||
|
<!-- |
||||
|
/** |
||||
|
* History View - Notification History |
||||
|
* |
||||
|
* @author Matthew Raymer |
||||
|
* @version 1.0.0 |
||||
|
*/ |
||||
|
--> |
||||
|
|
||||
|
<template> |
||||
|
<div class="history-view"> |
||||
|
<div class="view-header"> |
||||
|
<h1 class="page-title">📋 Notification History</h1> |
||||
|
<p class="page-subtitle">View delivered notifications</p> |
||||
|
</div> |
||||
|
|
||||
|
<div v-if="hasHistory" class="history-list"> |
||||
|
<HistoryItem |
||||
|
v-for="item in notificationHistory" |
||||
|
:key="item.id" |
||||
|
:history="item" |
||||
|
/> |
||||
|
</div> |
||||
|
|
||||
|
<div v-else class="no-history"> |
||||
|
<div class="empty-state"> |
||||
|
<span class="empty-icon">📋</span> |
||||
|
<h3 class="empty-title">No History Available</h3> |
||||
|
<p class="empty-description"> |
||||
|
Notification history will appear here after notifications are delivered |
||||
|
</p> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</template> |
||||
|
|
||||
|
<script lang="ts"> |
||||
|
import { Component, Vue } from 'vue-facing-decorator' |
||||
|
import HistoryItem from '@/components/items/HistoryItem.vue' |
||||
|
import { useNotificationsStore } from '@/stores/notifications' |
||||
|
|
||||
|
@Component({ |
||||
|
components: { |
||||
|
HistoryItem |
||||
|
} |
||||
|
}) |
||||
|
export default class HistoryView extends Vue { |
||||
|
private notificationsStore = useNotificationsStore() |
||||
|
|
||||
|
get hasHistory(): boolean { |
||||
|
return this.notificationHistory.length > 0 |
||||
|
} |
||||
|
|
||||
|
get notificationHistory() { |
||||
|
return this.notificationsStore.notificationHistory |
||||
|
} |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<style scoped> |
||||
|
.history-view { |
||||
|
max-width: 800px; |
||||
|
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; |
||||
|
} |
||||
|
|
||||
|
.history-list { |
||||
|
display: flex; |
||||
|
flex-direction: column; |
||||
|
gap: 12px; |
||||
|
} |
||||
|
|
||||
|
.no-history { |
||||
|
display: flex; |
||||
|
justify-content: center; |
||||
|
align-items: center; |
||||
|
min-height: 400px; |
||||
|
} |
||||
|
|
||||
|
.empty-state { |
||||
|
text-align: center; |
||||
|
padding: 40px 20px; |
||||
|
background: rgba(255, 255, 255, 0.1); |
||||
|
border-radius: 16px; |
||||
|
backdrop-filter: blur(10px); |
||||
|
border: 1px solid rgba(255, 255, 255, 0.2); |
||||
|
} |
||||
|
|
||||
|
.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; |
||||
|
} |
||||
|
|
||||
|
/* Responsive design */ |
||||
|
@media (max-width: 768px) { |
||||
|
.history-view { |
||||
|
padding: 16px; |
||||
|
} |
||||
|
|
||||
|
.empty-state { |
||||
|
padding: 32px 16px; |
||||
|
} |
||||
|
|
||||
|
.empty-icon { |
||||
|
font-size: 3rem; |
||||
|
} |
||||
|
} |
||||
|
</style> |
@ -0,0 +1,260 @@ |
|||||
|
<!-- |
||||
|
/** |
||||
|
* Home View - Main Dashboard |
||||
|
* |
||||
|
* Vue 3 + vue-facing-decorator + TypeScript |
||||
|
* |
||||
|
* @author Matthew Raymer |
||||
|
* @version 1.0.0 |
||||
|
*/ |
||||
|
--> |
||||
|
|
||||
|
<template> |
||||
|
<div class="home-view"> |
||||
|
<!-- Welcome Section --> |
||||
|
<div class="welcome-section"> |
||||
|
<h1 class="welcome-title"> |
||||
|
🔔 Daily Notification Test |
||||
|
</h1> |
||||
|
<p class="welcome-subtitle"> |
||||
|
Vue 3 + Vite + Capacitor + vue-facing-decorator |
||||
|
</p> |
||||
|
</div> |
||||
|
|
||||
|
<!-- Quick Actions --> |
||||
|
<div class="quick-actions"> |
||||
|
<h2 class="section-title">Quick Actions</h2> |
||||
|
<div class="action-grid"> |
||||
|
<ActionCard |
||||
|
icon="📅" |
||||
|
title="Schedule Notification" |
||||
|
description="Schedule a new daily notification" |
||||
|
@click="navigateToSchedule" |
||||
|
:loading="isScheduling" |
||||
|
/> |
||||
|
<ActionCard |
||||
|
icon="📊" |
||||
|
title="Check Status" |
||||
|
description="View notification system status" |
||||
|
@click="checkSystemStatus" |
||||
|
:loading="isCheckingStatus" |
||||
|
/> |
||||
|
<ActionCard |
||||
|
icon="📱" |
||||
|
title="View Notifications" |
||||
|
description="Manage scheduled notifications" |
||||
|
@click="navigateToNotifications" |
||||
|
/> |
||||
|
<ActionCard |
||||
|
icon="📋" |
||||
|
title="View History" |
||||
|
description="See notification delivery history" |
||||
|
@click="navigateToHistory" |
||||
|
/> |
||||
|
<ActionCard |
||||
|
icon="📜" |
||||
|
title="View Logs" |
||||
|
description="View and copy Android logs" |
||||
|
@click="navigateToLogs" |
||||
|
/> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<!-- System Status --> |
||||
|
<div class="status-section"> |
||||
|
<h2 class="section-title">System Status</h2> |
||||
|
<StatusCard :status="notificationStatus" /> |
||||
|
</div> |
||||
|
|
||||
|
<!-- Next Notification --> |
||||
|
<div v-if="nextNotification" class="next-notification"> |
||||
|
<h2 class="section-title">Next Notification</h2> |
||||
|
<NotificationCard :notification="nextNotification" /> |
||||
|
</div> |
||||
|
|
||||
|
<!-- Recent Activity --> |
||||
|
<div v-if="recentHistory.length > 0" class="recent-activity"> |
||||
|
<h2 class="section-title">Recent Activity</h2> |
||||
|
<div class="activity-list"> |
||||
|
<ActivityItem |
||||
|
v-for="item in recentHistory" |
||||
|
:key="item.id" |
||||
|
:activity="item" |
||||
|
/> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</template> |
||||
|
|
||||
|
<script lang="ts"> |
||||
|
import { Component, Vue } from 'vue-facing-decorator' |
||||
|
import { Capacitor } from '@capacitor/core' |
||||
|
import ActionCard from '@/components/cards/ActionCard.vue' |
||||
|
import StatusCard from '@/components/cards/StatusCard.vue' |
||||
|
import NotificationCard from '@/components/cards/NotificationCard.vue' |
||||
|
import ActivityItem from '@/components/items/ActivityItem.vue' |
||||
|
import { useAppStore } from '@/stores/app' |
||||
|
import { useNotificationsStore } from '@/stores/notifications' |
||||
|
|
||||
|
@Component({ |
||||
|
components: { |
||||
|
ActionCard, |
||||
|
StatusCard, |
||||
|
NotificationCard, |
||||
|
ActivityItem |
||||
|
} |
||||
|
}) |
||||
|
export default class HomeView extends Vue { |
||||
|
private appStore = useAppStore() |
||||
|
private notificationsStore = useNotificationsStore() |
||||
|
isCheckingStatus = false |
||||
|
|
||||
|
get isScheduling(): boolean { |
||||
|
return this.notificationsStore.isScheduling |
||||
|
} |
||||
|
|
||||
|
get notificationStatus() { |
||||
|
return this.appStore.notificationStatus |
||||
|
} |
||||
|
|
||||
|
get nextNotification() { |
||||
|
return this.notificationsStore.nextNotification |
||||
|
} |
||||
|
|
||||
|
get recentHistory() { |
||||
|
return this.notificationsStore.notificationHistory.slice(0, 5) |
||||
|
} |
||||
|
|
||||
|
async mounted(): Promise<void> { |
||||
|
await this.initializeHome() |
||||
|
} |
||||
|
|
||||
|
private async initializeHome(): Promise<void> { |
||||
|
try { |
||||
|
// Check system status |
||||
|
await this.checkSystemStatus() |
||||
|
|
||||
|
// Load recent notifications |
||||
|
await this.notificationsStore.getLastNotification() |
||||
|
|
||||
|
} catch (error) { |
||||
|
console.error('❌ Failed to initialize home:', error) |
||||
|
this.appStore.setError('Failed to load home data: ' + (error as Error).message) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
async checkSystemStatus(): Promise<void> { |
||||
|
if (!Capacitor.isNativePlatform()) { |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
try { |
||||
|
this.isCheckingStatus = true |
||||
|
await this.notificationsStore.checkStatus() |
||||
|
} catch (error) { |
||||
|
console.error('❌ Failed to check system status:', error) |
||||
|
} finally { |
||||
|
this.isCheckingStatus = false |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
navigateToSchedule(): void { |
||||
|
this.$router.push('/schedule') |
||||
|
} |
||||
|
|
||||
|
navigateToNotifications(): void { |
||||
|
this.$router.push('/notifications') |
||||
|
} |
||||
|
|
||||
|
navigateToHistory(): void { |
||||
|
this.$router.push('/history') |
||||
|
} |
||||
|
|
||||
|
navigateToLogs(): void { |
||||
|
this.$router.push('/logs') |
||||
|
} |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<style scoped> |
||||
|
.home-view { |
||||
|
max-width: 1200px; |
||||
|
margin: 0 auto; |
||||
|
padding: 20px; |
||||
|
} |
||||
|
|
||||
|
.welcome-section { |
||||
|
text-align: center; |
||||
|
margin-bottom: 32px; |
||||
|
padding: 24px; |
||||
|
background: rgba(255, 255, 255, 0.1); |
||||
|
border-radius: 16px; |
||||
|
backdrop-filter: blur(10px); |
||||
|
} |
||||
|
|
||||
|
.welcome-title { |
||||
|
font-size: 2.5rem; |
||||
|
font-weight: 700; |
||||
|
color: white; |
||||
|
margin: 0 0 8px 0; |
||||
|
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3); |
||||
|
} |
||||
|
|
||||
|
.welcome-subtitle { |
||||
|
font-size: 1.1rem; |
||||
|
color: rgba(255, 255, 255, 0.9); |
||||
|
margin: 0; |
||||
|
font-weight: 300; |
||||
|
} |
||||
|
|
||||
|
.section-title { |
||||
|
font-size: 1.5rem; |
||||
|
font-weight: 600; |
||||
|
color: white; |
||||
|
margin: 0 0 16px 0; |
||||
|
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3); |
||||
|
} |
||||
|
|
||||
|
.quick-actions { |
||||
|
margin-bottom: 32px; |
||||
|
} |
||||
|
|
||||
|
.action-grid { |
||||
|
display: grid; |
||||
|
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); |
||||
|
gap: 16px; |
||||
|
} |
||||
|
|
||||
|
.status-section { |
||||
|
margin-bottom: 32px; |
||||
|
} |
||||
|
|
||||
|
.next-notification { |
||||
|
margin-bottom: 32px; |
||||
|
} |
||||
|
|
||||
|
.recent-activity { |
||||
|
margin-bottom: 32px; |
||||
|
} |
||||
|
|
||||
|
.activity-list { |
||||
|
display: flex; |
||||
|
flex-direction: column; |
||||
|
gap: 12px; |
||||
|
} |
||||
|
|
||||
|
/* Responsive design */ |
||||
|
@media (max-width: 768px) { |
||||
|
.home-view { |
||||
|
padding: 16px; |
||||
|
} |
||||
|
|
||||
|
.welcome-title { |
||||
|
font-size: 2rem; |
||||
|
} |
||||
|
|
||||
|
.action-grid { |
||||
|
grid-template-columns: 1fr; |
||||
|
} |
||||
|
} |
||||
|
</style> |
@ -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> |
@ -0,0 +1,117 @@ |
|||||
|
<!-- |
||||
|
/** |
||||
|
* Not Found View - 404 Page |
||||
|
* |
||||
|
* @author Matthew Raymer |
||||
|
* @version 1.0.0 |
||||
|
*/ |
||||
|
--> |
||||
|
|
||||
|
<template> |
||||
|
<div class="not-found-view"> |
||||
|
<div class="not-found-content"> |
||||
|
<span class="not-found-icon">🔍</span> |
||||
|
<h1 class="not-found-title">404</h1> |
||||
|
<h2 class="not-found-subtitle">Page Not Found</h2> |
||||
|
<p class="not-found-description"> |
||||
|
The page you're looking for doesn't exist. |
||||
|
</p> |
||||
|
<button class="home-button" @click="goHome"> |
||||
|
🏠 Go Home |
||||
|
</button> |
||||
|
</div> |
||||
|
</div> |
||||
|
</template> |
||||
|
|
||||
|
<script lang="ts"> |
||||
|
import { Component, Vue } from 'vue-facing-decorator' |
||||
|
|
||||
|
@Component |
||||
|
export default class NotFoundView extends Vue { |
||||
|
goHome(): void { |
||||
|
this.$router.push('/') |
||||
|
} |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<style scoped> |
||||
|
.not-found-view { |
||||
|
display: flex; |
||||
|
justify-content: center; |
||||
|
align-items: center; |
||||
|
min-height: 60vh; |
||||
|
padding: 20px; |
||||
|
} |
||||
|
|
||||
|
.not-found-content { |
||||
|
text-align: center; |
||||
|
padding: 40px; |
||||
|
background: rgba(255, 255, 255, 0.1); |
||||
|
border-radius: 16px; |
||||
|
backdrop-filter: blur(10px); |
||||
|
border: 1px solid rgba(255, 255, 255, 0.2); |
||||
|
} |
||||
|
|
||||
|
.not-found-icon { |
||||
|
font-size: 4rem; |
||||
|
display: block; |
||||
|
margin-bottom: 16px; |
||||
|
} |
||||
|
|
||||
|
.not-found-title { |
||||
|
font-size: 4rem; |
||||
|
font-weight: 700; |
||||
|
color: white; |
||||
|
margin: 0 0 8px 0; |
||||
|
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3); |
||||
|
} |
||||
|
|
||||
|
.not-found-subtitle { |
||||
|
font-size: 1.5rem; |
||||
|
font-weight: 600; |
||||
|
color: white; |
||||
|
margin: 0 0 16px 0; |
||||
|
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3); |
||||
|
} |
||||
|
|
||||
|
.not-found-description { |
||||
|
font-size: 1.1rem; |
||||
|
color: rgba(255, 255, 255, 0.8); |
||||
|
margin: 0 0 24px 0; |
||||
|
line-height: 1.5; |
||||
|
} |
||||
|
|
||||
|
.home-button { |
||||
|
background: linear-gradient(135deg, #4caf50, #45a049); |
||||
|
color: white; |
||||
|
border: none; |
||||
|
padding: 12px 24px; |
||||
|
border-radius: 8px; |
||||
|
font-size: 1rem; |
||||
|
font-weight: 600; |
||||
|
cursor: pointer; |
||||
|
transition: all 0.3s ease; |
||||
|
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3); |
||||
|
} |
||||
|
|
||||
|
.home-button:hover { |
||||
|
background: linear-gradient(135deg, #45a049, #3d8b40); |
||||
|
transform: translateY(-2px); |
||||
|
box-shadow: 0 8px 25px rgba(76, 175, 80, 0.3); |
||||
|
} |
||||
|
|
||||
|
/* Responsive design */ |
||||
|
@media (max-width: 768px) { |
||||
|
.not-found-content { |
||||
|
padding: 32px 20px; |
||||
|
} |
||||
|
|
||||
|
.not-found-title { |
||||
|
font-size: 3rem; |
||||
|
} |
||||
|
|
||||
|
.not-found-icon { |
||||
|
font-size: 3rem; |
||||
|
} |
||||
|
} |
||||
|
</style> |
@ -0,0 +1,179 @@ |
|||||
|
<!-- |
||||
|
/** |
||||
|
* Notifications View - Manage Scheduled Notifications |
||||
|
* |
||||
|
* @author Matthew Raymer |
||||
|
* @version 1.0.0 |
||||
|
*/ |
||||
|
--> |
||||
|
|
||||
|
<template> |
||||
|
<div class="notifications-view"> |
||||
|
<div class="view-header"> |
||||
|
<h1 class="page-title">📱 Scheduled Notifications</h1> |
||||
|
<p class="page-subtitle">Manage your daily notifications</p> |
||||
|
</div> |
||||
|
|
||||
|
<div v-if="hasScheduledNotifications" class="notifications-list"> |
||||
|
<NotificationCard |
||||
|
v-for="notification in scheduledNotifications" |
||||
|
:key="notification.id" |
||||
|
:notification="notification" |
||||
|
@cancel="handleCancelNotification" |
||||
|
/> |
||||
|
</div> |
||||
|
|
||||
|
<div v-else class="no-notifications"> |
||||
|
<div class="empty-state"> |
||||
|
<span class="empty-icon">📅</span> |
||||
|
<h3 class="empty-title">No Notifications Scheduled</h3> |
||||
|
<p class="empty-description"> |
||||
|
Schedule your first daily notification to get started |
||||
|
</p> |
||||
|
<button class="empty-action" @click="navigateToSchedule"> |
||||
|
📅 Schedule Notification |
||||
|
</button> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</template> |
||||
|
|
||||
|
<script lang="ts"> |
||||
|
import { Component, Vue } from 'vue-facing-decorator' |
||||
|
import NotificationCard from '@/components/cards/NotificationCard.vue' |
||||
|
import { useNotificationsStore } from '@/stores/notifications' |
||||
|
|
||||
|
@Component({ |
||||
|
components: { |
||||
|
NotificationCard |
||||
|
} |
||||
|
}) |
||||
|
export default class NotificationsView extends Vue { |
||||
|
private notificationsStore = useNotificationsStore() |
||||
|
|
||||
|
get hasScheduledNotifications(): boolean { |
||||
|
return this.notificationsStore.hasScheduledNotifications |
||||
|
} |
||||
|
|
||||
|
get scheduledNotifications() { |
||||
|
return this.notificationsStore.scheduledNotifications |
||||
|
} |
||||
|
|
||||
|
async handleCancelNotification(notificationId: string): Promise<void> { |
||||
|
try { |
||||
|
await this.notificationsStore.cancelReminder(notificationId) |
||||
|
} catch (error) { |
||||
|
console.error('❌ Failed to cancel notification:', error) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
navigateToSchedule(): void { |
||||
|
this.$router.push('/schedule') |
||||
|
} |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<style scoped> |
||||
|
.notifications-view { |
||||
|
max-width: 800px; |
||||
|
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; |
||||
|
} |
||||
|
|
||||
|
.notifications-list { |
||||
|
display: flex; |
||||
|
flex-direction: column; |
||||
|
gap: 16px; |
||||
|
} |
||||
|
|
||||
|
.no-notifications { |
||||
|
display: flex; |
||||
|
justify-content: center; |
||||
|
align-items: center; |
||||
|
min-height: 400px; |
||||
|
} |
||||
|
|
||||
|
.empty-state { |
||||
|
text-align: center; |
||||
|
padding: 40px 20px; |
||||
|
background: rgba(255, 255, 255, 0.1); |
||||
|
border-radius: 16px; |
||||
|
backdrop-filter: blur(10px); |
||||
|
border: 1px solid rgba(255, 255, 255, 0.2); |
||||
|
} |
||||
|
|
||||
|
.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 0 24px 0; |
||||
|
line-height: 1.5; |
||||
|
} |
||||
|
|
||||
|
.empty-action { |
||||
|
background: linear-gradient(135deg, #4caf50, #45a049); |
||||
|
color: white; |
||||
|
border: none; |
||||
|
padding: 12px 24px; |
||||
|
border-radius: 8px; |
||||
|
font-size: 1rem; |
||||
|
font-weight: 600; |
||||
|
cursor: pointer; |
||||
|
transition: all 0.3s ease; |
||||
|
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3); |
||||
|
} |
||||
|
|
||||
|
.empty-action:hover { |
||||
|
background: linear-gradient(135deg, #45a049, #3d8b40); |
||||
|
transform: translateY(-2px); |
||||
|
box-shadow: 0 8px 25px rgba(76, 175, 80, 0.3); |
||||
|
} |
||||
|
|
||||
|
/* Responsive design */ |
||||
|
@media (max-width: 768px) { |
||||
|
.notifications-view { |
||||
|
padding: 16px; |
||||
|
} |
||||
|
|
||||
|
.empty-state { |
||||
|
padding: 32px 16px; |
||||
|
} |
||||
|
|
||||
|
.empty-icon { |
||||
|
font-size: 3rem; |
||||
|
} |
||||
|
} |
||||
|
</style> |
@ -0,0 +1,518 @@ |
|||||
|
<!-- |
||||
|
/** |
||||
|
* Schedule View - Schedule New Notifications |
||||
|
* |
||||
|
* Vue 3 + vue-facing-decorator + TypeScript |
||||
|
* |
||||
|
* @author Matthew Raymer |
||||
|
* @version 1.0.0 |
||||
|
*/ |
||||
|
--> |
||||
|
|
||||
|
<template> |
||||
|
<div class="schedule-view"> |
||||
|
<div class="schedule-header"> |
||||
|
<h1 class="page-title">📅 Schedule Notification</h1> |
||||
|
<p class="page-subtitle">Create a new daily notification</p> |
||||
|
</div> |
||||
|
|
||||
|
<div class="schedule-form"> |
||||
|
<form @submit.prevent="handleSubmit"> |
||||
|
<!-- Time Input --> |
||||
|
<div class="form-group"> |
||||
|
<label class="form-label">Time</label> |
||||
|
<input |
||||
|
v-model="form.time" |
||||
|
type="time" |
||||
|
class="form-input" |
||||
|
required |
||||
|
:disabled="isScheduling" |
||||
|
/> |
||||
|
</div> |
||||
|
|
||||
|
<!-- Title Input --> |
||||
|
<div class="form-group"> |
||||
|
<label class="form-label">Title</label> |
||||
|
<input |
||||
|
v-model="form.title" |
||||
|
type="text" |
||||
|
class="form-input" |
||||
|
placeholder="Daily Update" |
||||
|
:disabled="isScheduling" |
||||
|
/> |
||||
|
</div> |
||||
|
|
||||
|
<!-- Body Input --> |
||||
|
<div class="form-group"> |
||||
|
<label class="form-label">Message</label> |
||||
|
<textarea |
||||
|
v-model="form.body" |
||||
|
class="form-textarea" |
||||
|
placeholder="Your daily notification message" |
||||
|
rows="3" |
||||
|
:disabled="isScheduling" |
||||
|
></textarea> |
||||
|
</div> |
||||
|
|
||||
|
<!-- Options --> |
||||
|
<div class="form-options"> |
||||
|
<div class="option-group"> |
||||
|
<label class="option-label"> |
||||
|
<input |
||||
|
v-model="form.sound" |
||||
|
type="checkbox" |
||||
|
class="option-checkbox" |
||||
|
:disabled="isScheduling" |
||||
|
/> |
||||
|
<span class="option-text">🔊 Sound</span> |
||||
|
</label> |
||||
|
</div> |
||||
|
|
||||
|
<div class="option-group"> |
||||
|
<label class="option-label"> |
||||
|
<span class="option-text">Priority</span> |
||||
|
<select |
||||
|
v-model="form.priority" |
||||
|
class="form-select" |
||||
|
:disabled="isScheduling" |
||||
|
> |
||||
|
<option value="low">Low</option> |
||||
|
<option value="default">Default</option> |
||||
|
<option value="high">High</option> |
||||
|
</select> |
||||
|
</label> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<!-- URL Input --> |
||||
|
<div class="form-group"> |
||||
|
<label class="form-label">URL (Optional)</label> |
||||
|
<input |
||||
|
v-model="form.url" |
||||
|
type="url" |
||||
|
class="form-input" |
||||
|
placeholder="https://example.com" |
||||
|
:disabled="isScheduling" |
||||
|
/> |
||||
|
</div> |
||||
|
|
||||
|
<!-- Submit Button --> |
||||
|
<div class="form-actions"> |
||||
|
<button |
||||
|
type="submit" |
||||
|
class="submit-button" |
||||
|
:disabled="isScheduling || !isFormValid" |
||||
|
:class="{ 'loading': isScheduling }" |
||||
|
> |
||||
|
<span v-if="isScheduling" class="button-spinner"></span> |
||||
|
<span v-else>📅 Schedule Notification</span> |
||||
|
</button> |
||||
|
</div> |
||||
|
</form> |
||||
|
</div> |
||||
|
|
||||
|
<!-- Quick Schedule Options --> |
||||
|
<div class="quick-schedule"> |
||||
|
<h2 class="section-title">Quick Schedule</h2> |
||||
|
<div class="quick-options"> |
||||
|
<button |
||||
|
v-for="option in quickOptions" |
||||
|
:key="option.label" |
||||
|
class="quick-button" |
||||
|
@click="fillQuickOption(option)" |
||||
|
:disabled="isScheduling" |
||||
|
> |
||||
|
<span class="quick-icon">{{ option.icon }}</span> |
||||
|
<span class="quick-label">{{ option.label }}</span> |
||||
|
</button> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</template> |
||||
|
|
||||
|
<script lang="ts"> |
||||
|
import { Component, Vue } from 'vue-facing-decorator' |
||||
|
import { useAppStore } from '@/stores/app' |
||||
|
import { useNotificationsStore } from '@/stores/notifications' |
||||
|
|
||||
|
interface QuickOption { |
||||
|
label: string |
||||
|
icon: string |
||||
|
time: string |
||||
|
title: string |
||||
|
body: string |
||||
|
} |
||||
|
|
||||
|
interface ScheduleForm { |
||||
|
time: string |
||||
|
title: string |
||||
|
body: string |
||||
|
sound: boolean |
||||
|
priority: string |
||||
|
url: string |
||||
|
} |
||||
|
|
||||
|
@Component |
||||
|
export default class ScheduleView extends Vue { |
||||
|
private appStore = useAppStore() |
||||
|
private notificationsStore = useNotificationsStore() |
||||
|
|
||||
|
form: ScheduleForm = { |
||||
|
time: '', |
||||
|
title: 'Daily Update', |
||||
|
body: 'Your daily notification is ready', |
||||
|
sound: true, |
||||
|
priority: 'default', |
||||
|
url: '' |
||||
|
} |
||||
|
|
||||
|
quickOptions: QuickOption[] = [ |
||||
|
{ |
||||
|
label: 'Morning', |
||||
|
icon: '🌅', |
||||
|
time: '08:00', |
||||
|
title: 'Good Morning!', |
||||
|
body: 'Ready to make today amazing?' |
||||
|
}, |
||||
|
{ |
||||
|
label: 'Lunch', |
||||
|
icon: '🍽️', |
||||
|
time: '12:00', |
||||
|
title: 'Lunch Break', |
||||
|
body: 'Time for a well-deserved break!' |
||||
|
}, |
||||
|
{ |
||||
|
label: 'Evening', |
||||
|
icon: '🌆', |
||||
|
time: '18:00', |
||||
|
title: 'Evening Update', |
||||
|
body: 'How was your day?' |
||||
|
}, |
||||
|
{ |
||||
|
label: 'Bedtime', |
||||
|
icon: '🌙', |
||||
|
time: '22:00', |
||||
|
title: 'Bedtime Reminder', |
||||
|
body: 'Time to wind down and rest' |
||||
|
} |
||||
|
] |
||||
|
|
||||
|
get isScheduling(): boolean { |
||||
|
return this.notificationsStore.isScheduling |
||||
|
} |
||||
|
|
||||
|
get isFormValid(): boolean { |
||||
|
return this.form.time.length > 0 && |
||||
|
this.form.title.trim().length > 0 && |
||||
|
this.form.body.trim().length > 0 |
||||
|
} |
||||
|
|
||||
|
mounted(): void { |
||||
|
this.initializeForm() |
||||
|
} |
||||
|
|
||||
|
private initializeForm(): void { |
||||
|
// Set default time to 1 hour from now |
||||
|
const now = new Date() |
||||
|
now.setHours(now.getHours() + 1) |
||||
|
this.form.time = now.toTimeString().slice(0, 5) |
||||
|
} |
||||
|
|
||||
|
async handleSubmit(): Promise<void> { |
||||
|
if (!this.isFormValid || this.isScheduling) { |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
try { |
||||
|
await this.notificationsStore.scheduleNotification({ |
||||
|
time: this.form.time, |
||||
|
title: this.form.title.trim(), |
||||
|
body: this.form.body.trim(), |
||||
|
sound: this.form.sound, |
||||
|
priority: this.form.priority, |
||||
|
url: this.form.url.trim() || undefined |
||||
|
}) |
||||
|
|
||||
|
// Show success message |
||||
|
this.appStore.setError('Notification scheduled successfully!') |
||||
|
|
||||
|
// Reset form |
||||
|
this.resetForm() |
||||
|
|
||||
|
// Navigate back to home |
||||
|
setTimeout(() => { |
||||
|
this.$router.push('/') |
||||
|
}, 1500) |
||||
|
|
||||
|
} catch (error) { |
||||
|
console.error('❌ Failed to schedule notification:', error) |
||||
|
this.appStore.setError('Failed to schedule notification: ' + (error as Error).message) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
fillQuickOption(option: QuickOption): void { |
||||
|
this.form.time = option.time |
||||
|
this.form.title = option.title |
||||
|
this.form.body = option.body |
||||
|
} |
||||
|
|
||||
|
resetForm(): void { |
||||
|
this.form = { |
||||
|
time: '', |
||||
|
title: 'Daily Update', |
||||
|
body: 'Your daily notification is ready', |
||||
|
sound: true, |
||||
|
priority: 'default', |
||||
|
url: '' |
||||
|
} |
||||
|
this.initializeForm() |
||||
|
} |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<style scoped> |
||||
|
.schedule-view { |
||||
|
max-width: 600px; |
||||
|
margin: 0 auto; |
||||
|
padding: 20px; |
||||
|
} |
||||
|
|
||||
|
.schedule-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; |
||||
|
} |
||||
|
|
||||
|
.schedule-form { |
||||
|
background: rgba(255, 255, 255, 0.1); |
||||
|
border-radius: 16px; |
||||
|
padding: 24px; |
||||
|
backdrop-filter: blur(10px); |
||||
|
border: 1px solid rgba(255, 255, 255, 0.2); |
||||
|
margin-bottom: 32px; |
||||
|
} |
||||
|
|
||||
|
.form-group { |
||||
|
margin-bottom: 20px; |
||||
|
} |
||||
|
|
||||
|
.form-label { |
||||
|
display: block; |
||||
|
font-size: 0.9rem; |
||||
|
font-weight: 600; |
||||
|
color: white; |
||||
|
margin-bottom: 8px; |
||||
|
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3); |
||||
|
} |
||||
|
|
||||
|
.form-input, |
||||
|
.form-textarea, |
||||
|
.form-select { |
||||
|
width: 100%; |
||||
|
padding: 12px 16px; |
||||
|
border: 1px solid rgba(255, 255, 255, 0.3); |
||||
|
border-radius: 8px; |
||||
|
background: rgba(255, 255, 255, 0.1); |
||||
|
color: white; |
||||
|
font-size: 1rem; |
||||
|
transition: all 0.3s ease; |
||||
|
} |
||||
|
|
||||
|
.form-input:focus, |
||||
|
.form-textarea:focus, |
||||
|
.form-select:focus { |
||||
|
outline: none; |
||||
|
border-color: rgba(255, 255, 255, 0.5); |
||||
|
background: rgba(255, 255, 255, 0.15); |
||||
|
} |
||||
|
|
||||
|
.form-input::placeholder, |
||||
|
.form-textarea::placeholder { |
||||
|
color: rgba(255, 255, 255, 0.6); |
||||
|
} |
||||
|
|
||||
|
.form-textarea { |
||||
|
resize: vertical; |
||||
|
min-height: 80px; |
||||
|
} |
||||
|
|
||||
|
.form-options { |
||||
|
display: grid; |
||||
|
grid-template-columns: 1fr 1fr; |
||||
|
gap: 16px; |
||||
|
margin-bottom: 20px; |
||||
|
} |
||||
|
|
||||
|
.option-group { |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
} |
||||
|
|
||||
|
.option-label { |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
gap: 8px; |
||||
|
cursor: pointer; |
||||
|
color: white; |
||||
|
font-size: 0.9rem; |
||||
|
font-weight: 500; |
||||
|
} |
||||
|
|
||||
|
.option-checkbox { |
||||
|
width: 18px; |
||||
|
height: 18px; |
||||
|
accent-color: #4caf50; |
||||
|
} |
||||
|
|
||||
|
.option-text { |
||||
|
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3); |
||||
|
} |
||||
|
|
||||
|
.form-select { |
||||
|
margin-left: auto; |
||||
|
width: auto; |
||||
|
min-width: 100px; |
||||
|
} |
||||
|
|
||||
|
.form-actions { |
||||
|
margin-top: 24px; |
||||
|
} |
||||
|
|
||||
|
.submit-button { |
||||
|
width: 100%; |
||||
|
padding: 16px 24px; |
||||
|
background: linear-gradient(135deg, #4caf50, #45a049); |
||||
|
color: white; |
||||
|
border: none; |
||||
|
border-radius: 12px; |
||||
|
font-size: 1.1rem; |
||||
|
font-weight: 600; |
||||
|
cursor: pointer; |
||||
|
transition: all 0.3s ease; |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
justify-content: center; |
||||
|
gap: 8px; |
||||
|
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3); |
||||
|
} |
||||
|
|
||||
|
.submit-button:hover:not(:disabled) { |
||||
|
background: linear-gradient(135deg, #45a049, #3d8b40); |
||||
|
transform: translateY(-2px); |
||||
|
box-shadow: 0 8px 25px rgba(76, 175, 80, 0.3); |
||||
|
} |
||||
|
|
||||
|
.submit-button:disabled { |
||||
|
opacity: 0.6; |
||||
|
cursor: not-allowed; |
||||
|
transform: none; |
||||
|
} |
||||
|
|
||||
|
.submit-button.loading { |
||||
|
background: linear-gradient(135deg, #9e9e9e, #757575); |
||||
|
} |
||||
|
|
||||
|
.button-spinner { |
||||
|
width: 20px; |
||||
|
height: 20px; |
||||
|
border: 2px solid rgba(255, 255, 255, 0.3); |
||||
|
border-top: 2px solid white; |
||||
|
border-radius: 50%; |
||||
|
animation: spin 1s linear infinite; |
||||
|
} |
||||
|
|
||||
|
.quick-schedule { |
||||
|
background: rgba(255, 255, 255, 0.1); |
||||
|
border-radius: 16px; |
||||
|
padding: 24px; |
||||
|
backdrop-filter: blur(10px); |
||||
|
border: 1px solid rgba(255, 255, 255, 0.2); |
||||
|
} |
||||
|
|
||||
|
.section-title { |
||||
|
font-size: 1.3rem; |
||||
|
font-weight: 600; |
||||
|
color: white; |
||||
|
margin: 0 0 16px 0; |
||||
|
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3); |
||||
|
} |
||||
|
|
||||
|
.quick-options { |
||||
|
display: grid; |
||||
|
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); |
||||
|
gap: 12px; |
||||
|
} |
||||
|
|
||||
|
.quick-button { |
||||
|
display: flex; |
||||
|
flex-direction: column; |
||||
|
align-items: center; |
||||
|
gap: 8px; |
||||
|
padding: 16px 12px; |
||||
|
background: rgba(255, 255, 255, 0.1); |
||||
|
border: 1px solid rgba(255, 255, 255, 0.2); |
||||
|
border-radius: 12px; |
||||
|
color: white; |
||||
|
cursor: pointer; |
||||
|
transition: all 0.3s ease; |
||||
|
text-decoration: none; |
||||
|
} |
||||
|
|
||||
|
.quick-button:hover:not(:disabled) { |
||||
|
background: rgba(255, 255, 255, 0.15); |
||||
|
transform: translateY(-2px); |
||||
|
} |
||||
|
|
||||
|
.quick-button:disabled { |
||||
|
opacity: 0.5; |
||||
|
cursor: not-allowed; |
||||
|
} |
||||
|
|
||||
|
.quick-icon { |
||||
|
font-size: 1.5rem; |
||||
|
} |
||||
|
|
||||
|
.quick-label { |
||||
|
font-size: 0.85rem; |
||||
|
font-weight: 500; |
||||
|
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3); |
||||
|
} |
||||
|
|
||||
|
@keyframes spin { |
||||
|
0% { transform: rotate(0deg); } |
||||
|
100% { transform: rotate(360deg); } |
||||
|
} |
||||
|
|
||||
|
/* Responsive design */ |
||||
|
@media (max-width: 768px) { |
||||
|
.schedule-view { |
||||
|
padding: 16px; |
||||
|
} |
||||
|
|
||||
|
.schedule-form { |
||||
|
padding: 20px; |
||||
|
} |
||||
|
|
||||
|
.form-options { |
||||
|
grid-template-columns: 1fr; |
||||
|
} |
||||
|
|
||||
|
.quick-options { |
||||
|
grid-template-columns: repeat(2, 1fr); |
||||
|
} |
||||
|
} |
||||
|
</style> |
@ -0,0 +1,71 @@ |
|||||
|
<!-- |
||||
|
/** |
||||
|
* Settings View - App Settings |
||||
|
* |
||||
|
* @author Matthew Raymer |
||||
|
* @version 1.0.0 |
||||
|
*/ |
||||
|
--> |
||||
|
|
||||
|
<template> |
||||
|
<div class="settings-view"> |
||||
|
<div class="view-header"> |
||||
|
<h1 class="page-title">⚙️ Settings</h1> |
||||
|
<p class="page-subtitle">Configure app preferences</p> |
||||
|
</div> |
||||
|
|
||||
|
<div class="settings-content"> |
||||
|
<p class="coming-soon">Settings panel coming soon...</p> |
||||
|
</div> |
||||
|
</div> |
||||
|
</template> |
||||
|
|
||||
|
<script lang="ts"> |
||||
|
import { Component, Vue } from 'vue-facing-decorator' |
||||
|
|
||||
|
@Component |
||||
|
export default class SettingsView extends Vue {} |
||||
|
</script> |
||||
|
|
||||
|
<style scoped> |
||||
|
.settings-view { |
||||
|
max-width: 800px; |
||||
|
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; |
||||
|
} |
||||
|
|
||||
|
.settings-content { |
||||
|
background: rgba(255, 255, 255, 0.1); |
||||
|
border-radius: 16px; |
||||
|
padding: 40px; |
||||
|
backdrop-filter: blur(10px); |
||||
|
border: 1px solid rgba(255, 255, 255, 0.2); |
||||
|
text-align: center; |
||||
|
} |
||||
|
|
||||
|
.coming-soon { |
||||
|
font-size: 1.2rem; |
||||
|
color: rgba(255, 255, 255, 0.8); |
||||
|
margin: 0; |
||||
|
} |
||||
|
</style> |
@ -0,0 +1,310 @@ |
|||||
|
<!-- |
||||
|
/** |
||||
|
* Status View - System Status and Diagnostics |
||||
|
* |
||||
|
* @author Matthew Raymer |
||||
|
* @version 1.0.0 |
||||
|
*/ |
||||
|
--> |
||||
|
|
||||
|
<template> |
||||
|
<div class="status-view"> |
||||
|
<div class="view-header"> |
||||
|
<h1 class="page-title">📊 System Status</h1> |
||||
|
<p class="page-subtitle">Notification system diagnostics</p> |
||||
|
</div> |
||||
|
|
||||
|
<!-- Status Overview --> |
||||
|
<div class="status-overview"> |
||||
|
<StatusCard |
||||
|
:status="notificationStatus" |
||||
|
@refresh="refreshStatus" |
||||
|
/> |
||||
|
</div> |
||||
|
|
||||
|
<!-- Action Buttons --> |
||||
|
<div class="status-actions"> |
||||
|
<h2 class="section-title">Actions</h2> |
||||
|
<div class="action-grid"> |
||||
|
<button |
||||
|
class="action-button" |
||||
|
@click="refreshStatus" |
||||
|
:disabled="isRefreshing" |
||||
|
> |
||||
|
<span class="action-icon">🔄</span> |
||||
|
<span class="action-text">Refresh Status</span> |
||||
|
</button> |
||||
|
|
||||
|
<button |
||||
|
class="action-button" |
||||
|
@click="openChannelSettings" |
||||
|
> |
||||
|
<span class="action-icon">🔧</span> |
||||
|
<span class="action-text">Channel Settings</span> |
||||
|
</button> |
||||
|
|
||||
|
<button |
||||
|
class="action-button" |
||||
|
@click="openExactAlarmSettings" |
||||
|
> |
||||
|
<span class="action-icon">⏰</span> |
||||
|
<span class="action-text">Alarm Settings</span> |
||||
|
</button> |
||||
|
|
||||
|
<button |
||||
|
class="action-button" |
||||
|
@click="testNotification" |
||||
|
:disabled="!canScheduleNow" |
||||
|
> |
||||
|
<span class="action-icon">🧪</span> |
||||
|
<span class="action-text">Test Notification</span> |
||||
|
</button> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<!-- Detailed Information --> |
||||
|
<div v-if="notificationStatus" class="detailed-info"> |
||||
|
<h2 class="section-title">Detailed Information</h2> |
||||
|
<div class="info-grid"> |
||||
|
<InfoCard |
||||
|
title="Platform" |
||||
|
:value="platformInfo" |
||||
|
icon="📱" |
||||
|
/> |
||||
|
<InfoCard |
||||
|
title="Android Version" |
||||
|
:value="`API ${notificationStatus.androidVersion}`" |
||||
|
icon="🤖" |
||||
|
/> |
||||
|
<InfoCard |
||||
|
title="Channel ID" |
||||
|
:value="notificationStatus.channelId" |
||||
|
icon="📢" |
||||
|
/> |
||||
|
<InfoCard |
||||
|
title="Next Scheduled" |
||||
|
:value="nextScheduledText" |
||||
|
icon="⏰" |
||||
|
/> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</template> |
||||
|
|
||||
|
<script lang="ts"> |
||||
|
import { Component, Vue } from 'vue-facing-decorator' |
||||
|
import { Capacitor } from '@capacitor/core' |
||||
|
import StatusCard from '@/components/cards/StatusCard.vue' |
||||
|
import InfoCard from '@/components/cards/InfoCard.vue' |
||||
|
import { useAppStore } from '@/stores/app' |
||||
|
import { useNotificationsStore } from '@/stores/notifications' |
||||
|
|
||||
|
@Component({ |
||||
|
components: { |
||||
|
StatusCard, |
||||
|
InfoCard |
||||
|
} |
||||
|
}) |
||||
|
export default class StatusView extends Vue { |
||||
|
private appStore = useAppStore() |
||||
|
private notificationsStore = useNotificationsStore() |
||||
|
isRefreshing = false |
||||
|
|
||||
|
get notificationStatus() { |
||||
|
return this.appStore.notificationStatus |
||||
|
} |
||||
|
|
||||
|
get canScheduleNow(): boolean { |
||||
|
return this.notificationStatus?.canScheduleNow ?? false |
||||
|
} |
||||
|
|
||||
|
get platformInfo(): string { |
||||
|
const platform = Capacitor.getPlatform() |
||||
|
const isNative = Capacitor.isNativePlatform() |
||||
|
return `${platform} ${isNative ? '(Native)' : '(Web)'}` |
||||
|
} |
||||
|
|
||||
|
get nextScheduledText(): string { |
||||
|
if (!this.notificationStatus || this.notificationStatus.nextScheduledAt <= 0) { |
||||
|
return 'None' |
||||
|
} |
||||
|
|
||||
|
const date = new Date(this.notificationStatus.nextScheduledAt) |
||||
|
return date.toLocaleString() |
||||
|
} |
||||
|
|
||||
|
async mounted(): Promise<void> { |
||||
|
await this.refreshStatus() |
||||
|
} |
||||
|
|
||||
|
async refreshStatus(): Promise<void> { |
||||
|
try { |
||||
|
this.isRefreshing = true |
||||
|
await this.notificationsStore.checkStatus() |
||||
|
} catch (error) { |
||||
|
console.error('❌ Failed to refresh status:', error) |
||||
|
} finally { |
||||
|
this.isRefreshing = false |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
async openChannelSettings(): Promise<void> { |
||||
|
if (!Capacitor.isNativePlatform() || !window.DailyNotification) { |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
try { |
||||
|
await window.DailyNotification.openChannelSettings() |
||||
|
} catch (error) { |
||||
|
console.error('❌ Failed to open channel settings:', error) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
async openExactAlarmSettings(): Promise<void> { |
||||
|
if (!Capacitor.isNativePlatform() || !window.DailyNotification) { |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
try { |
||||
|
await window.DailyNotification.openExactAlarmSettings() |
||||
|
} catch (error) { |
||||
|
console.error('❌ Failed to open exact alarm settings:', error) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
async testNotification(): Promise<void> { |
||||
|
try { |
||||
|
await this.notificationsStore.scheduleNotification({ |
||||
|
time: this.getTestTime(), |
||||
|
title: 'Test Notification', |
||||
|
body: 'This is a test notification from the status page', |
||||
|
sound: true, |
||||
|
priority: 'high' |
||||
|
}) |
||||
|
|
||||
|
this.appStore.setError('Test notification scheduled successfully!') |
||||
|
|
||||
|
} catch (error) { |
||||
|
console.error('❌ Failed to schedule test notification:', error) |
||||
|
this.appStore.setError('Failed to schedule test notification: ' + (error as Error).message) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
getTestTime(): string { |
||||
|
const now = new Date() |
||||
|
now.setMinutes(now.getMinutes() + 2) // 2 minutes from now |
||||
|
return now.toTimeString().slice(0, 5) |
||||
|
} |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<style scoped> |
||||
|
.status-view { |
||||
|
max-width: 1000px; |
||||
|
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; |
||||
|
} |
||||
|
|
||||
|
.status-overview { |
||||
|
margin-bottom: 32px; |
||||
|
} |
||||
|
|
||||
|
.status-actions { |
||||
|
margin-bottom: 32px; |
||||
|
} |
||||
|
|
||||
|
.section-title { |
||||
|
font-size: 1.5rem; |
||||
|
font-weight: 600; |
||||
|
color: white; |
||||
|
margin: 0 0 16px 0; |
||||
|
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3); |
||||
|
} |
||||
|
|
||||
|
.action-grid { |
||||
|
display: grid; |
||||
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); |
||||
|
gap: 16px; |
||||
|
} |
||||
|
|
||||
|
.action-button { |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
gap: 12px; |
||||
|
padding: 16px 20px; |
||||
|
background: rgba(255, 255, 255, 0.1); |
||||
|
border: 1px solid rgba(255, 255, 255, 0.2); |
||||
|
border-radius: 12px; |
||||
|
color: white; |
||||
|
cursor: pointer; |
||||
|
transition: all 0.3s ease; |
||||
|
backdrop-filter: blur(10px); |
||||
|
text-decoration: none; |
||||
|
} |
||||
|
|
||||
|
.action-button:hover:not(:disabled) { |
||||
|
background: rgba(255, 255, 255, 0.15); |
||||
|
transform: translateY(-2px); |
||||
|
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.2); |
||||
|
} |
||||
|
|
||||
|
.action-button:disabled { |
||||
|
opacity: 0.5; |
||||
|
cursor: not-allowed; |
||||
|
} |
||||
|
|
||||
|
.action-icon { |
||||
|
font-size: 1.2rem; |
||||
|
} |
||||
|
|
||||
|
.action-text { |
||||
|
font-size: 0.95rem; |
||||
|
font-weight: 500; |
||||
|
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3); |
||||
|
} |
||||
|
|
||||
|
.detailed-info { |
||||
|
margin-bottom: 32px; |
||||
|
} |
||||
|
|
||||
|
.info-grid { |
||||
|
display: grid; |
||||
|
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); |
||||
|
gap: 16px; |
||||
|
} |
||||
|
|
||||
|
/* Responsive design */ |
||||
|
@media (max-width: 768px) { |
||||
|
.status-view { |
||||
|
padding: 16px; |
||||
|
} |
||||
|
|
||||
|
.action-grid { |
||||
|
grid-template-columns: 1fr; |
||||
|
} |
||||
|
|
||||
|
.info-grid { |
||||
|
grid-template-columns: 1fr; |
||||
|
} |
||||
|
} |
||||
|
</style> |
@ -1,15 +1,45 @@ |
|||||
{ |
{ |
||||
"compilerOptions": { |
"compilerOptions": { |
||||
"target": "ES2020", |
"target": "ES2020", |
||||
"module": "ES2020", |
"useDefineForClassFields": true, |
||||
"moduleResolution": "node", |
"module": "ESNext", |
||||
"strict": true, |
"lib": ["ES2020", "DOM", "DOM.Iterable"], |
||||
"esModuleInterop": true, |
|
||||
"skipLibCheck": true, |
"skipLibCheck": true, |
||||
"forceConsistentCasingInFileNames": true, |
"moduleResolution": "bundler", |
||||
"outDir": "./dist", |
"allowImportingTsExtensions": true, |
||||
"rootDir": "./src" |
"resolveJsonModule": true, |
||||
|
"isolatedModules": true, |
||||
|
"noEmit": true, |
||||
|
"jsx": "preserve", |
||||
|
"strict": true, |
||||
|
"noUnusedLocals": false, |
||||
|
"noUnusedParameters": false, |
||||
|
"noFallthroughCasesInSwitch": true, |
||||
|
"experimentalDecorators": true, |
||||
|
"emitDecoratorMetadata": true, |
||||
|
"baseUrl": ".", |
||||
|
"paths": { |
||||
|
"@/*": ["./src/*"], |
||||
|
"@components/*": ["./src/components/*"], |
||||
|
"@views/*": ["./src/views/*"], |
||||
|
"@stores/*": ["./src/stores/*"], |
||||
|
"@services/*": ["./src/services/*"], |
||||
|
"@types/*": ["./src/types/*"], |
||||
|
"@utils/*": ["./src/utils/*"] |
||||
|
}, |
||||
|
"types": [ |
||||
|
"vite/client", |
||||
|
"node" |
||||
|
] |
||||
}, |
}, |
||||
"include": ["src/**/*"], |
"include": [ |
||||
"exclude": ["node_modules", "dist"] |
"env.d.ts", |
||||
|
"src/**/*", |
||||
|
"src/**/*.vue" |
||||
|
], |
||||
|
"exclude": [ |
||||
|
"src/**/__tests__/*", |
||||
|
"node_modules", |
||||
|
"dist" |
||||
|
] |
||||
} |
} |
@ -0,0 +1,70 @@ |
|||||
|
/** |
||||
|
* Vite Configuration for Daily Notification Test App |
||||
|
* |
||||
|
* Vue 3 + TypeScript + Capacitor setup with vue-facing-decorator support |
||||
|
* |
||||
|
* @author Matthew Raymer |
||||
|
* @version 1.0.0 |
||||
|
*/ |
||||
|
|
||||
|
import { defineConfig } from 'vite' |
||||
|
import vue from '@vitejs/plugin-vue' |
||||
|
import { resolve } from 'path' |
||||
|
|
||||
|
// https://vitejs.dev/config/
|
||||
|
export default defineConfig({ |
||||
|
plugins: [vue()], |
||||
|
|
||||
|
// Build configuration for Capacitor
|
||||
|
build: { |
||||
|
outDir: 'dist', |
||||
|
assetsDir: 'assets', |
||||
|
sourcemap: true, |
||||
|
rollupOptions: { |
||||
|
input: { |
||||
|
main: resolve(__dirname, 'index.html') |
||||
|
} |
||||
|
} |
||||
|
}, |
||||
|
|
||||
|
// Development server configuration
|
||||
|
server: { |
||||
|
host: '0.0.0.0', |
||||
|
port: 3000, |
||||
|
strictPort: true, |
||||
|
hmr: { |
||||
|
port: 3001 |
||||
|
} |
||||
|
}, |
||||
|
|
||||
|
// Preview server configuration
|
||||
|
preview: { |
||||
|
host: '0.0.0.0', |
||||
|
port: 4173, |
||||
|
strictPort: true |
||||
|
}, |
||||
|
|
||||
|
// Path resolution
|
||||
|
resolve: { |
||||
|
alias: { |
||||
|
'@': resolve(__dirname, 'src'), |
||||
|
'@components': resolve(__dirname, 'src/components'), |
||||
|
'@views': resolve(__dirname, 'src/views'), |
||||
|
'@stores': resolve(__dirname, 'src/stores'), |
||||
|
'@services': resolve(__dirname, 'src/services'), |
||||
|
'@types': resolve(__dirname, 'src/types'), |
||||
|
'@utils': resolve(__dirname, 'src/utils') |
||||
|
} |
||||
|
}, |
||||
|
|
||||
|
// TypeScript configuration
|
||||
|
esbuild: { |
||||
|
target: 'es2020' |
||||
|
}, |
||||
|
|
||||
|
// Define global constants
|
||||
|
define: { |
||||
|
__VUE_OPTIONS_API__: true, |
||||
|
__VUE_PROD_DEVTOOLS__: false |
||||
|
} |
||||
|
}) |
Loading…
Reference in new issue