chore: initial commit

This commit is contained in:
Matthew Raymer
2025-10-15 10:46:50 +00:00
parent 54478b1c97
commit 1e6c4bf7fc
174 changed files with 6867 additions and 21404 deletions

View File

@@ -0,0 +1,8 @@
[*.{js,jsx,mjs,cjs,ts,tsx,mts,cts,vue,css,scss,sass,less,styl}]
charset = utf-8
indent_size = 2
indent_style = space
insert_final_newline = true
trim_trailing_whitespace = true
end_of_line = lf
max_line_length = 100

View File

@@ -0,0 +1 @@
* text=auto eol=lf

View File

@@ -0,0 +1,30 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
.DS_Store
dist
dist-ssr
coverage
*.local
/cypress/videos/
/cypress/screenshots/
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
*.tsbuildinfo

View File

@@ -0,0 +1,48 @@
# daily-notification-test
This template should help get you started developing with Vue 3 in Vite.
## Recommended IDE Setup
[VS Code](https://code.visualstudio.com/) + [Vue (Official)](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur).
## Recommended Browser Setup
- Chromium-based browsers (Chrome, Edge, Brave, etc.):
- [Vue.js devtools](https://chromewebstore.google.com/detail/vuejs-devtools/nhdogjmejiglipccpnnnanhbledajbpd)
- [Turn on Custom Object Formatter in Chrome DevTools](http://bit.ly/object-formatters)
- Firefox:
- [Vue.js devtools](https://addons.mozilla.org/en-US/firefox/addon/vue-js-devtools/)
- [Turn on Custom Object Formatter in Firefox DevTools](https://fxdx.dev/firefox-devtools-custom-object-formatters/)
## Type Support for `.vue` Imports in TS
TypeScript cannot handle type information for `.vue` imports by default, so we replace the `tsc` CLI with `vue-tsc` for type checking. In editors, we need [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) to make the TypeScript language service aware of `.vue` types.
## Customize configuration
See [Vite Configuration Reference](https://vite.dev/config/).
## Project Setup
```sh
npm install
```
### Compile and Hot-Reload for Development
```sh
npm run dev
```
### Type-Check, Compile and Minify for Production
```sh
npm run build
```
### Lint with [ESLint](https://eslint.org/)
```sh
npm run lint
```

View File

@@ -0,0 +1,101 @@
# Using Android gitignore template: https://github.com/github/gitignore/blob/HEAD/Android.gitignore
# Built application files
*.apk
*.aar
*.ap_
*.aab
# Files for the ART/Dalvik VM
*.dex
# Java class files
*.class
# Generated files
bin/
gen/
out/
# Uncomment the following line in case you need and you don't have the release build type files in your app
# release/
# Gradle files
.gradle/
build/
# Local configuration file (sdk path, etc)
local.properties
# Proguard folder generated by Eclipse
proguard/
# Log Files
*.log
# Android Studio Navigation editor temp files
.navigation/
# Android Studio captures folder
captures/
# IntelliJ
*.iml
.idea/workspace.xml
.idea/tasks.xml
.idea/gradle.xml
.idea/assetWizardSettings.xml
.idea/dictionaries
.idea/libraries
# Android Studio 3 in .gitignore file.
.idea/caches
.idea/modules.xml
# Comment next line if keeping position of elements in Navigation Editor is relevant for you
.idea/navEditor.xml
# Keystore files
# Uncomment the following lines if you do not want to check your keystore files in.
#*.jks
#*.keystore
# External native build folder generated in Android Studio 2.2 and later
.externalNativeBuild
.cxx/
# Google Services (e.g. APIs or Firebase)
# google-services.json
# Freeline
freeline.py
freeline/
freeline_project_description.json
# fastlane
fastlane/report.xml
fastlane/Preview.html
fastlane/screenshots
fastlane/test_output
fastlane/readme.md
# Version control
vcs.xml
# lint
lint/intermediates/
lint/generated/
lint/outputs/
lint/tmp/
# lint/reports/
# Android Profiling
*.hprof
# Cordova plugins for Capacitor
capacitor-cordova-android-plugins
# Copied web assets
app/src/main/assets/public
# Generated Config files
app/src/main/assets/capacitor.config.json
app/src/main/assets/capacitor.plugins.json
app/src/main/res/xml/config.xml

View File

@@ -0,0 +1,2 @@
/build/*
!/build/.npmkeep

View File

@@ -0,0 +1,54 @@
apply plugin: 'com.android.application'
android {
namespace "com.timesafari.dailynotification.test"
compileSdkVersion rootProject.ext.compileSdkVersion
defaultConfig {
applicationId "com.timesafari.dailynotification.test"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
aaptOptions {
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
// Default: https://android.googlesource.com/platform/frameworks/base/+/282e181b58cf72b6ca770dc7ca5f91f135444502/tools/aapt/AaptAssets.cpp#61
ignoreAssetsPattern '!.svn:!.git:!.ds_store:!*.scc:.*:!CVS:!thumbs.db:!picasa.ini:!*~'
}
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}
repositories {
flatDir{
dirs '../capacitor-cordova-android-plugins/src/main/libs', 'libs'
}
}
dependencies {
implementation fileTree(include: ['*.jar'], dir: 'libs')
implementation "androidx.appcompat:appcompat:$androidxAppCompatVersion"
implementation "androidx.coordinatorlayout:coordinatorlayout:$androidxCoordinatorLayoutVersion"
implementation "androidx.core:core-splashscreen:$coreSplashScreenVersion"
implementation project(':capacitor-android')
testImplementation "junit:junit:$junitVersion"
androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion"
androidTestImplementation "androidx.test.espresso:espresso-core:$androidxEspressoCoreVersion"
implementation project(':capacitor-cordova-android-plugins')
}
apply from: 'capacitor.build.gradle'
try {
def servicesJSON = file('google-services.json')
if (servicesJSON.text) {
apply plugin: 'com.google.gms.google-services'
}
} catch(Exception e) {
logger.info("google-services.json not found, google-services plugin not applied. Push Notifications won't work")
}

View File

@@ -0,0 +1,19 @@
// DO NOT EDIT THIS FILE! IT IS GENERATED EACH TIME "capacitor update" IS RUN
android {
compileOptions {
sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_17
}
}
apply from: "../capacitor-cordova-android-plugins/cordova.variables.gradle"
dependencies {
}
if (hasProperty('postBuildExtras')) {
postBuildExtras()
}

View File

@@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile

View File

@@ -0,0 +1,26 @@
package com.getcapacitor.myapp;
import static org.junit.Assert.*;
import android.content.Context;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.platform.app.InstrumentationRegistry;
import org.junit.Test;
import org.junit.runner.RunWith;
/**
* Instrumented test, which will execute on an Android device.
*
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
*/
@RunWith(AndroidJUnit4.class)
public class ExampleInstrumentedTest {
@Test
public void useAppContext() throws Exception {
// Context of the app under test.
Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
assertEquals("com.getcapacitor.app", appContext.getPackageName());
}
}

View File

@@ -0,0 +1,73 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|smallestScreenSize|screenLayout|uiMode|navigation"
android:name=".MainActivity"
android:label="@string/title_activity_main"
android:theme="@style/AppTheme.NoActionBarLaunch"
android:launchMode="singleTask"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<!-- DailyNotification Plugin Components -->
<receiver
android:name="com.timesafari.dailynotification.DailyNotificationReceiver"
android:enabled="true"
android:exported="false">
<intent-filter>
<action android:name="com.timesafari.daily.NOTIFICATION" />
</intent-filter>
</receiver>
<receiver
android:name="com.timesafari.dailynotification.BootReceiver"
android:enabled="true"
android:exported="true"
android:directBootAware="true">
<intent-filter android:priority="1000">
<action android:name="android.intent.action.LOCKED_BOOT_COMPLETED" />
<action android:name="android.intent.action.BOOT_COMPLETED" />
<action android:name="android.intent.action.MY_PACKAGE_REPLACED" />
<action android:name="android.intent.action.PACKAGE_REPLACED" />
<data android:scheme="package" />
</intent-filter>
</receiver>
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths"></meta-data>
</provider>
</application>
<!-- Permissions -->
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
<uses-permission android:name="android.permission.USE_EXACT_ALARM" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
</manifest>

View File

@@ -0,0 +1,5 @@
package com.timesafari.dailynotification.test;
import com.getcapacitor.BridgeActivity;
public class MainActivity extends BridgeActivity {}

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View File

@@ -0,0 +1,34 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportHeight="108"
android:viewportWidth="108">
<path
android:fillType="evenOdd"
android:pathData="M32,64C32,64 38.39,52.99 44.13,50.95C51.37,48.37 70.14,49.57 70.14,49.57L108.26,87.69L108,109.01L75.97,107.97L32,64Z"
android:strokeColor="#00000000"
android:strokeWidth="1">
<aapt:attr name="android:fillColor">
<gradient
android:endX="78.5885"
android:endY="90.9159"
android:startX="48.7653"
android:startY="61.0927"
android:type="linear">
<item
android:color="#44000000"
android:offset="0.0" />
<item
android:color="#00000000"
android:offset="1.0" />
</gradient>
</aapt:attr>
</path>
<path
android:fillColor="#FFFFFF"
android:fillType="nonZero"
android:pathData="M66.94,46.02L66.94,46.02C72.44,50.07 76,56.61 76,64L32,64C32,56.61 35.56,50.11 40.98,46.06L36.18,41.19C35.45,40.45 35.45,39.3 36.18,38.56C36.91,37.81 38.05,37.81 38.78,38.56L44.25,44.05C47.18,42.57 50.48,41.71 54,41.71C57.48,41.71 60.78,42.57 63.68,44.05L69.11,38.56C69.84,37.81 70.98,37.81 71.71,38.56C72.44,39.3 72.44,40.45 71.71,41.19L66.94,46.02ZM62.94,56.92C64.08,56.92 65,56.01 65,54.88C65,53.76 64.08,52.85 62.94,52.85C61.8,52.85 60.88,53.76 60.88,54.88C60.88,56.01 61.8,56.92 62.94,56.92ZM45.06,56.92C46.2,56.92 47.13,56.01 47.13,54.88C47.13,53.76 46.2,52.85 45.06,52.85C43.92,52.85 43,53.76 43,54.88C43,56.01 43.92,56.92 45.06,56.92Z"
android:strokeColor="#00000000"
android:strokeWidth="1" />
</vector>

View File

@@ -0,0 +1,170 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportHeight="108"
android:viewportWidth="108">
<path
android:fillColor="#26A69A"
android:pathData="M0,0h108v108h-108z" />
<path
android:fillColor="#00000000"
android:pathData="M9,0L9,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,0L19,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M29,0L29,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M39,0L39,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M49,0L49,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M59,0L59,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M69,0L69,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M79,0L79,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M89,0L89,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M99,0L99,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,9L108,9"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,19L108,19"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,29L108,29"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,39L108,39"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,49L108,49"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,59L108,59"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,69L108,69"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,79L108,79"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,89L108,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,99L108,99"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,29L89,29"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,39L89,39"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,49L89,49"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,59L89,59"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,69L89,69"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,79L89,79"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M29,19L29,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M39,19L39,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M49,19L49,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M59,19L59,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M69,19L69,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M79,19L79,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
</vector>

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<WebView
android:layout_width="match_parent"
android:layout_height="match_parent" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
</adaptive-icon>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
</adaptive-icon>

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">#FFFFFF</color>
</resources>

View File

@@ -0,0 +1,7 @@
<?xml version='1.0' encoding='utf-8'?>
<resources>
<string name="app_name">Daily Notification Test</string>
<string name="title_activity_main">Daily Notification Test</string>
<string name="package_name">com.timesafari.dailynotification.test</string>
<string name="custom_url_scheme">com.timesafari.dailynotification.test</string>
</resources>

View File

@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Base application theme. -->
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
<!-- Customize your theme here. -->
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>
</style>
<style name="AppTheme.NoActionBar" parent="Theme.AppCompat.DayNight.NoActionBar">
<item name="windowActionBar">false</item>
<item name="windowNoTitle">true</item>
<item name="android:background">@null</item>
</style>
<style name="AppTheme.NoActionBarLaunch" parent="Theme.SplashScreen">
<item name="android:background">@drawable/splash</item>
</style>
</resources>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<external-path name="my_images" path="." />
<cache-path name="my_cache_images" path="." />
</paths>

View File

@@ -0,0 +1,18 @@
package com.getcapacitor.myapp;
import static org.junit.Assert.*;
import org.junit.Test;
/**
* Example local unit test, which will execute on the development machine (host).
*
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
*/
public class ExampleUnitTest {
@Test
public void addition_isCorrect() throws Exception {
assertEquals(4, 2 + 2);
}
}

View File

@@ -0,0 +1,29 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
repositories {
google()
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:8.7.2'
classpath 'com.google.gms:google-services:4.4.2'
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
}
apply from: "variables.gradle"
allprojects {
repositories {
google()
mavenCentral()
}
}
task clean(type: Delete) {
delete rootProject.buildDir
}

View File

@@ -0,0 +1,3 @@
// DO NOT EDIT THIS FILE! IT IS GENERATED EACH TIME "capacitor update" IS RUN
include ':capacitor-android'
project(':capacitor-android').projectDir = new File('../node_modules/@capacitor/android/capacitor')

View File

@@ -0,0 +1,22 @@
# Project-wide Gradle settings.
# IDE (e.g. Android Studio) users:
# Gradle settings configured through the IDE *will override*
# any settings specified in this file.
# For more details on how to configure your build environment visit
# http://www.gradle.org/docs/current/userguide/build_environment.html
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
org.gradle.jvmargs=-Xmx1536m
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
# org.gradle.parallel=true
# AndroidX package structure to make it clearer which packages are bundled with the
# Android operating system, and which are packaged with your app's APK
# https://developer.android.com/topic/libraries/support-library/androidx-rn
android.useAndroidX=true

View File

@@ -0,0 +1,7 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-all.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

View File

@@ -0,0 +1,252 @@
#!/bin/sh
#
# Copyright © 2015-2021 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# SPDX-License-Identifier: Apache-2.0
#
##############################################################################
#
# Gradle start up script for POSIX generated by Gradle.
#
# Important for running:
#
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
# noncompliant, but you have some other compliant shell such as ksh or
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh Gradle
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
# * functions;
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
# * compound commands having a testable exit status, especially «case»;
# * various built-in commands including «command», «set», and «ulimit».
#
# Important for patching:
#
# (2) This script targets any POSIX shell, so it avoids extensions provided
# by Bash, Ksh, etc; in particular arrays are avoided.
#
# The "traditional" practice of packing multiple parameters into a
# space-separated string is a well documented source of bugs and security
# problems, so this is (mostly) avoided, by progressively accumulating
# options in "$@", and eventually passing that to Java.
#
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
# see the in-line comments for details.
#
# There are tweaks for specific operating systems such as AIX, CygWin,
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
#
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
app_path=$0
# Need this for daisy-chained symlinks.
while
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
[ -h "$app_path" ]
do
ls=$( ls -ld "$app_path" )
link=${ls#*' -> '}
case $link in #(
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
esac
done
# This is normally unused
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s
' "$PWD" ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
warn () {
echo "$*"
} >&2
die () {
echo
echo "$*"
echo
exit 1
} >&2
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "$( uname )" in #(
CYGWIN* ) cygwin=true ;; #(
Darwin* ) darwin=true ;; #(
MSYS* | MINGW* ) msys=true ;; #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD=$JAVA_HOME/jre/sh/java
else
JAVACMD=$JAVA_HOME/bin/java
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD=java
if ! command -v java >/dev/null 2>&1
then
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
fi
# Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
fi
# Collect all arguments for the java command, stacking in reverse order:
# * args from the command line
# * the main class name
# * -classpath
# * -D...appname settings
# * --module-path (only if needed)
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
# Now convert the arguments - kludge to limit ourselves to /bin/sh
for arg do
if
case $arg in #(
-*) false ;; # don't mess with options #(
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
[ -e "$t" ] ;; #(
*) false ;;
esac
then
arg=$( cygpath --path --ignore --mixed "$arg" )
fi
# Roll the args list around exactly as many times as the number of
# args, so each arg winds up back in the position where it started, but
# possibly modified.
#
# NB: a `for` loop captures its iteration list before it begins, so
# changing the positional parameters here affects neither the number of
# iterations, nor the values presented in `arg`.
shift # remove old arg
set -- "$@" "$arg" # push replacement arg
done
fi
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command:
# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# and any embedded shellness will be escaped.
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# treated as '${Hostname}' itself on the command line.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
org.gradle.wrapper.GradleWrapperMain \
"$@"
# Stop when "xargs" is not available.
if ! command -v xargs >/dev/null 2>&1
then
die "xargs is not available"
fi
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
# set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#
eval "set -- $(
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
xargs -n1 |
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
tr '\n' ' '
)" '"$@"'
exec "$JAVACMD" "$@"

View File

@@ -0,0 +1,94 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@rem SPDX-License-Identifier: Apache-2.0
@rem
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=.
@rem This is normally unused
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell
if %ERRORLEVEL% equ 0 goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
set EXIT_CODE=%ERRORLEVEL%
if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega

View File

@@ -0,0 +1,5 @@
include ':app'
include ':capacitor-cordova-android-plugins'
project(':capacitor-cordova-android-plugins').projectDir = new File('./capacitor-cordova-android-plugins/')
apply from: 'capacitor.settings.gradle'

View File

@@ -0,0 +1,16 @@
ext {
minSdkVersion = 23
compileSdkVersion = 35
targetSdkVersion = 35
androidxActivityVersion = '1.9.2'
androidxAppCompatVersion = '1.7.0'
androidxCoordinatorLayoutVersion = '1.2.0'
androidxCoreVersion = '1.15.0'
androidxFragmentVersion = '1.8.4'
coreSplashScreenVersion = '1.0.1'
androidxWebkitVersion = '1.12.1'
junitVersion = '4.13.2'
androidxJunitVersion = '1.2.1'
androidxEspressoCoreVersion = '3.6.1'
cordovaAndroidVersion = '10.1.1'
}

View File

@@ -0,0 +1,14 @@
import type { CapacitorConfig } from '@capacitor/cli';
const config: CapacitorConfig = {
appId: 'com.timesafari.dailynotification.test',
appName: 'Daily Notification Test',
webDir: 'dist',
plugins: {
Clipboard: {
// Enable clipboard functionality
}
}
};
export default config;

View File

@@ -0,0 +1 @@
/// <reference types="vite/client" />

View File

@@ -0,0 +1,20 @@
import { globalIgnores } from 'eslint/config'
import { defineConfigWithVueTs, vueTsConfigs } from '@vue/eslint-config-typescript'
import pluginVue from 'eslint-plugin-vue'
// To allow more languages other than `ts` in `.vue` files, uncomment the following lines:
// import { configureVueProject } from '@vue/eslint-config-typescript'
// configureVueProject({ scriptLangs: ['ts', 'tsx'] })
// More info at https://github.com/vuejs/eslint-config-typescript/#advanced-setup
export default defineConfigWithVueTs(
{
name: 'app/files-to-lint',
files: ['**/*.{ts,mts,tsx,vue}'],
},
globalIgnores(['**/dist/**', '**/dist-ssr/**', '**/coverage/**']),
pluginVue.configs['flat/essential'],
vueTsConfigs.recommended,
)

View File

@@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="">
<head>
<meta charset="UTF-8">
<link rel="icon" href="/favicon.ico">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Vite App</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,42 @@
{
"name": "daily-notification-test",
"version": "0.0.0",
"private": true,
"type": "module",
"engines": {
"node": "^20.19.0 || >=22.12.0"
},
"scripts": {
"dev": "vite",
"build": "run-p type-check \"build-only {@}\" --",
"preview": "vite preview",
"build-only": "vite build",
"type-check": "vue-tsc --build",
"lint": "eslint . --fix"
},
"dependencies": {
"@capacitor/android": "^6.2.1",
"@capacitor/cli": "^6.2.1",
"@capacitor/core": "^6.2.1",
"date-fns": "^4.1.0",
"pinia": "^3.0.3",
"vue": "^3.5.22",
"vue-facing-decorator": "^4.0.1",
"vue-router": "^4.5.1"
},
"devDependencies": {
"@tsconfig/node22": "^22.0.2",
"@types/node": "^22.18.6",
"@vitejs/plugin-vue": "^6.0.1",
"@vue/eslint-config-typescript": "^14.6.0",
"@vue/tsconfig": "^0.8.1",
"eslint": "^9.33.0",
"eslint-plugin-vue": "~10.4.0",
"jiti": "^2.5.1",
"npm-run-all2": "^8.0.4",
"typescript": "~5.9.0",
"vite": "^7.1.7",
"vite-plugin-vue-devtools": "^8.0.2",
"vue-tsc": "^3.1.0"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

@@ -0,0 +1,125 @@
<!--
/**
* Main App Component - Platform Neutral Vue 3 + vue-facing-decorator
*
* Cross-platform DailyNotification test app for Android/iOS/Electron
*
* @author Matthew Raymer
* @version 1.0.0
*/
-->
<template>
<div id="app" class="app-container">
<!-- Header Navigation -->
<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 setup lang="ts">
import { onMounted, computed } from 'vue'
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'
const appStore = useAppStore()
const isLoading = computed(() => appStore.isLoading)
const errorMessage = computed(() => appStore.errorMessage)
const initializeApp = async (): Promise<void> => {
try {
appStore.setLoading(true)
// Initialize platform detection
const platform = Capacitor.getPlatform()
const isNative = Capacitor.isNativePlatform()
appStore.setPlatform(platform, isNative)
console.log('🚀 Daily Notification Test App Started')
console.log('📱 Platform:', platform)
console.log('🔧 Native Platform:', isNative)
// Check if DailyNotification plugin is available
if (isNative && (window as any).DailyNotification) {
console.log('✅ DailyNotification plugin available')
// Initialize plugin status check
await checkPluginStatus()
} else if (isNative) {
console.warn('⚠️ DailyNotification plugin not available')
appStore.setError('DailyNotification plugin not loaded. Please restart the app.')
} else {
console.log('🌐 Running in web mode - plugin not available')
}
} catch (error) {
console.error('❌ App initialization failed:', error)
appStore.setError('Failed to initialize app: ' + (error as Error).message)
} finally {
appStore.setLoading(false)
}
}
const checkPluginStatus = async (): Promise<void> => {
try {
if ((window as any).DailyNotification) {
const status = await (window as any).DailyNotification.checkStatus()
appStore.setNotificationStatus(status)
console.log('📊 Plugin status:', status)
}
} catch (error) {
console.error('❌ Plugin status check failed:', error)
}
}
const clearError = (): void => {
appStore.clearError()
}
onMounted(() => {
initializeApp()
})
</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: 0;
overflow-y: auto;
}
/* Platform-specific adjustments */
@media (max-width: 768px) {
.main-content {
padding: 0;
}
}
</style>

View File

@@ -0,0 +1,86 @@
/* color palette from <https://github.com/vuejs/theme> */
:root {
--vt-c-white: #ffffff;
--vt-c-white-soft: #f8f8f8;
--vt-c-white-mute: #f2f2f2;
--vt-c-black: #181818;
--vt-c-black-soft: #222222;
--vt-c-black-mute: #282828;
--vt-c-indigo: #2c3e50;
--vt-c-divider-light-1: rgba(60, 60, 60, 0.29);
--vt-c-divider-light-2: rgba(60, 60, 60, 0.12);
--vt-c-divider-dark-1: rgba(84, 84, 84, 0.65);
--vt-c-divider-dark-2: rgba(84, 84, 84, 0.48);
--vt-c-text-light-1: var(--vt-c-indigo);
--vt-c-text-light-2: rgba(60, 60, 60, 0.66);
--vt-c-text-dark-1: var(--vt-c-white);
--vt-c-text-dark-2: rgba(235, 235, 235, 0.64);
}
/* semantic color variables for this project */
:root {
--color-background: var(--vt-c-white);
--color-background-soft: var(--vt-c-white-soft);
--color-background-mute: var(--vt-c-white-mute);
--color-border: var(--vt-c-divider-light-2);
--color-border-hover: var(--vt-c-divider-light-1);
--color-heading: var(--vt-c-text-light-1);
--color-text: var(--vt-c-text-light-1);
--section-gap: 160px;
}
@media (prefers-color-scheme: dark) {
:root {
--color-background: var(--vt-c-black);
--color-background-soft: var(--vt-c-black-soft);
--color-background-mute: var(--vt-c-black-mute);
--color-border: var(--vt-c-divider-dark-2);
--color-border-hover: var(--vt-c-divider-dark-1);
--color-heading: var(--vt-c-text-dark-1);
--color-text: var(--vt-c-text-dark-2);
}
}
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
font-weight: normal;
}
body {
min-height: 100vh;
color: var(--color-text);
background: var(--color-background);
transition:
color 0.5s,
background-color 0.5s;
line-height: 1.6;
font-family:
Inter,
-apple-system,
BlinkMacSystemFont,
'Segoe UI',
Roboto,
Oxygen,
Ubuntu,
Cantarell,
'Fira Sans',
'Droid Sans',
'Helvetica Neue',
sans-serif;
font-size: 15px;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 261.76 226.69"><path d="M161.096.001l-30.225 52.351L100.647.001H-.005l130.877 226.688L261.749.001z" fill="#41b883"/><path d="M161.096.001l-30.225 52.351L100.647.001H52.346l78.526 136.01L209.398.001z" fill="#34495e"/></svg>

After

Width:  |  Height:  |  Size: 276 B

View File

@@ -0,0 +1,35 @@
@import './base.css';
#app {
max-width: 1280px;
margin: 0 auto;
padding: 2rem;
font-weight: normal;
}
a,
.green {
text-decoration: none;
color: hsla(160, 100%, 37%, 1);
transition: 0.4s;
padding: 3px;
}
@media (hover: hover) {
a:hover {
background-color: hsla(160, 100%, 37%, 0.2);
}
}
@media (min-width: 1024px) {
body {
display: flex;
place-items: center;
}
#app {
display: grid;
grid-template-columns: 1fr 1fr;
padding: 0 2rem;
}
}

