feat(test-app): refactor to Vue 3 + Vite + vue-facing-decorator architecture
Complete refactoring of android-test app to modern Vue 3 stack: ## 🚀 New Architecture - Vue 3 with Composition API and TypeScript - Vite for fast development and building - vue-facing-decorator for class-based components - Pinia for reactive state management - Vue Router for navigation - Modern glassmorphism UI design ## 📱 App Structure - Comprehensive component library (cards, items, layout, ui) - Pinia stores for app and notification state management - Full view system (Home, Schedule, Notifications, Status, History) - Responsive design for mobile and desktop - TypeScript throughout with proper type definitions ## 🎨 Features - Dashboard with quick actions and status overview - Schedule notifications with time picker and options - Notification management with cancel functionality - System status with permission checks and diagnostics - Notification history with delivery tracking - Settings panel (placeholder for future features) ## 🔧 Technical Implementation - Class-based Vue components using vue-facing-decorator - Reactive Pinia stores with proper TypeScript types - Capacitor integration for native Android functionality - ESLint and TypeScript configuration - Vite build system with proper aliases and optimization ## 📚 Documentation - Comprehensive README with setup and usage instructions - Component documentation and examples - Development and production build instructions - Testing and debugging guidelines This creates a production-ready test app that closely mirrors the actual TimeSafari app architecture, making it ideal for plugin testing and demonstration purposes.
This commit is contained in:
20
test-apps/android-test/.eslintrc.cjs
Normal file
20
test-apps/android-test/.eslintrc.cjs
Normal file
@@ -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'
|
||||
}
|
||||
}
|
||||
329
test-apps/android-test/README.md
Normal file
329
test-apps/android-test/README.md
Normal file
@@ -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**
|
||||
@@ -2,7 +2,7 @@ import { CapacitorConfig } from '@capacitor/cli';
|
||||
|
||||
const config: CapacitorConfig = {
|
||||
appId: 'com.timesafari.dailynotification.androidtest',
|
||||
appName: 'Daily Notification Android Test',
|
||||
appName: 'Daily Notification Test - Vue 3',
|
||||
webDir: 'dist',
|
||||
server: {
|
||||
androidScheme: 'https'
|
||||
|
||||
90
test-apps/android-test/env.d.ts
vendored
Normal file
90
test-apps/android-test/env.d.ts
vendored
Normal file
@@ -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 {}
|
||||
73
test-apps/android-test/index.html
Normal file
73
test-apps/android-test/index.html
Normal file
@@ -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>
|
||||
@@ -1,29 +1,42 @@
|
||||
{
|
||||
"name": "daily-notification-android-test",
|
||||
"version": "1.0.0",
|
||||
"description": "Minimal Android test app for Daily Notification Plugin",
|
||||
"main": "index.js",
|
||||
"description": "Vue 3 + Vite + Capacitor test app for Daily Notification Plugin",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "webpack --mode=production",
|
||||
"dev": "webpack serve --mode=development",
|
||||
"dev": "vite",
|
||||
"build": "vue-tsc && vite build",
|
||||
"preview": "vite preview",
|
||||
"android": "npx cap run 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",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@capacitor/core": "^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": {
|
||||
"webpack": "^5.88.0",
|
||||
"webpack-cli": "^5.1.0",
|
||||
"webpack-dev-server": "^4.15.0",
|
||||
"html-webpack-plugin": "^5.5.0",
|
||||
"typescript": "^5.0.0",
|
||||
"ts-loader": "^9.4.0"
|
||||
"@capacitor/cli": "^5.0.0",
|
||||
"@types/node": "^20.0.0",
|
||||
"@typescript-eslint/eslint-plugin": "^6.0.0",
|
||||
"@typescript-eslint/parser": "^6.0.0",
|
||||
"@vitejs/plugin-vue": "^4.5.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"
|
||||
}
|
||||
}
|
||||
145
test-apps/android-test/src/App.vue
Normal file
145
test-apps/android-test/src/App.vue
Normal file
@@ -0,0 +1,145 @@
|
||||
<!--
|
||||
/**
|
||||
* 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) {
|
||||
throw new Error('DailyNotification plugin not available')
|
||||
}
|
||||
|
||||
console.log('✅ DailyNotification plugin available')
|
||||
} else {
|
||||
console.log('🌐 Running in web mode')
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private 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>
|
||||
163
test-apps/android-test/src/components/cards/ActionCard.vue
Normal file
163
test-apps/android-test/src/components/cards/ActionCard.vue
Normal file
@@ -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
|
||||
|
||||
private 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>
|
||||
95
test-apps/android-test/src/components/cards/InfoCard.vue
Normal file
95
test-apps/android-test/src/components/cards/InfoCard.vue
Normal file
@@ -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>
|
||||
181
test-apps/android-test/src/components/cards/NotificationCard.vue
Normal file
181
test-apps/android-test/src/components/cards/NotificationCard.vue
Normal file
@@ -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>
|
||||
264
test-apps/android-test/src/components/cards/StatusCard.vue
Normal file
264
test-apps/android-test/src/components/cards/StatusCard.vue
Normal file
@@ -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`
|
||||
}
|
||||
}
|
||||
|
||||
private 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>
|
||||
141
test-apps/android-test/src/components/items/ActivityItem.vue
Normal file
141
test-apps/android-test/src/components/items/ActivityItem.vue
Normal file
@@ -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>
|
||||
170
test-apps/android-test/src/components/items/HistoryItem.vue
Normal file
170
test-apps/android-test/src/components/items/HistoryItem.vue
Normal file
@@ -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>
|
||||
99
test-apps/android-test/src/components/items/StatusItem.vue
Normal file
99
test-apps/android-test/src/components/items/StatusItem.vue
Normal file
@@ -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>
|
||||
75
test-apps/android-test/src/components/layout/AppFooter.vue
Normal file
75
test-apps/android-test/src/components/layout/AppFooter.vue
Normal file
@@ -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>
|
||||
238
test-apps/android-test/src/components/layout/AppHeader.vue
Normal file
238
test-apps/android-test/src/components/layout/AppHeader.vue
Normal file
@@ -0,0 +1,238 @@
|
||||
<!--
|
||||
/**
|
||||
* 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()
|
||||
|
||||
private 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: '📋' }
|
||||
]
|
||||
|
||||
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>
|
||||
161
test-apps/android-test/src/components/ui/ErrorDialog.vue
Normal file
161
test-apps/android-test/src/components/ui/ErrorDialog.vue
Normal file
@@ -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
|
||||
|
||||
private handleClose(): void {
|
||||
this.$emit('close')
|
||||
}
|
||||
|
||||
private handleRetry(): void {
|
||||
this.$emit('retry')
|
||||
}
|
||||
|
||||
private 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>
|
||||
78
test-apps/android-test/src/components/ui/LoadingOverlay.vue
Normal file
78
test-apps/android-test/src/components/ui/LoadingOverlay.vue
Normal file
@@ -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>
|
||||
42
test-apps/android-test/src/main.ts
Normal file
42
test-apps/android-test/src/main.ts
Normal file
@@ -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'))
|
||||
100
test-apps/android-test/src/router/index.ts
Normal file
100
test-apps/android-test/src/router/index.ts
Normal file
@@ -0,0 +1,100 @@
|
||||
/**
|
||||
* 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: '/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 ${from.name || 'unknown'} to ${to.name || 'unknown'}`)
|
||||
|
||||
next()
|
||||
})
|
||||
|
||||
router.afterEach((to, from) => {
|
||||
// Clear any previous errors on successful navigation
|
||||
console.log(`✅ Navigation completed: ${to.name || 'unknown'}`)
|
||||
})
|
||||
|
||||
export default router
|
||||
108
test-apps/android-test/src/stores/app.ts
Normal file
108
test-apps/android-test/src/stores/app.ts
Normal file
@@ -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
|
||||
}
|
||||
})
|
||||
275
test-apps/android-test/src/stores/notifications.ts
Normal file
275
test-apps/android-test/src/stores/notifications.ts
Normal file
@@ -0,0 +1,275 @@
|
||||
/**
|
||||
* 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() || !window.DailyNotification) {
|
||||
throw new Error('DailyNotification plugin not available')
|
||||
}
|
||||
|
||||
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() || !window.DailyNotification) {
|
||||
throw new Error('DailyNotification plugin not available')
|
||||
}
|
||||
|
||||
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() || !window.DailyNotification) {
|
||||
throw new Error('DailyNotification plugin not available')
|
||||
}
|
||||
|
||||
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() || !window.DailyNotification) {
|
||||
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() || !window.DailyNotification) {
|
||||
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
|
||||
}
|
||||
})
|
||||
144
test-apps/android-test/src/views/HistoryView.vue
Normal file
144
test-apps/android-test/src/views/HistoryView.vue
Normal file
@@ -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>
|
||||
250
test-apps/android-test/src/views/HomeView.vue
Normal file
250
test-apps/android-test/src/views/HomeView.vue
Normal file
@@ -0,0 +1,250 @@
|
||||
<!--
|
||||
/**
|
||||
* 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"
|
||||
/>
|
||||
</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()
|
||||
private 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)
|
||||
}
|
||||
}
|
||||
|
||||
private 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
|
||||
}
|
||||
}
|
||||
|
||||
private navigateToSchedule(): void {
|
||||
this.$router.push('/schedule')
|
||||
}
|
||||
|
||||
private navigateToNotifications(): void {
|
||||
this.$router.push('/notifications')
|
||||
}
|
||||
|
||||
private navigateToHistory(): void {
|
||||
this.$router.push('/history')
|
||||
}
|
||||
}
|
||||
</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>
|
||||
117
test-apps/android-test/src/views/NotFoundView.vue
Normal file
117
test-apps/android-test/src/views/NotFoundView.vue
Normal file
@@ -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 {
|
||||
private 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>
|
||||
179
test-apps/android-test/src/views/NotificationsView.vue
Normal file
179
test-apps/android-test/src/views/NotificationsView.vue
Normal file
@@ -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
|
||||
}
|
||||
|
||||
private async handleCancelNotification(notificationId: string): Promise<void> {
|
||||
try {
|
||||
await this.notificationsStore.cancelReminder(notificationId)
|
||||
} catch (error) {
|
||||
console.error('❌ Failed to cancel notification:', error)
|
||||
}
|
||||
}
|
||||
|
||||
private 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>
|
||||
519
test-apps/android-test/src/views/ScheduleView.vue
Normal file
519
test-apps/android-test/src/views/ScheduleView.vue
Normal file
@@ -0,0 +1,519 @@
|
||||
<!--
|
||||
/**
|
||||
* 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 { Capacitor } from '@capacitor/core'
|
||||
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()
|
||||
|
||||
private form: ScheduleForm = {
|
||||
time: '',
|
||||
title: 'Daily Update',
|
||||
body: 'Your daily notification is ready',
|
||||
sound: true,
|
||||
priority: 'default',
|
||||
url: ''
|
||||
}
|
||||
|
||||
private 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)
|
||||
}
|
||||
|
||||
private 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)
|
||||
}
|
||||
}
|
||||
|
||||
private fillQuickOption(option: QuickOption): void {
|
||||
this.form.time = option.time
|
||||
this.form.title = option.title
|
||||
this.form.body = option.body
|
||||
}
|
||||
|
||||
private 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>
|
||||
71
test-apps/android-test/src/views/SettingsView.vue
Normal file
71
test-apps/android-test/src/views/SettingsView.vue
Normal file
@@ -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>
|
||||
310
test-apps/android-test/src/views/StatusView.vue
Normal file
310
test-apps/android-test/src/views/StatusView.vue
Normal file
@@ -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()
|
||||
private 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()
|
||||
}
|
||||
|
||||
private 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
|
||||
}
|
||||
}
|
||||
|
||||
private 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)
|
||||
}
|
||||
}
|
||||
|
||||
private 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)
|
||||
}
|
||||
}
|
||||
|
||||
private 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)
|
||||
}
|
||||
}
|
||||
|
||||
private 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 @@
|
||||
{
|
||||
"extends": "@vue/tsconfig/tsconfig.dom.json",
|
||||
"include": [
|
||||
"env.d.ts",
|
||||
"src/**/*",
|
||||
"src/**/*.vue"
|
||||
],
|
||||
"exclude": [
|
||||
"src/**/__tests__/*"
|
||||
],
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"module": "ES2020",
|
||||
"moduleResolution": "node",
|
||||
"composite": 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"
|
||||
],
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"experimentalDecorators": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
"target": "ES2020",
|
||||
"useDefineForClassFields": true,
|
||||
"module": "ESNext",
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src"
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "preserve"
|
||||
}
|
||||
}
|
||||
70
test-apps/android-test/vite.config.ts
Normal file
70
test-apps/android-test/vite.config.ts
Normal file
@@ -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
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user