Browse Source

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.
master
Matthew Raymer 7 days ago
parent
commit
6213235a16
  1. 20
      test-apps/android-test/.eslintrc.cjs
  2. 329
      test-apps/android-test/README.md
  3. 2
      test-apps/android-test/capacitor.config.ts
  4. 90
      test-apps/android-test/env.d.ts
  5. 73
      test-apps/android-test/index.html
  6. 39
      test-apps/android-test/package.json
  7. 145
      test-apps/android-test/src/App.vue
  8. 163
      test-apps/android-test/src/components/cards/ActionCard.vue
  9. 95
      test-apps/android-test/src/components/cards/InfoCard.vue
  10. 181
      test-apps/android-test/src/components/cards/NotificationCard.vue
  11. 264
      test-apps/android-test/src/components/cards/StatusCard.vue
  12. 141
      test-apps/android-test/src/components/items/ActivityItem.vue
  13. 170
      test-apps/android-test/src/components/items/HistoryItem.vue
  14. 99
      test-apps/android-test/src/components/items/StatusItem.vue
  15. 75
      test-apps/android-test/src/components/layout/AppFooter.vue
  16. 238
      test-apps/android-test/src/components/layout/AppHeader.vue
  17. 161
      test-apps/android-test/src/components/ui/ErrorDialog.vue
  18. 78
      test-apps/android-test/src/components/ui/LoadingOverlay.vue
  19. 42
      test-apps/android-test/src/main.ts
  20. 100
      test-apps/android-test/src/router/index.ts
  21. 108
      test-apps/android-test/src/stores/app.ts
  22. 275
      test-apps/android-test/src/stores/notifications.ts
  23. 144
      test-apps/android-test/src/views/HistoryView.vue
  24. 250
      test-apps/android-test/src/views/HomeView.vue
  25. 117
      test-apps/android-test/src/views/NotFoundView.vue
  26. 179
      test-apps/android-test/src/views/NotificationsView.vue
  27. 519
      test-apps/android-test/src/views/ScheduleView.vue
  28. 71
      test-apps/android-test/src/views/SettingsView.vue
  29. 310
      test-apps/android-test/src/views/StatusView.vue
  30. 52
      test-apps/android-test/tsconfig.json
  31. 70
      test-apps/android-test/vite.config.ts

20
test-apps/android-test/.eslintrc.cjs

@ -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

@ -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
test-apps/android-test/capacitor.config.ts

@ -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

@ -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

@ -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>

39
test-apps/android-test/package.json

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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>

52
test-apps/android-test/tsconfig.json

@ -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

@ -0,0 +1,70 @@
/**
* Vite Configuration for Daily Notification Test App
*
* Vue 3 + TypeScript + Capacitor setup with vue-facing-decorator support
*
* @author Matthew Raymer
* @version 1.0.0
*/
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { resolve } from 'path'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [vue()],
// Build configuration for Capacitor
build: {
outDir: 'dist',
assetsDir: 'assets',
sourcemap: true,
rollupOptions: {
input: {
main: resolve(__dirname, 'index.html')
}
}
},
// Development server configuration
server: {
host: '0.0.0.0',
port: 3000,
strictPort: true,
hmr: {
port: 3001
}
},
// Preview server configuration
preview: {
host: '0.0.0.0',
port: 4173,
strictPort: true
},
// Path resolution
resolve: {
alias: {
'@': resolve(__dirname, 'src'),
'@components': resolve(__dirname, 'src/components'),
'@views': resolve(__dirname, 'src/views'),
'@stores': resolve(__dirname, 'src/stores'),
'@services': resolve(__dirname, 'src/services'),
'@types': resolve(__dirname, 'src/types'),
'@utils': resolve(__dirname, 'src/utils')
}
},
// TypeScript configuration
esbuild: {
target: 'es2020'
},
// Define global constants
define: {
__VUE_OPTIONS_API__: true,
__VUE_PROD_DEVTOOLS__: false
}
})
Loading…
Cancel
Save