View File

@@ -0,0 +1,41 @@
<script setup lang="ts">
defineProps<{
msg: string
}>()
</script>
<template>
<div class="greetings">
<h1 class="green">{{ msg }}</h1>
<h3>
Youve successfully created a project with
<a href="https://vite.dev/" target="_blank" rel="noopener">Vite</a> +
<a href="https://vuejs.org/" target="_blank" rel="noopener">Vue 3</a>. What's next?
</h3>
</div>
</template>
<style scoped>
h1 {
font-weight: 500;
font-size: 2.6rem;
position: relative;
top: -10px;
}
h3 {
font-size: 1.2rem;
}
.greetings h1,
.greetings h3 {
text-align: center;
}
@media (min-width: 1024px) {
.greetings h1,
.greetings h3 {
text-align: left;
}
}
</style>

View File

@@ -0,0 +1,94 @@
<script setup lang="ts">
import WelcomeItem from './WelcomeItem.vue'
import DocumentationIcon from './icons/IconDocumentation.vue'
import ToolingIcon from './icons/IconTooling.vue'
import EcosystemIcon from './icons/IconEcosystem.vue'
import CommunityIcon from './icons/IconCommunity.vue'
import SupportIcon from './icons/IconSupport.vue'
const openReadmeInEditor = () => fetch('/__open-in-editor?file=README.md')
</script>
<template>
<WelcomeItem>
<template #icon>
<DocumentationIcon />
</template>
<template #heading>Documentation</template>
Vues
<a href="https://vuejs.org/" target="_blank" rel="noopener">official documentation</a>
provides you with all information you need to get started.
</WelcomeItem>
<WelcomeItem>
<template #icon>
<ToolingIcon />
</template>
<template #heading>Tooling</template>
This project is served and bundled with
<a href="https://vite.dev/guide/features.html" target="_blank" rel="noopener">Vite</a>. The
recommended IDE setup is
<a href="https://code.visualstudio.com/" target="_blank" rel="noopener">VSCode</a>
+
<a href="https://github.com/vuejs/language-tools" target="_blank" rel="noopener">Vue - Official</a>. If
you need to test your components and web pages, check out
<a href="https://vitest.dev/" target="_blank" rel="noopener">Vitest</a>
and
<a href="https://www.cypress.io/" target="_blank" rel="noopener">Cypress</a>
/
<a href="https://playwright.dev/" target="_blank" rel="noopener">Playwright</a>.
<br />
More instructions are available in
<a href="javascript:void(0)" @click="openReadmeInEditor"><code>README.md</code></a
>.
</WelcomeItem>
<WelcomeItem>
<template #icon>
<EcosystemIcon />
</template>
<template #heading>Ecosystem</template>
Get official tools and libraries for your project:
<a href="https://pinia.vuejs.org/" target="_blank" rel="noopener">Pinia</a>,
<a href="https://router.vuejs.org/" target="_blank" rel="noopener">Vue Router</a>,
<a href="https://test-utils.vuejs.org/" target="_blank" rel="noopener">Vue Test Utils</a>, and
<a href="https://github.com/vuejs/devtools" target="_blank" rel="noopener">Vue Dev Tools</a>. If
you need more resources, we suggest paying
<a href="https://github.com/vuejs/awesome-vue" target="_blank" rel="noopener">Awesome Vue</a>
a visit.
</WelcomeItem>
<WelcomeItem>
<template #icon>
<CommunityIcon />
</template>
<template #heading>Community</template>
Got stuck? Ask your question on
<a href="https://chat.vuejs.org" target="_blank" rel="noopener">Vue Land</a>
(our official Discord server), or
<a href="https://stackoverflow.com/questions/tagged/vue.js" target="_blank" rel="noopener"
>StackOverflow</a
>. You should also follow the official
<a href="https://bsky.app/profile/vuejs.org" target="_blank" rel="noopener">@vuejs.org</a>
Bluesky account or the
<a href="https://x.com/vuejs" target="_blank" rel="noopener">@vuejs</a>
X account for latest news in the Vue world.
</WelcomeItem>
<WelcomeItem>
<template #icon>
<SupportIcon />
</template>
<template #heading>Support Vue</template>
As an independent project, Vue relies on community backing for its sustainability. You can help
us by
<a href="https://vuejs.org/sponsor/" target="_blank" rel="noopener">becoming a sponsor</a>.
</WelcomeItem>
</template>

View File

@@ -0,0 +1,87 @@
<template>
<div class="item">
<i>
<slot name="icon"></slot>
</i>
<div class="details">
<h3>
<slot name="heading"></slot>
</h3>
<slot></slot>
</div>
</div>
</template>
<style scoped>
.item {
margin-top: 2rem;
display: flex;
position: relative;
}
.details {
flex: 1;
margin-left: 1rem;
}
i {
display: flex;
place-items: center;
place-content: center;
width: 32px;
height: 32px;
color: var(--color-text);
}
h3 {
font-size: 1.2rem;
font-weight: 500;
margin-bottom: 0.4rem;
color: var(--color-heading);
}
@media (min-width: 1024px) {
.item {
margin-top: 0;
padding: 0.4rem 0 1rem calc(var(--section-gap) / 2);
}
i {
top: calc(50% - 25px);
left: -26px;
position: absolute;
border: 1px solid var(--color-border);
background: var(--color-background);
border-radius: 8px;
width: 50px;
height: 50px;
}
.item:before {
content: ' ';
border-left: 1px solid var(--color-border);
position: absolute;
left: 0;
bottom: calc(50% + 25px);
height: calc(50% - 25px);
}
.item:after {
content: ' ';
border-left: 1px solid var(--color-border);
position: absolute;
left: 0;
top: calc(50% + 25px);
height: calc(50% - 25px);
}
.item:first-of-type:before {
display: none;
}
.item:last-of-type:after {
display: none;
}
}
</style>

View File

@@ -0,0 +1,141 @@
<!--
/**
* Action Card Component - Platform Neutral Action Card
*
* Reusable card component for displaying actions
*
* @author Matthew Raymer
* @version 1.0.0
*/
-->
<template>
<div class="action-card" @click="handleClick" :class="{ loading: loading }">
<div class="card-content">
<div class="card-icon">{{ icon }}</div>
<div class="card-text">
<h3 class="card-title">{{ title }}</h3>
<p class="card-description">{{ description }}</p>
</div>
<div class="card-arrow">
<span v-if="loading" class="loading-spinner"></span>
<span v-else class="arrow-icon"></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
handleClick(): void {
if (!this.loading) {
this.$emit('click')
}
}
}
</script>
<style scoped>
.action-card {
background: rgba(255, 255, 255, 0.1);
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 12px;
padding: 20px;
cursor: pointer;
transition: all 0.2s ease;
backdrop-filter: blur(10px);
}
.action-card:hover {
background: rgba(255, 255, 255, 0.15);
border-color: rgba(255, 255, 255, 0.3);
transform: translateY(-2px);
}
.action-card.loading {
cursor: not-allowed;
opacity: 0.7;
}
.card-content {
display: flex;
align-items: center;
gap: 16px;
}
.card-icon {
font-size: 24px;
flex-shrink: 0;
}
.card-text {
flex: 1;
}
.card-title {
margin: 0 0 4px 0;
font-size: 16px;
font-weight: 600;
color: white;
}
.card-description {
margin: 0;
font-size: 14px;
color: rgba(255, 255, 255, 0.8);
line-height: 1.4;
}
.card-arrow {
flex-shrink: 0;
color: rgba(255, 255, 255, 0.6);
font-size: 18px;
}
.loading-spinner {
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/* Mobile responsiveness */
@media (max-width: 768px) {
.action-card {
padding: 16px;
}
.card-content {
gap: 12px;
}
.card-icon {
font-size: 20px;
}
.card-title {
font-size: 15px;
}
.card-description {
font-size: 13px;
}
}
</style>

View File

@@ -0,0 +1,202 @@
<!--
/**
* Status Card Component - Platform Neutral Status Display
*
* Reusable card component for displaying system status
*
* @author Matthew Raymer
* @version 1.0.0
*/
-->
<template>
<div class="status-card">
<div class="card-header">
<h3 class="card-title">System Status</h3>
<button class="refresh-button" @click="refreshStatus" :disabled="isRefreshing">
<span v-if="isRefreshing" class="loading-spinner"></span>
<span v-else class="refresh-icon">🔄</span>
</button>
</div>
<div class="status-items">
<div
v-for="item in statusItems"
:key="item.label"
class="status-item"
:class="`status-${item.status}`"
>
<div class="status-label">{{ item.label }}</div>
<div class="status-value">
<span class="status-indicator" :class="`indicator-${item.status}`"></span>
{{ item.value }}
</div>
</div>
</div>
</div>
</template>
<script lang="ts">
import { Component, Vue, Prop } from 'vue-facing-decorator'
interface StatusItem {
label: string
value: string
status: 'success' | 'error' | 'warning' | 'info'
}
@Component
export default class StatusCard extends Vue {
@Prop({ required: true })
statusItems!: StatusItem[]
@Prop({ default: false })
isRefreshing!: boolean
refreshStatus(): void {
this.$emit('refresh')
}
}
</script>
<style scoped>
.status-card {
background: rgba(255, 255, 255, 0.1);
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 12px;
padding: 20px;
backdrop-filter: blur(10px);
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
}
.card-title {
margin: 0;
font-size: 18px;
font-weight: 600;
color: white;
}
.refresh-button {
background: rgba(255, 255, 255, 0.1);
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 6px;
padding: 8px;
cursor: pointer;
transition: all 0.2s ease;
color: white;
}
.refresh-button:hover:not(:disabled) {
background: rgba(255, 255, 255, 0.2);
}
.refresh-button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.refresh-icon, .loading-spinner {
font-size: 16px;
}
.loading-spinner {
animation: spin 1s linear infinite;
}
.status-items {
display: flex;
flex-direction: column;
gap: 12px;
}
.status-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px;
background: rgba(255, 255, 255, 0.05);
border-radius: 8px;
border-left: 4px solid transparent;
}
.status-item.status-success {
border-left-color: #4caf50;
}
.status-item.status-error {
border-left-color: #f44336;
}
.status-item.status-warning {
border-left-color: #ff9800;
}
.status-item.status-info {
border-left-color: #2196f3;
}
.status-label {
font-size: 14px;
font-weight: 500;
color: rgba(255, 255, 255, 0.9);
}
.status-value {
display: flex;
align-items: center;
gap: 8px;
font-size: 14px;
font-weight: 600;
color: white;
}
.status-indicator {
width: 8px;
height: 8px;
border-radius: 50%;
}
.indicator-success {
background: #4caf50;
}
.indicator-error {
background: #f44336;
}
.indicator-warning {
background: #ff9800;
}
.indicator-info {
background: #2196f3;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/* Mobile responsiveness */
@media (max-width: 768px) {
.status-card {
padding: 16px;
}
.status-item {
flex-direction: column;
align-items: flex-start;
gap: 8px;
}
.status-value {
align-self: flex-end;
}
}
</style>

View File

@@ -0,0 +1,7 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor">
<path
d="M15 4a1 1 0 1 0 0 2V4zm0 11v-1a1 1 0 0 0-1 1h1zm0 4l-.707.707A1 1 0 0 0 16 19h-1zm-4-4l.707-.707A1 1 0 0 0 11 14v1zm-4.707-1.293a1 1 0 0 0-1.414 1.414l1.414-1.414zm-.707.707l-.707-.707.707.707zM9 11v-1a1 1 0 0 0-.707.293L9 11zm-4 0h1a1 1 0 0 0-1-1v1zm0 4H4a1 1 0 0 0 1.707.707L5 15zm10-9h2V4h-2v2zm2 0a1 1 0 0 1 1 1h2a3 3 0 0 0-3-3v2zm1 1v6h2V7h-2zm0 6a1 1 0 0 1-1 1v2a3 3 0 0 0 3-3h-2zm-1 1h-2v2h2v-2zm-3 1v4h2v-4h-2zm1.707 3.293l-4-4-1.414 1.414 4 4 1.414-1.414zM11 14H7v2h4v-2zm-4 0c-.276 0-.525-.111-.707-.293l-1.414 1.414C5.42 15.663 6.172 16 7 16v-2zm-.707 1.121l3.414-3.414-1.414-1.414-3.414 3.414 1.414 1.414zM9 12h4v-2H9v2zm4 0a3 3 0 0 0 3-3h-2a1 1 0 0 1-1 1v2zm3-3V3h-2v6h2zm0-6a3 3 0 0 0-3-3v2a1 1 0 0 1 1 1h2zm-3-3H3v2h10V0zM3 0a3 3 0 0 0-3 3h2a1 1 0 0 1 1-1V0zM0 3v6h2V3H0zm0 6a3 3 0 0 0 3 3v-2a1 1 0 0 1-1-1H0zm3 3h2v-2H3v2zm1-1v4h2v-4H4zm1.707 4.707l.586-.586-1.414-1.414-.586.586 1.414 1.414z"
/>
</svg>
</template>

View File

@@ -0,0 +1,7 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="17" fill="currentColor">
<path
d="M11 2.253a1 1 0 1 0-2 0h2zm-2 13a1 1 0 1 0 2 0H9zm.447-12.167a1 1 0 1 0 1.107-1.666L9.447 3.086zM1 2.253L.447 1.42A1 1 0 0 0 0 2.253h1zm0 13H0a1 1 0 0 0 1.553.833L1 15.253zm8.447.833a1 1 0 1 0 1.107-1.666l-1.107 1.666zm0-14.666a1 1 0 1 0 1.107 1.666L9.447 1.42zM19 2.253h1a1 1 0 0 0-.447-.833L19 2.253zm0 13l-.553.833A1 1 0 0 0 20 15.253h-1zm-9.553-.833a1 1 0 1 0 1.107 1.666L9.447 14.42zM9 2.253v13h2v-13H9zm1.553-.833C9.203.523 7.42 0 5.5 0v2c1.572 0 2.961.431 3.947 1.086l1.107-1.666zM5.5 0C3.58 0 1.797.523.447 1.42l1.107 1.666C2.539 2.431 3.928 2 5.5 2V0zM0 2.253v13h2v-13H0zm1.553 13.833C2.539 15.431 3.928 15 5.5 15v-2c-1.92 0-3.703.523-5.053 1.42l1.107 1.666zM5.5 15c1.572 0 2.961.431 3.947 1.086l1.107-1.666C9.203 13.523 7.42 13 5.5 13v2zm5.053-11.914C11.539 2.431 12.928 2 14.5 2V0c-1.92 0-3.703.523-5.053 1.42l1.107 1.666zM14.5 2c1.573 0 2.961.431 3.947 1.086l1.107-1.666C18.203.523 16.421 0 14.5 0v2zm3.5.253v13h2v-13h-2zm1.553 12.167C18.203 13.523 16.421 13 14.5 13v2c1.573 0 2.961.431 3.947 1.086l1.107-1.666zM14.5 13c-1.92 0-3.703.523-5.053 1.42l1.107 1.666C11.539 15.431 12.928 15 14.5 15v-2z"
/>
</svg>
</template>

View File

@@ -0,0 +1,7 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="20" fill="currentColor">
<path
d="M11.447 8.894a1 1 0 1 0-.894-1.789l.894 1.789zm-2.894-.789a1 1 0 1 0 .894 1.789l-.894-1.789zm0 1.789a1 1 0 1 0 .894-1.789l-.894 1.789zM7.447 7.106a1 1 0 1 0-.894 1.789l.894-1.789zM10 9a1 1 0 1 0-2 0h2zm-2 2.5a1 1 0 1 0 2 0H8zm9.447-5.606a1 1 0 1 0-.894-1.789l.894 1.789zm-2.894-.789a1 1 0 1 0 .894 1.789l-.894-1.789zm2 .789a1 1 0 1 0 .894-1.789l-.894 1.789zm-1.106-2.789a1 1 0 1 0-.894 1.789l.894-1.789zM18 5a1 1 0 1 0-2 0h2zm-2 2.5a1 1 0 1 0 2 0h-2zm-5.447-4.606a1 1 0 1 0 .894-1.789l-.894 1.789zM9 1l.447-.894a1 1 0 0 0-.894 0L9 1zm-2.447.106a1 1 0 1 0 .894 1.789l-.894-1.789zm-6 3a1 1 0 1 0 .894 1.789L.553 4.106zm2.894.789a1 1 0 1 0-.894-1.789l.894 1.789zm-2-.789a1 1 0 1 0-.894 1.789l.894-1.789zm1.106 2.789a1 1 0 1 0 .894-1.789l-.894 1.789zM2 5a1 1 0 1 0-2 0h2zM0 7.5a1 1 0 1 0 2 0H0zm8.553 12.394a1 1 0 1 0 .894-1.789l-.894 1.789zm-1.106-2.789a1 1 0 1 0-.894 1.789l.894-1.789zm1.106 1a1 1 0 1 0 .894 1.789l-.894-1.789zm2.894.789a1 1 0 1 0-.894-1.789l.894 1.789zM8 19a1 1 0 1 0 2 0H8zm2-2.5a1 1 0 1 0-2 0h2zm-7.447.394a1 1 0 1 0 .894-1.789l-.894 1.789zM1 15H0a1 1 0 0 0 .553.894L1 15zm1-2.5a1 1 0 1 0-2 0h2zm12.553 2.606a1 1 0 1 0 .894 1.789l-.894-1.789zM17 15l.447.894A1 1 0 0 0 18 15h-1zm1-2.5a1 1 0 1 0-2 0h2zm-7.447-5.394l-2 1 .894 1.789 2-1-.894-1.789zm-1.106 1l-2-1-.894 1.789 2 1 .894-1.789zM8 9v2.5h2V9H8zm8.553-4.894l-2 1 .894 1.789 2-1-.894-1.789zm.894 0l-2-1-.894 1.789 2 1 .894-1.789zM16 5v2.5h2V5h-2zm-4.553-3.894l-2-1-.894 1.789 2 1 .894-1.789zm-2.894-1l-2 1 .894 1.789 2-1L8.553.106zM1.447 5.894l2-1-.894-1.789-2 1 .894 1.789zm-.894 0l2 1 .894-1.789-2-1-.894 1.789zM0 5v2.5h2V5H0zm9.447 13.106l-2-1-.894 1.789 2 1 .894-1.789zm0 1.789l2-1-.894-1.789-2 1 .894 1.789zM10 19v-2.5H8V19h2zm-6.553-3.894l-2-1-.894 1.789 2 1 .894-1.789zM2 15v-2.5H0V15h2zm13.447 1.894l2-1-.894-1.789-2 1 .894 1.789zM18 15v-2.5h-2V15h2z"
/>
</svg>
</template>

View File

@@ -0,0 +1,7 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor">
<path
d="M10 3.22l-.61-.6a5.5 5.5 0 0 0-7.666.105 5.5 5.5 0 0 0-.114 7.665L10 18.78l8.39-8.4a5.5 5.5 0 0 0-.114-7.665 5.5 5.5 0 0 0-7.666-.105l-.61.61z"
/>
</svg>
</template>

View File

@@ -0,0 +1,19 @@
<!-- This icon is from <https://github.com/Templarian/MaterialDesign>, distributed under Apache 2.0 (https://www.apache.org/licenses/LICENSE-2.0) license-->
<template>
<svg
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
aria-hidden="true"
role="img"
class="iconify iconify--mdi"
width="24"
height="24"
preserveAspectRatio="xMidYMid meet"
viewBox="0 0 24 24"
>
<path
d="M20 18v-4h-3v1h-2v-1H9v1H7v-1H4v4h16M6.33 8l-1.74 4H7v-1h2v1h6v-1h2v1h2.41l-1.74-4H6.33M9 5v1h6V5H9m12.84 7.61c.1.22.16.48.16.8V18c0 .53-.21 1-.6 1.41c-.4.4-.85.59-1.4.59H4c-.55 0-1-.19-1.4-.59C2.21 19 2 18.53 2 18v-4.59c0-.32.06-.58.16-.8L4.5 7.22C4.84 6.41 5.45 6 6.33 6H7V5c0-.55.18-1 .57-1.41C7.96 3.2 8.44 3 9 3h6c.56 0 1.04.2 1.43.59c.39.41.57.86.57 1.41v1h.67c.88 0 1.49.41 1.83 1.22l2.34 5.39z"
fill="currentColor"
></path>
</svg>
</template>

View File

@@ -0,0 +1,113 @@
<!--
/**
* App Footer Component - Platform Neutral Footer
*
* Cross-platform footer with version and platform info
*
* @author Matthew Raymer
* @version 1.0.0
*/
-->
<template>
<footer class="app-footer">
<div class="footer-content">
<div class="footer-info">
<span class="app-version">v1.0.0</span>
<span class="platform-info">{{ platformInfo }}</span>
</div>
<div class="footer-links">
<a href="#" class="footer-link" @click="showAbout">About</a>
<a href="#" class="footer-link" @click="showHelp">Help</a>
</div>
</div>
</footer>
</template>
<script lang="ts">
import { Component, Vue } from 'vue-facing-decorator'
import { useAppStore } from '@/stores/app'
@Component
export default class AppFooter extends Vue {
private appStore = useAppStore()
get platformInfo(): string {
const platform = this.appStore.platform
const isNative = this.appStore.isNative
return `${platform.charAt(0).toUpperCase() + platform.slice(1)} ${isNative ? '(Native)' : '(Web)'}`
}
showAbout(): void {
// TODO: Show about dialog
console.log('About clicked')
}
showHelp(): void {
// TODO: Show help dialog
console.log('Help clicked')
}
}
</script>
<style scoped>
.app-footer {
background: rgba(0, 0, 0, 0.1);
border-top: 1px solid rgba(255, 255, 255, 0.1);
padding: 12px 20px;
margin-top: auto;
}
.footer-content {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 12px;
color: rgba(255, 255, 255, 0.7);
}
.footer-info {
display: flex;
gap: 12px;
align-items: center;
}
.app-version {
font-weight: 500;
}
.platform-info {
padding: 2px 6px;
background: rgba(255, 255, 255, 0.1);
border-radius: 4px;
font-size: 11px;
}
.footer-links {
display: flex;
gap: 16px;
}
.footer-link {
color: rgba(255, 255, 255, 0.7);
text-decoration: none;
transition: color 0.2s ease;
}
.footer-link:hover {
color: rgba(255, 255, 255, 0.9);
}
/* Mobile responsiveness */
@media (max-width: 768px) {
.footer-content {
flex-direction: column;
gap: 8px;
text-align: center;
}
.footer-info {
justify-content: center;
}
}
</style>

View File

@@ -0,0 +1,270 @@
<!--
/**
* App Header Component - Platform Neutral Navigation
*
* Cross-platform header with navigation tabs
*
* @author Matthew Raymer
* @version 1.0.0
*/
-->
<template>
<header class="app-header">
<div class="header-content">
<!-- App Title -->
<div class="app-title">
<h1 class="title-text">
🔔 Daily Notification Test
</h1>
<div class="platform-badge" :class="platformClass">
{{ platformName }}
</div>
</div>
<!-- Status Indicator -->
<div class="status-indicator" :class="statusClass">
<div class="status-dot"></div>
<span class="status-text">{{ statusText }}</span>
</div>
</div>
<!-- Navigation Tabs -->
<nav class="navigation-tabs">
<router-link
v-for="item in navigationItems"
:key="item.name"
:to="item.path"
class="nav-tab"
:class="{ active: $route.name === item.name }"
>
<span class="nav-icon">{{ item.icon }}</span>
<span class="nav-label">{{ item.label }}</span>
</router-link>
</nav>
</header>
</template>
<script lang="ts">
import { Component, Vue } from 'vue-facing-decorator'
import { useAppStore } from '@/stores/app'
interface NavigationItem {
name: string
path: string
label: string
icon: string
}
@Component
export default class AppHeader extends Vue {
private appStore = useAppStore()
navigationItems: NavigationItem[] = [
{ name: 'Home', path: '/', label: 'Home', icon: '🏠' },
{ name: 'Schedule', path: '/schedule', label: 'Schedule', icon: '📅' },
{ name: 'Notifications', path: '/notifications', label: 'Notifications', icon: '🔔' },
{ name: 'Status', path: '/status', label: 'Status', icon: '📊' }
]
get platformName(): string {
const platform = this.appStore.platform
return platform.charAt(0).toUpperCase() + platform.slice(1)
}
get platformClass(): string {
return `platform-${this.appStore.platform}`
}
get statusClass(): string {
const status = this.appStore.notificationStatus
if (!status) return 'unknown'
if (status.canScheduleNow) return 'ready'
return 'not-ready'
}
get statusText(): 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;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
}
.app-title {
display: flex;
align-items: center;
gap: 12px;
}
.title-text {
margin: 0;
font-size: 20px;
font-weight: 600;
color: white;
}
.platform-badge {
padding: 4px 8px;
border-radius: 12px;
font-size: 12px;
font-weight: 500;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.platform-android {
background: rgba(76, 175, 80, 0.2);
color: #4caf50;
border: 1px solid rgba(76, 175, 80, 0.3);
}
.platform-ios {
background: rgba(0, 122, 255, 0.2);
color: #007aff;
border: 1px solid rgba(0, 122, 255, 0.3);
}
.platform-electron {
background: rgba(138, 43, 226, 0.2);
color: #8a2be2;
border: 1px solid rgba(138, 43, 226, 0.3);
}
.platform-web {
background: rgba(255, 152, 0, 0.2);
color: #ff9800;
border: 1px solid rgba(255, 152, 0, 0.3);
}
.status-indicator {
display: flex;
align-items: center;
gap: 8px;
padding: 6px 12px;
border-radius: 16px;
font-size: 14px;
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;
}
.navigation-tabs {
display: flex;
gap: 4px;
overflow-x: auto;
padding-bottom: 4px;
}
.nav-tab {
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
padding: 8px 12px;
border-radius: 8px;
text-decoration: none;
color: rgba(255, 255, 255, 0.7);
transition: all 0.2s ease;
min-width: 60px;
white-space: nowrap;
}
.nav-tab:hover {
background: rgba(255, 255, 255, 0.1);
color: rgba(255, 255, 255, 0.9);
}
.nav-tab.active {
background: rgba(255, 255, 255, 0.2);
color: white;
}
.nav-icon {
font-size: 16px;
}
.nav-label {
font-size: 11px;
font-weight: 500;
}
/* Mobile responsiveness */
@media (max-width: 768px) {
.header-content {
flex-direction: column;
gap: 12px;
align-items: flex-start;
}
.navigation-tabs {
width: 100%;
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 2px;
}
.nav-tab {
min-width: auto;
padding: 6px 4px;
}
.nav-label {
font-size: 10px;
}
.nav-icon {
font-size: 14px;
}
}
/* Very small screens - 2 rows */
@media (max-width: 480px) {
.navigation-tabs {
grid-template-columns: repeat(4, 1fr);
grid-template-rows: repeat(2, 1fr);
}
}
</style>

View File

@@ -0,0 +1,163 @@
<!--
/**
* Error Dialog Component - Platform Neutral Error Display
*
* Modal dialog for displaying error messages
*
* @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">
<div class="error-icon"></div>
<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 secondary" @click="handleClose">
Close
</button>
<button class="error-button primary" @click="handleRetry">
Retry
</button>
</div>
</div>
</div>
</template>
<script setup lang="ts">
interface Props {
message: string
}
const props = defineProps<Props>()
const emit = defineEmits<{
close: []
retry: []
}>()
const handleClose = (): void => {
emit('close')
}
const handleRetry = (): void => {
emit('retry')
emit('close')
}
const handleOverlayClick = (): void => {
handleClose()
}
</script>
<style scoped>
.error-dialog-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 10000;
padding: 20px;
}
.error-dialog {
background: white;
border-radius: 12px;
padding: 24px;
max-width: 400px;
width: 100%;
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.2);
}
.error-header {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 16px;
}
.error-icon {
font-size: 24px;
}
.error-title {
margin: 0;
font-size: 18px;
font-weight: 600;
color: #d32f2f;
}
.error-content {
margin-bottom: 24px;
}
.error-message {
margin: 0;
color: #666;
line-height: 1.5;
}
.error-actions {
display: flex;
gap: 12px;
justify-content: flex-end;
}
.error-button {
padding: 10px 20px;
border: none;
border-radius: 6px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
}
.error-button.secondary {
background: #f5f5f5;
color: #666;
}
.error-button.secondary:hover {
background: #e0e0e0;
}
.error-button.primary {
background: #1976d2;
color: white;
}
.error-button.primary:hover {
background: #1565c0;
}
/* Mobile responsiveness */
@media (max-width: 768px) {
.error-dialog {
margin: 20px;
padding: 20px;
}
.error-actions {
flex-direction: column;
}
.error-button {
width: 100%;
}
}
</style>

View File

@@ -0,0 +1,67 @@
<!--
/**
* Loading Overlay Component - Platform Neutral Loading
*
* Full-screen loading overlay with spinner
*
* @author Matthew Raymer
* @version 1.0.0
*/
-->
<template>
<div class="loading-overlay">
<div class="loading-content">
<div class="loading-spinner"></div>
<div class="loading-text">Loading Daily Notification Test...</div>
</div>
</div>
</template>
<script setup lang="ts">
// Loading overlay component - no props or logic needed
</script>
<style scoped>
.loading-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 9999;
backdrop-filter: blur(4px);
}
.loading-content {
display: flex;
flex-direction: column;
align-items: center;
gap: 16px;
color: white;
}
.loading-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 {
font-size: 16px;
font-weight: 500;
text-align: center;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
</style>

View File

@@ -0,0 +1,14 @@
import './assets/main.css'
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
import router from './router'
const app = createApp(App)
app.use(createPinia())
app.use(router)
app.mount('#app')

View File

@@ -0,0 +1,109 @@
import { createRouter, createWebHistory } from 'vue-router'
import HomeView from '../views/HomeView.vue'
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
path: '/',
name: 'Home',
component: HomeView,
meta: {
title: 'Daily Notification Test',
requiresAuth: false
}
},
{
path: '/schedule',
name: 'Schedule',
component: () => import('../views/ScheduleView.vue'),
meta: {
title: 'Schedule Notification',
requiresAuth: false
}
},
{
path: '/notifications',
name: 'Notifications',
component: () => import('../views/NotificationsView.vue'),
meta: {
title: 'Notification Management',
requiresAuth: false
}
},
{
path: '/status',
name: 'Status',
component: () => import('../views/StatusView.vue'),
meta: {
title: 'System Status',
requiresAuth: false
}
},
{
path: '/history',
name: 'History',
component: () => import('../views/HistoryView.vue'),
meta: {
title: 'Notification History',
requiresAuth: false
}
},
{
path: '/logs',
name: 'Logs',
component: () => import('../views/LogsView.vue'),
meta: {
title: 'System Logs',
requiresAuth: false
}
},
{
path: '/settings',
name: 'Settings',
component: () => import('../views/SettingsView.vue'),
meta: {
title: 'Settings',
requiresAuth: false
}
},
{
path: '/about',
name: 'About',
component: () => import('../views/AboutView.vue'),
meta: {
title: 'About',
requiresAuth: false
}
},
{
path: '/:pathMatch(.*)*',
name: 'NotFound',
component: () => import('../views/NotFoundView.vue'),
meta: {
title: 'Page Not Found',
requiresAuth: false
}
}
],
})
// Global navigation guards
router.beforeEach((to, from, next) => {
// Set page title
if (to.meta?.title) {
document.title = `${to.meta.title} - Daily Notification Test`
}
// Add loading state
console.log(`🔄 Navigating from ${String(from.name) || 'unknown'} to ${String(to.name) || 'unknown'}`)
next()
})
router.afterEach((to) => {
// Clear any previous errors on successful navigation
console.log(`✅ Navigation completed: ${String(to.name) || 'unknown'}`)
})
export default router

View File

@@ -0,0 +1,109 @@
/**
* App Store - Global Application State
*
* Pinia store for managing global app state, loading, and errors
* Platform-neutral design for Android/iOS/Electron
*
* @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
}
})

View File

@@ -0,0 +1,12 @@
import { ref, computed } from 'vue'
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', () => {
const count = ref(0)
const doubleCount = computed(() => count.value * 2)
function increment() {
count.value++
}
return { count, doubleCount, increment }
})

View File

@@ -0,0 +1,15 @@
<template>
<div class="about">
<h1>This is an about page</h1>
</div>
</template>
<style>
@media (min-width: 1024px) {
.about {
min-height: 100vh;
display: flex;
align-items: center;
}
}
</style>

View File

@@ -0,0 +1,54 @@
<template>
<div class="history-view">
<div class="view-header">
<h1 class="page-title">📋 History</h1>
<p class="page-subtitle">Notification history and activity</p>
</div>
<div class="placeholder-content">
<p>History view coming soon...</p>
</div>
</div>
</template>
<script lang="ts">
import { Component, Vue } from 'vue-facing-decorator'
@Component
export default class HistoryView extends Vue {}
</script>
<style scoped>
.history-view {
padding: 20px;
max-width: 1200px;
margin: 0 auto;
}
.view-header {
text-align: center;
margin-bottom: 30px;
}
.page-title {
margin: 0 0 8px 0;
font-size: 28px;
font-weight: 700;
color: white;
}
.page-subtitle {
margin: 0;
font-size: 16px;
color: rgba(255, 255, 255, 0.8);
}
.placeholder-content {
background: rgba(255, 255, 255, 0.1);
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 12px;
padding: 40px;
text-align: center;
color: rgba(255, 255, 255, 0.8);
backdrop-filter: blur(10px);
}
</style>

View File

@@ -0,0 +1,332 @@
<!--
/**
* Home View - Main Dashboard
*
* Platform-neutral home view with quick actions
*
* @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 + vue-facing-decorator + Capacitor
</p>
<div class="platform-info">
<span class="platform-badge" :class="platformClass">
{{ platformName }}
</span>
<span class="status-badge" :class="statusClass">
{{ statusText }}
</span>
</div>
</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="Check notification history"
@click="navigateToHistory"
/>
<ActionCard
icon="📜"
title="View Logs"
description="Check system logs and debug info"
@click="navigateToLogs"
/>
<ActionCard
icon="⚙️"
title="Settings"
description="Configure app preferences"
@click="navigateToSettings"
/>
</div>
</div>
<!-- System Status -->
<div class="system-status">
<h2 class="section-title">System Status</h2>
<StatusCard :status="systemStatus" @refresh="refreshSystemStatus" />
</div>
</div>
</template>
<script lang="ts">
import { Component, Vue } from 'vue-facing-decorator'
import { useRouter } from 'vue-router'
import { useAppStore } from '@/stores/app'
import ActionCard from '@/components/cards/ActionCard.vue'
import StatusCard from '@/components/cards/StatusCard.vue'
@Component({
components: {
ActionCard,
StatusCard
}
})
export default class HomeView extends Vue {
private router = useRouter()
private appStore = useAppStore()
isScheduling = false
isCheckingStatus = false
get platformName(): string {
const platform = this.appStore.platform
return platform.charAt(0).toUpperCase() + platform.slice(1)
}
get platformClass(): string {
return `platform-${this.appStore.platform}`
}
get statusClass(): string {
const status = this.appStore.notificationStatus
if (!status) return 'unknown'
if (status.canScheduleNow) return 'ready'
return 'not-ready'
}
get statusText(): string {
const status = this.appStore.notificationStatus
if (!status) return 'Unknown'
if (status.canScheduleNow) return 'Ready'
return 'Not Ready'
}
get systemStatus() {
const status = this.appStore.notificationStatus
if (!status) {
return [
{ label: 'Platform', value: this.platformName, status: 'info' },
{ label: 'Plugin', value: 'Not Available', status: 'error' }
]
}
return [
{ label: 'Platform', value: this.platformName, status: 'success' },
{ label: 'Plugin', value: 'Available', status: 'success' },
{ label: 'Notifications', value: status.postNotificationsGranted ? 'Granted' : 'Denied', status: status.postNotificationsGranted ? 'success' : 'error' },
{ label: 'Exact Alarms', value: status.exactAlarmsGranted ? 'Granted' : 'Denied', status: status.exactAlarmsGranted ? 'success' : 'error' },
{ label: 'Channel', value: status.channelEnabled ? 'Enabled' : 'Disabled', status: status.channelEnabled ? 'success' : 'warning' }
]
}
navigateToSchedule(): void {
this.router.push('/schedule')
}
navigateToNotifications(): void {
this.router.push('/notifications')
}
navigateToHistory(): void {
this.router.push('/history')
}
navigateToLogs(): void {
this.router.push('/logs')
}
navigateToSettings(): void {
this.router.push('/settings')
}
async checkSystemStatus(): Promise<void> {
this.isCheckingStatus = true
try {
// Refresh plugin status
await this.refreshSystemStatus()
} catch (error) {
console.error('❌ Status check failed:', error)
this.appStore.setError('Failed to check system status: ' + (error as Error).message)
} finally {
this.isCheckingStatus = false
}
}
async refreshSystemStatus(): Promise<void> {
try {
if (window.DailyNotification) {
const status = await window.DailyNotification.checkStatus()
this.appStore.setNotificationStatus(status)
console.log('✅ System status refreshed:', status)
}
} catch (error) {
console.error('❌ Failed to refresh system status:', error)
throw error
}
}
}
</script>
<style scoped>
.home-view {
padding: 20px;
max-width: 1200px;
margin: 0 auto;
}
.welcome-section {
text-align: center;
margin-bottom: 40px;
padding: 40px 20px;
background: rgba(255, 255, 255, 0.1);
border-radius: 16px;
backdrop-filter: blur(10px);
}
.welcome-title {
margin: 0 0 12px 0;
font-size: 32px;
font-weight: 700;
color: white;
}
.welcome-subtitle {
margin: 0 0 20px 0;
font-size: 16px;
color: rgba(255, 255, 255, 0.8);
}
.platform-info {
display: flex;
justify-content: center;
gap: 12px;
flex-wrap: wrap;
}
.platform-badge {
padding: 6px 12px;
border-radius: 16px;
font-size: 14px;
font-weight: 500;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.platform-android {
background: rgba(76, 175, 80, 0.2);
color: #4caf50;
border: 1px solid rgba(76, 175, 80, 0.3);
}
.platform-ios {
background: rgba(0, 122, 255, 0.2);
color: #007aff;
border: 1px solid rgba(0, 122, 255, 0.3);
}
.platform-electron {
background: rgba(138, 43, 226, 0.2);
color: #8a2be2;
border: 1px solid rgba(138, 43, 226, 0.3);
}
.platform-web {
background: rgba(255, 152, 0, 0.2);
color: #ff9800;
border: 1px solid rgba(255, 152, 0, 0.3);
}
.status-badge {
padding: 6px 12px;
border-radius: 16px;
font-size: 14px;
font-weight: 500;
}
.status-badge.ready {
background: rgba(76, 175, 80, 0.2);
color: #4caf50;
border: 1px solid rgba(76, 175, 80, 0.3);
}
.status-badge.not-ready {
background: rgba(244, 67, 54, 0.2);
color: #f44336;
border: 1px solid rgba(244, 67, 54, 0.3);
}
.status-badge.unknown {
background: rgba(158, 158, 158, 0.2);
color: #9e9e9e;
border: 1px solid rgba(158, 158, 158, 0.3);
}
.quick-actions {
margin-bottom: 40px;
}
.section-title {
margin: 0 0 20px 0;
font-size: 24px;
font-weight: 600;
color: white;
}
.action-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 16px;
}
.system-status {
margin-bottom: 40px;
}
/* Mobile responsiveness */
@media (max-width: 768px) {
.home-view {
padding: 16px;
}
.welcome-section {
padding: 24px 16px;
}
.welcome-title {
font-size: 24px;
}
.action-grid {
grid-template-columns: 1fr;
}
.platform-info {
flex-direction: column;
align-items: center;
}
}
</style>

View File

@@ -0,0 +1,475 @@
<!--
/**
* Logs View - Platform Neutral Log Display
*
* View and copy system logs with clipboard functionality
*
* @author Matthew Raymer
* @version 1.0.0
*/
-->
<template>
<div class="logs-view">
<div class="view-header">
<h1 class="page-title">📋 System Logs</h1>
<p class="page-subtitle">View and copy DailyNotification plugin logs</p>
</div>
<div class="logs-controls">
<button
class="control-button refresh-button"
@click="refreshLogs"
:disabled="isRefreshing"
>
<span v-if="isRefreshing">🔄</span>
<span v-else>🔄</span>
{{ isRefreshing ? 'Refreshing...' : 'Refresh Logs' }}
</button>
<button
class="control-button copy-button"
@click="copyLogsToClipboard"
:disabled="!hasLogs || isCopying"
>
<span v-if="isCopying">📋</span>
<span v-else>📋</span>
{{ isCopying ? 'Copying...' : 'Copy to Clipboard' }}
</button>
<button
class="control-button clear-button"
@click="clearLogs"
:disabled="!hasLogs"
>
🗑 Clear Logs
</button>
</div>
<div class="logs-container">
<div v-if="hasLogs" class="logs-content">
<div class="logs-header">
<span class="logs-count">{{ logs.length }} log entries</span>
<span class="last-updated">Last updated: {{ formatTimestamp(lastUpdated) }}</span>
</div>
<div class="log-entries">
<div
v-for="(log, index) in logs"
:key="index"
:class="['log-entry', `log-level-${log.level.toLowerCase()}`]"
>
<span class="log-timestamp">{{ formatTimestamp(log.timestamp) }}</span>
<span class="log-level">[{{ log.level }}]</span>
<span class="log-tag">{{ log.tag }}:</span>
<span class="log-message">{{ log.message }}</span>
</div>
</div>
</div>
<div v-else class="no-logs">
<span class="empty-icon">📜</span>
<p class="empty-message">No logs available. Click "Refresh Logs" to fetch them.</p>
</div>
</div>
<div v-if="feedbackMessage" :class="['feedback-message', feedbackType]">
{{ feedbackMessage }}
</div>
</div>
</template>
<script lang="ts">
import { Component, Vue } from 'vue-facing-decorator'
import { Capacitor } from '@capacitor/core'
import { format } from 'date-fns'
import { useAppStore } from '@/stores/app'
interface LogEntry {
timestamp: number
level: string
tag: string
message: string
}
@Component
export default class LogsView extends Vue {
private appStore = useAppStore()
logs: LogEntry[] = []
isRefreshing = false
isCopying = false
lastUpdated: number = Date.now()
feedbackMessage: string | null = null
feedbackType: 'success' | 'error' = 'success'
get hasLogs(): boolean {
return this.logs.length > 0
}
mounted(): void {
this.refreshLogs()
}
async refreshLogs(): Promise<void> {
if (!Capacitor.isNativePlatform()) {
this.appStore.setError('Log fetching is only available on native platforms.')
return
}
this.isRefreshing = true
this.clearFeedback()
try {
// For now, create mock logs - replace with actual plugin call
// Example: const rawLogs = await DailyNotificationPlugin.getLogs();
const mockLogs: LogEntry[] = [
{ timestamp: Date.now() - 5000, level: 'DEBUG', tag: 'DailyNotificationPlugin', message: 'Plugin initialized' },
{ timestamp: Date.now() - 4000, level: 'INFO', tag: 'DailyNotificationScheduler', message: 'Notification scheduled for 09:00' },
{ timestamp: Date.now() - 3000, level: 'WARN', tag: 'DailyNotificationWorker', message: 'JIT freshness check skipped: content too fresh' },
{ timestamp: Date.now() - 2000, level: 'ERROR', tag: 'DailyNotificationStorage', message: 'Failed to save notification: disk full' },
{ timestamp: Date.now() - 1000, level: 'INFO', tag: 'DailyNotificationReceiver', message: 'Received NOTIFICATION intent' }
]
this.logs = mockLogs.sort((a, b) => a.timestamp - b.timestamp)
this.lastUpdated = Date.now()
this.showFeedback(`✅ Fetched ${this.logs.length} log entries.`, 'success')
} catch (error) {
console.error('❌ Failed to refresh logs:', error)
this.appStore.setError('Failed to fetch logs: ' + (error as Error).message)
this.showFeedback('❌ Failed to fetch logs.', 'error')
} finally {
this.isRefreshing = false
}
}
async copyLogsToClipboard(): Promise<void> {
if (!this.hasLogs) {
this.showFeedback('No logs to copy.', 'error')
return
}
this.isCopying = true
this.clearFeedback()
const logsText = this.formatLogsForCopy()
try {
if (Capacitor.isNativePlatform()) {
// Use Capacitor Clipboard plugin if available
// Assuming Clipboard plugin is installed and configured
await window.Capacitor.Plugins.Clipboard.write({
string: logsText
})
} else {
// Web clipboard API
await navigator.clipboard.writeText(logsText)
}
this.showFeedback(`✅ Copied ${this.logs.length} log entries to clipboard!`, 'success')
} catch (error) {
console.error('❌ Failed to copy logs:', error)
this.appStore.setError('Failed to copy logs: ' + (error as Error).message)
this.showFeedback('❌ Failed to copy logs.', 'error')
} finally {
this.isCopying = false
}
}
clearLogs(): void {
this.logs = []
this.clearFeedback()
this.showFeedback('🗑️ Logs cleared.', 'success')
}
formatTimestamp(timestamp: number): string {
return format(new Date(timestamp), 'yyyy-MM-dd HH:mm:ss')
}
formatLogsForCopy(): string {
let formattedText = `DailyNotification Plugin Logs\n`
formattedText += `Generated: ${format(new Date(), 'MM/dd/yyyy, h:mm:ss a')}\n`
formattedText += `Total Entries: ${this.logs.length}\n\n`
this.logs.forEach(log => {
formattedText += `${this.formatTimestamp(log.timestamp)} ${log.level}/${log.tag}: ${log.message}\n`
})
return formattedText
}
showFeedback(message: string, type: 'success' | 'error'): void {
this.feedbackMessage = message
this.feedbackType = type
setTimeout(() => {
this.feedbackMessage = null
}, 3000) // Hide after 3 seconds
}
clearFeedback(): void {
this.feedbackMessage = null
this.feedbackType = 'success'
}
}
</script>
<style scoped>
.logs-view {
padding: 20px;
max-width: 1200px;
margin: 0 auto;
}
.view-header {
text-align: center;
margin-bottom: 30px;
}
.page-title {
margin: 0 0 8px 0;
font-size: 28px;
font-weight: 700;
color: white;
}
.page-subtitle {
margin: 0;
font-size: 16px;
color: rgba(255, 255, 255, 0.8);
}
.logs-controls {
display: flex;
gap: 12px;
margin-bottom: 24px;
flex-wrap: wrap;
justify-content: center;
}
.control-button {
padding: 12px 20px;
border: none;
border-radius: 8px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
display: flex;
align-items: center;
gap: 8px;
}
.refresh-button {
background: #2196f3;
color: white;
}
.refresh-button:hover:not(:disabled) {
background: #1976d2;
}
.copy-button {
background: #ff9800;
color: white;
}
.copy-button:hover:not(:disabled) {
background: #f57c00;
}
.clear-button {
background: #f44336;
color: white;
}
.clear-button:hover:not(:disabled) {
background: #d32f2f;
}
.control-button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.logs-container {
background: rgba(255, 255, 255, 0.1);
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 12px;
backdrop-filter: blur(10px);
overflow: hidden;
}
.logs-content {
padding: 20px;
}
.logs-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
padding-bottom: 12px;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
.logs-count {
font-size: 14px;
font-weight: 500;
color: rgba(255, 255, 255, 0.8);
}
.last-updated {
font-size: 12px;
color: rgba(255, 255, 255, 0.6);
}
.log-entries {
max-height: 400px;
overflow-y: auto;
display: flex;
flex-direction: column;
gap: 8px;
}
.log-entry {
display: flex;
align-items: flex-start;
gap: 12px;
padding: 8px 12px;
border-radius: 6px;
font-family: 'Courier New', monospace;
font-size: 13px;
line-height: 1.4;
}
.log-level-debug {
background: rgba(33, 150, 243, 0.1);
border-left: 3px solid #2196f3;
}
.log-level-info {
background: rgba(76, 175, 80, 0.1);
border-left: 3px solid #4caf50;
}
.log-level-warn {
background: rgba(255, 152, 0, 0.1);
border-left: 3px solid #ff9800;
}
.log-level-error {
background: rgba(244, 67, 54, 0.1);
border-left: 3px solid #f44336;
}
.log-timestamp {
color: rgba(255, 255, 255, 0.6);
font-size: 12px;
flex-shrink: 0;
}
.log-level {
font-weight: 600;
flex-shrink: 0;
min-width: 60px;
}
.log-level-debug .log-level {
color: #2196f3;
}
.log-level-info .log-level {
color: #4caf50;
}
.log-level-warn .log-level {
color: #ff9800;
}
.log-level-error .log-level {
color: #f44336;
}
.log-tag {
color: rgba(255, 255, 255, 0.8);
font-weight: 500;
flex-shrink: 0;
}
.log-message {
color: white;
flex: 1;
}
.no-logs {
padding: 40px 20px;
text-align: center;
color: rgba(255, 255, 255, 0.6);
}
.empty-icon {
font-size: 48px;
display: block;
margin-bottom: 16px;
}
.empty-message {
margin: 0;
font-size: 16px;
}
.feedback-message {
position: fixed;
bottom: 20px;
left: 50%;
transform: translateX(-50%);
padding: 12px 20px;
border-radius: 8px;
font-size: 14px;
font-weight: 500;
z-index: 1000;
backdrop-filter: blur(10px);
}
.feedback-message.success {
background: rgba(76, 175, 80, 0.9);
color: white;
border: 1px solid rgba(76, 175, 80, 0.3);
}
.feedback-message.error {
background: rgba(244, 67, 54, 0.9);
color: white;
border: 1px solid rgba(244, 67, 54, 0.3);
}
/* Mobile responsiveness */
@media (max-width: 768px) {
.logs-view {
padding: 16px;
}
.logs-controls {
flex-direction: column;
align-items: stretch;
}
.control-button {
justify-content: center;
}
.logs-header {
flex-direction: column;
gap: 8px;
align-items: flex-start;
}
.log-entry {
flex-direction: column;
gap: 4px;
}
.log-timestamp {
font-size: 11px;
}
}
</style>

View File

@@ -0,0 +1,70 @@
<template>
<div class="not-found-view">
<div class="view-header">
<h1 class="page-title">404</h1>
<p class="page-subtitle">Page not found</p>
</div>
<div class="placeholder-content">
<p>The page you're looking for doesn't exist.</p>
<router-link to="/" class="home-link"> Back to Home</router-link>
</div>
</div>
</template>
<script lang="ts">
import { Component, Vue } from 'vue-facing-decorator'
@Component
export default class NotFoundView extends Vue {}
</script>
<style scoped>
.not-found-view {
padding: 20px;
max-width: 1200px;
margin: 0 auto;
}
.view-header {
text-align: center;
margin-bottom: 30px;
}
.page-title {
margin: 0 0 8px 0;
font-size: 28px;
font-weight: 700;
color: white;
}
.page-subtitle {
margin: 0;
font-size: 16px;
color: rgba(255, 255, 255, 0.8);
}
.placeholder-content {
background: rgba(255, 255, 255, 0.1);
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 12px;
padding: 40px;
text-align: center;
color: rgba(255, 255, 255, 0.8);
backdrop-filter: blur(10px);
}
.home-link {
display: inline-block;
margin-top: 20px;
padding: 12px 24px;
background: #1976d2;
color: white;
text-decoration: none;
border-radius: 8px;
transition: background 0.2s ease;
}
.home-link:hover {
background: #1565c0;
}
</style>

View File

@@ -0,0 +1,54 @@
<template>
<div class="notifications-view">
<div class="view-header">
<h1 class="page-title">🔔 Notifications</h1>
<p class="page-subtitle">Manage scheduled notifications</p>
</div>
<div class="placeholder-content">
<p>Notifications management coming soon...</p>
</div>
</div>
</template>
<script lang="ts">
import { Component, Vue } from 'vue-facing-decorator'
@Component
export default class NotificationsView extends Vue {}
</script>
<style scoped>
.notifications-view {
padding: 20px;
max-width: 1200px;
margin: 0 auto;
}
.view-header {
text-align: center;
margin-bottom: 30px;
}
.page-title {
margin: 0 0 8px 0;
font-size: 28px;
font-weight: 700;
color: white;
}
.page-subtitle {
margin: 0;
font-size: 16px;
color: rgba(255, 255, 255, 0.8);
}
.placeholder-content {
background: rgba(255, 255, 255, 0.1);
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 12px;
padding: 40px;
text-align: center;
color: rgba(255, 255, 255, 0.8);
backdrop-filter: blur(10px);
}
</style>

View File

@@ -0,0 +1,180 @@
<!--
/**
* Schedule View - Platform Neutral Scheduling Interface
*
* @author Matthew Raymer
* @version 1.0.0
*/
-->
<template>
<div class="schedule-view">
<div class="view-header">
<h1 class="page-title">📅 Schedule Notification</h1>
<p class="page-subtitle">Schedule a new daily notification</p>
</div>
<div class="schedule-form">
<div class="form-group">
<label class="form-label">Notification Time</label>
<input type="time" class="form-input" v-model="scheduleTime" />
</div>
<div class="form-group">
<label class="form-label">Title</label>
<input type="text" class="form-input" v-model="notificationTitle" placeholder="Daily Update" />
</div>
<div class="form-group">
<label class="form-label">Message</label>
<textarea class="form-textarea" v-model="notificationMessage" placeholder="Your daily notification message"></textarea>
</div>
<div class="form-actions">
<button class="action-button primary" @click="scheduleNotification" :disabled="isScheduling">
{{ isScheduling ? 'Scheduling...' : 'Schedule Notification' }}
</button>
</div>
</div>
</div>
</template>
<script lang="ts">
import { Component, Vue } from 'vue-facing-decorator'
@Component
export default class ScheduleView extends Vue {
scheduleTime = '09:00'
notificationTitle = 'Daily Update'
notificationMessage = 'Your daily notification is ready!'
isScheduling = false
async scheduleNotification(): Promise<void> {
this.isScheduling = true
try {
// TODO: Implement actual scheduling
console.log('Scheduling notification:', {
time: this.scheduleTime,
title: this.notificationTitle,
message: this.notificationMessage
})
// Mock success
await new Promise(resolve => setTimeout(resolve, 1000))
console.log('✅ Notification scheduled successfully')
} catch (error) {
console.error('❌ Failed to schedule notification:', error)
} finally {
this.isScheduling = false
}
}
}
</script>
<style scoped>
.schedule-view {
padding: 20px;
max-width: 600px;
margin: 0 auto;
}
.view-header {
text-align: center;
margin-bottom: 30px;
}
.page-title {
margin: 0 0 8px 0;
font-size: 28px;
font-weight: 700;
color: white;
}
.page-subtitle {
margin: 0;
font-size: 16px;
color: rgba(255, 255, 255, 0.8);
}
.schedule-form {
background: rgba(255, 255, 255, 0.1);
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 12px;
padding: 24px;
backdrop-filter: blur(10px);
}
.form-group {
margin-bottom: 20px;
}
.form-label {
display: block;
margin-bottom: 8px;
font-size: 14px;
font-weight: 500;
color: white;
}
.form-input, .form-textarea {
width: 100%;
padding: 12px;
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 8px;
background: rgba(255, 255, 255, 0.1);
color: white;
font-size: 14px;
transition: border-color 0.2s ease;
}
.form-input:focus, .form-textarea:focus {
outline: none;
border-color: rgba(255, 255, 255, 0.4);
}
.form-textarea {
min-height: 80px;
resize: vertical;
}
.form-actions {
margin-top: 24px;
}
.action-button {
width: 100%;
padding: 14px;
border: none;
border-radius: 8px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s ease;
}
.action-button.primary {
background: #1976d2;
color: white;
}
.action-button.primary:hover:not(:disabled) {
background: #1565c0;
}
.action-button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
/* Mobile responsiveness */
@media (max-width: 768px) {
.schedule-view {
padding: 16px;
}
.schedule-form {
padding: 20px;
}
}
</style>

View File

@@ -0,0 +1,54 @@
<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="placeholder-content">
<p>Settings view 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 {
padding: 20px;
max-width: 1200px;
margin: 0 auto;
}
.view-header {
text-align: center;
margin-bottom: 30px;
}
.page-title {
margin: 0 0 8px 0;
font-size: 28px;
font-weight: 700;
color: white;
}
.page-subtitle {
margin: 0;
font-size: 16px;
color: rgba(255, 255, 255, 0.8);
}
.placeholder-content {
background: rgba(255, 255, 255, 0.1);
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 12px;
padding: 40px;
text-align: center;
color: rgba(255, 255, 255, 0.8);
backdrop-filter: blur(10px);
}
</style>

View File

@@ -0,0 +1,54 @@
<template>
<div class="status-view">
<div class="view-header">
<h1 class="page-title">📊 Status</h1>
<p class="page-subtitle">System status and diagnostics</p>
</div>
<div class="placeholder-content">
<p>Status view coming soon...</p>
</div>
</div>
</template>
<script lang="ts">
import { Component, Vue } from 'vue-facing-decorator'
@Component
export default class StatusView extends Vue {}
</script>
<style scoped>
.status-view {
padding: 20px;
max-width: 1200px;
margin: 0 auto;
}
.view-header {
text-align: center;
margin-bottom: 30px;
}
.page-title {
margin: 0 0 8px 0;
font-size: 28px;
font-weight: 700;
color: white;
}
.page-subtitle {
margin: 0;
font-size: 16px;
color: rgba(255, 255, 255, 0.8);
}
.placeholder-content {
background: rgba(255, 255, 255, 0.1);
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 12px;
padding: 40px;
text-align: center;
color: rgba(255, 255, 255, 0.8);
backdrop-filter: blur(10px);
}
</style>

View File

@@ -0,0 +1,14 @@
{
"extends": "@vue/tsconfig/tsconfig.dom.json",
"include": ["env.d.ts", "src/**/*", "src/**/*.vue"],
"exclude": ["src/**/__tests__/*"],
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"useDefineForClassFields": false,
"paths": {
"@/*": ["./src/*"]
}
}
}

View File

@@ -0,0 +1,11 @@
{
"files": [],
"references": [
{
"path": "./tsconfig.node.json"
},
{
"path": "./tsconfig.app.json"
}
]
}

View File

@@ -0,0 +1,19 @@
{
"extends": "@tsconfig/node22/tsconfig.json",
"include": [
"vite.config.*",
"vitest.config.*",
"cypress.config.*",
"nightwatch.conf.*",
"playwright.config.*",
"eslint.config.*"
],
"compilerOptions": {
"noEmit": true,
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
"module": "ESNext",
"moduleResolution": "Bundler",
"types": ["node"]
}
}

View File

@@ -0,0 +1,18 @@
import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import vueDevTools from 'vite-plugin-vue-devtools'
// https://vite.dev/config/
export default defineConfig({
plugins: [
vue(),
vueDevTools(),
],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
},
},
})