Major PWA and mobile UI improvements
- Fixed PWA installation on Android by correcting manifest.json icon configuration - Made UI mobile-friendly with compact layout and sticky record button - Implemented auto-translation after transcription stops - Updated branding from 'Voice Translator' to 'Talk2Me' throughout - Added reverse proxy support with ProxyFix middleware - Created diagnostic tools for PWA troubleshooting - Added proper HTTP headers for service worker and manifest - Improved mobile CSS with responsive design - Fixed JavaScript bundling with webpack configuration - Updated service worker cache versioning - Added comprehensive PWA documentation These changes ensure the app works properly as a PWA on Android devices and provides a better mobile user experience.
This commit is contained in:
218
static/check-pwa-status.html
Normal file
218
static/check-pwa-status.html
Normal file
@@ -0,0 +1,218 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>PWA Status Check - Talk2Me</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
padding: 20px;
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
.status {
|
||||
padding: 10px;
|
||||
margin: 10px 0;
|
||||
border-radius: 5px;
|
||||
}
|
||||
.success {
|
||||
background: #d4edda;
|
||||
color: #155724;
|
||||
}
|
||||
.error {
|
||||
background: #f8d7da;
|
||||
color: #721c24;
|
||||
}
|
||||
.warning {
|
||||
background: #fff3cd;
|
||||
color: #856404;
|
||||
}
|
||||
.info {
|
||||
background: #d1ecf1;
|
||||
color: #0c5460;
|
||||
}
|
||||
button {
|
||||
padding: 10px 20px;
|
||||
margin: 10px 0;
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
}
|
||||
pre {
|
||||
background: #f5f5f5;
|
||||
padding: 10px;
|
||||
overflow-x: auto;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Talk2Me PWA Status Check</h1>
|
||||
|
||||
<div id="results"></div>
|
||||
|
||||
<h2>Actions</h2>
|
||||
<button onclick="testInstall()">Test Install Prompt</button>
|
||||
<button onclick="clearPWA()">Clear PWA Data</button>
|
||||
<button onclick="location.reload()">Refresh Page</button>
|
||||
|
||||
<script>
|
||||
const results = document.getElementById('results');
|
||||
|
||||
function addResult(message, type = 'info') {
|
||||
const div = document.createElement('div');
|
||||
div.className = `status ${type}`;
|
||||
div.textContent = message;
|
||||
results.appendChild(div);
|
||||
}
|
||||
|
||||
// Check HTTPS
|
||||
if (location.protocol === 'https:' || location.hostname === 'localhost') {
|
||||
addResult('✓ HTTPS enabled', 'success');
|
||||
} else {
|
||||
addResult('✗ HTTPS required for PWA', 'error');
|
||||
}
|
||||
|
||||
// Check Service Worker support
|
||||
if ('serviceWorker' in navigator) {
|
||||
addResult('✓ Service Worker supported', 'success');
|
||||
|
||||
// Check registration
|
||||
navigator.serviceWorker.getRegistration().then(reg => {
|
||||
if (reg) {
|
||||
addResult(`✓ Service Worker registered (scope: ${reg.scope})`, 'success');
|
||||
if (reg.active) {
|
||||
addResult('✓ Service Worker is active', 'success');
|
||||
} else if (reg.installing) {
|
||||
addResult('⚠ Service Worker is installing', 'warning');
|
||||
} else if (reg.waiting) {
|
||||
addResult('⚠ Service Worker is waiting', 'warning');
|
||||
}
|
||||
} else {
|
||||
addResult('✗ Service Worker not registered', 'error');
|
||||
}
|
||||
});
|
||||
} else {
|
||||
addResult('✗ Service Worker not supported', 'error');
|
||||
}
|
||||
|
||||
// Check manifest
|
||||
const manifestLink = document.querySelector('link[rel="manifest"]');
|
||||
if (manifestLink) {
|
||||
addResult('✓ Manifest link found', 'success');
|
||||
|
||||
fetch(manifestLink.href)
|
||||
.then(r => r.json())
|
||||
.then(manifest => {
|
||||
addResult('✓ Manifest loaded successfully', 'success');
|
||||
|
||||
// Check required fields
|
||||
const required = ['name', 'short_name', 'start_url', 'display', 'icons'];
|
||||
required.forEach(field => {
|
||||
if (manifest[field]) {
|
||||
addResult(`✓ Manifest has ${field}`, 'success');
|
||||
} else {
|
||||
addResult(`✗ Manifest missing ${field}`, 'error');
|
||||
}
|
||||
});
|
||||
|
||||
// Check icons
|
||||
if (manifest.icons && manifest.icons.length > 0) {
|
||||
const has192 = manifest.icons.some(i => i.sizes && i.sizes.includes('192'));
|
||||
const has512 = manifest.icons.some(i => i.sizes && i.sizes.includes('512'));
|
||||
|
||||
if (has192) addResult('✓ Has 192x192 icon', 'success');
|
||||
else addResult('✗ Missing 192x192 icon', 'error');
|
||||
|
||||
if (has512) addResult('✓ Has 512x512 icon', 'success');
|
||||
else addResult('⚠ Missing 512x512 icon (recommended)', 'warning');
|
||||
|
||||
// Check icon purposes
|
||||
manifest.icons.forEach((icon, i) => {
|
||||
if (icon.purpose) {
|
||||
addResult(`Icon ${i+1}: purpose="${icon.purpose}"`, 'info');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Show manifest content
|
||||
const pre = document.createElement('pre');
|
||||
pre.textContent = JSON.stringify(manifest, null, 2);
|
||||
results.appendChild(pre);
|
||||
})
|
||||
.catch(err => {
|
||||
addResult(`✗ Failed to load manifest: ${err}`, 'error');
|
||||
});
|
||||
} else {
|
||||
addResult('✗ No manifest link found', 'error');
|
||||
}
|
||||
|
||||
// Check if already installed
|
||||
if (window.matchMedia('(display-mode: standalone)').matches) {
|
||||
addResult('✓ App is running in standalone mode (already installed)', 'success');
|
||||
} else {
|
||||
addResult('App is running in browser mode', 'info');
|
||||
}
|
||||
|
||||
// Listen for install prompt
|
||||
let deferredPrompt;
|
||||
window.addEventListener('beforeinstallprompt', (e) => {
|
||||
e.preventDefault();
|
||||
deferredPrompt = e;
|
||||
addResult('✓ Install prompt is available!', 'success');
|
||||
addResult('Chrome recognizes this as an installable PWA', 'success');
|
||||
});
|
||||
|
||||
// Check Chrome version
|
||||
const userAgent = navigator.userAgent;
|
||||
const chromeMatch = userAgent.match(/Chrome\/(\d+)/);
|
||||
if (chromeMatch) {
|
||||
const version = parseInt(chromeMatch[1]);
|
||||
addResult(`Chrome version: ${version}`, 'info');
|
||||
if (version < 90) {
|
||||
addResult('⚠ Chrome version is old, consider updating', 'warning');
|
||||
}
|
||||
}
|
||||
|
||||
// Test install
|
||||
function testInstall() {
|
||||
if (deferredPrompt) {
|
||||
deferredPrompt.prompt();
|
||||
deferredPrompt.userChoice.then((choiceResult) => {
|
||||
if (choiceResult.outcome === 'accepted') {
|
||||
addResult('✓ User accepted the install prompt', 'success');
|
||||
} else {
|
||||
addResult('User dismissed the install prompt', 'warning');
|
||||
}
|
||||
deferredPrompt = null;
|
||||
});
|
||||
} else {
|
||||
addResult('No install prompt available. Chrome may not recognize this as installable.', 'error');
|
||||
addResult('Try: Menu (⋮) → Add to Home screen', 'info');
|
||||
}
|
||||
}
|
||||
|
||||
// Clear PWA data
|
||||
function clearPWA() {
|
||||
if ('serviceWorker' in navigator) {
|
||||
navigator.serviceWorker.getRegistrations().then(function(registrations) {
|
||||
for(let registration of registrations) {
|
||||
registration.unregister();
|
||||
}
|
||||
addResult('Service Workers unregistered', 'info');
|
||||
});
|
||||
}
|
||||
|
||||
if ('caches' in window) {
|
||||
caches.keys().then(function(names) {
|
||||
for (let name of names) {
|
||||
caches.delete(name);
|
||||
}
|
||||
addResult('Caches cleared', 'info');
|
||||
});
|
||||
}
|
||||
|
||||
addResult('PWA data cleared. Reload the page to re-register.', 'info');
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
@@ -422,6 +422,41 @@
|
||||
max-width: 300px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* Make the entire layout more compact on mobile */
|
||||
body {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* Reduce spacing on mobile */
|
||||
.mb-3 {
|
||||
margin-bottom: 0.5rem !important;
|
||||
}
|
||||
|
||||
.mb-4 {
|
||||
margin-bottom: 0.75rem !important;
|
||||
}
|
||||
|
||||
/* Compact cards on mobile */
|
||||
.card {
|
||||
margin-bottom: 8px !important;
|
||||
}
|
||||
|
||||
/* Hide less important elements on small screens */
|
||||
.text-muted.small {
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
/* Adjust button sizes */
|
||||
.btn {
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
/* Make dropdowns more compact */
|
||||
.form-select {
|
||||
font-size: 0.875rem;
|
||||
padding: 0.25rem 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* Streaming translation styles */
|
||||
|
@@ -714,6 +714,21 @@ function initApp(): void {
|
||||
language: data.detected_language || sourceLanguage.value,
|
||||
timestamp: new Date().toISOString()
|
||||
} as TranscriptionRecord);
|
||||
|
||||
// Automatically trigger translation
|
||||
setTimeout(async () => {
|
||||
statusIndicator.textContent = 'Automatically translating...';
|
||||
statusIndicator.classList.add('processing');
|
||||
try {
|
||||
await performTranslation();
|
||||
} catch (error) {
|
||||
console.error('Auto-translation failed:', error);
|
||||
statusIndicator.textContent = 'Transcription complete - Translation failed';
|
||||
statusIndicator.classList.remove('processing');
|
||||
statusIndicator.classList.add('warning');
|
||||
setTimeout(() => statusIndicator.classList.remove('warning'), 2000);
|
||||
}
|
||||
}, 500); // Small delay for better UX
|
||||
} else {
|
||||
sourceText.innerHTML = `<p class="text-danger fade-in">Error: ${data.error}</p>`;
|
||||
statusIndicator.textContent = 'Transcription failed';
|
||||
@@ -756,8 +771,8 @@ function initApp(): void {
|
||||
}
|
||||
);
|
||||
|
||||
// Translate button click event
|
||||
translateBtn.addEventListener('click', errorBoundary.wrapAsync(async function() {
|
||||
// Function to perform translation (extracted from button handler)
|
||||
const performTranslation = async function(): Promise<void> {
|
||||
if (!currentSourceText) {
|
||||
return;
|
||||
}
|
||||
@@ -794,7 +809,10 @@ function initApp(): void {
|
||||
}
|
||||
|
||||
// No cache hit, proceed with API call
|
||||
statusIndicator.textContent = 'Translating...';
|
||||
// Don't update status if already showing 'Automatically translating...'
|
||||
if (!statusIndicator.textContent?.includes('Automatically translating')) {
|
||||
statusIndicator.textContent = 'Translating...';
|
||||
}
|
||||
|
||||
// Use streaming if enabled
|
||||
if (streamingEnabled && navigator.onLine) {
|
||||
@@ -992,12 +1010,19 @@ function initApp(): void {
|
||||
statusIndicator.textContent = 'Translation failed';
|
||||
}
|
||||
}
|
||||
}, 'translation', async () => {
|
||||
hideProgress();
|
||||
hideLoadingOverlay();
|
||||
translatedText.innerHTML = '<p class="text-danger">Translation failed. Please try again.</p>';
|
||||
statusIndicator.textContent = 'Translation error - please retry';
|
||||
}));
|
||||
};
|
||||
|
||||
// Translate button click event (now just calls performTranslation)
|
||||
translateBtn.addEventListener('click', errorBoundary.wrapAsync(
|
||||
performTranslation,
|
||||
'translation',
|
||||
async () => {
|
||||
hideProgress();
|
||||
hideLoadingOverlay();
|
||||
translatedText.innerHTML = '<p class="text-danger">Translation failed. Please try again.</p>';
|
||||
statusIndicator.textContent = 'Translation error - please retry';
|
||||
}
|
||||
));
|
||||
|
||||
// Play source text
|
||||
playSource.addEventListener('click', function() {
|
||||
@@ -1398,7 +1423,7 @@ function initInstallPrompt(): void {
|
||||
const installButton = document.createElement('button');
|
||||
installButton.style.display = 'none';
|
||||
installButton.classList.add('btn', 'btn-success', 'fixed-bottom', 'm-3');
|
||||
installButton.innerHTML = 'Install Voice Translator <i class="fas fa-download ml-2"></i>';
|
||||
installButton.innerHTML = 'Install Talk2Me <i class="fas fa-download ml-2"></i>';
|
||||
document.body.appendChild(installButton);
|
||||
|
||||
window.addEventListener('beforeinstallprompt', (e: Event) => {
|
||||
|
@@ -1,30 +1,42 @@
|
||||
{
|
||||
"name": "Talk2Me",
|
||||
"short_name": "Translator",
|
||||
"description": "Translate spoken language between multiple languages with speech input and output",
|
||||
"short_name": "Talk2Me",
|
||||
"description": "Real-time voice translation app - translate spoken language instantly",
|
||||
"start_url": "/",
|
||||
"scope": "/",
|
||||
"display": "standalone",
|
||||
"orientation": "portrait",
|
||||
"background_color": "#ffffff",
|
||||
"theme_color": "#007bff",
|
||||
"icons": [
|
||||
{
|
||||
"src": "./static/icons/icon-192x192.png",
|
||||
"src": "/static/icons/icon-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png",
|
||||
"purpose": "any maskable"
|
||||
},
|
||||
{
|
||||
"src": "./static/icons/icon-512x512.png",
|
||||
"src": "/static/icons/icon-512x512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png",
|
||||
"purpose": "any maskable"
|
||||
}
|
||||
],
|
||||
"screenshots": [
|
||||
"shortcuts": [
|
||||
{
|
||||
"src": "./static/screenshots/screenshot1.png",
|
||||
"sizes": "1280x720",
|
||||
"type": "image/png"
|
||||
"name": "Start Recording",
|
||||
"short_name": "Record",
|
||||
"description": "Start voice recording for translation",
|
||||
"url": "/?action=record",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/static/icons/icon-192x192.png",
|
||||
"sizes": "192x192"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
],
|
||||
"categories": ["productivity", "utilities", "education"],
|
||||
"prefer_related_applications": false,
|
||||
"related_applications": []
|
||||
}
|
||||
|
41
static/pwa-update.js
Normal file
41
static/pwa-update.js
Normal file
@@ -0,0 +1,41 @@
|
||||
// PWA Update Helper
|
||||
// This script helps force PWA updates on clients
|
||||
|
||||
// Force service worker update
|
||||
if ('serviceWorker' in navigator) {
|
||||
navigator.serviceWorker.getRegistrations().then(function(registrations) {
|
||||
for(let registration of registrations) {
|
||||
registration.unregister().then(function() {
|
||||
console.log('Service worker unregistered');
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Re-register after a short delay
|
||||
setTimeout(function() {
|
||||
navigator.serviceWorker.register('/service-worker.js')
|
||||
.then(function(registration) {
|
||||
console.log('Service worker re-registered');
|
||||
registration.update();
|
||||
});
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
// Clear all caches
|
||||
if ('caches' in window) {
|
||||
caches.keys().then(function(names) {
|
||||
for (let name of names) {
|
||||
caches.delete(name);
|
||||
console.log('Cache cleared:', name);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Reload manifest
|
||||
var link = document.querySelector('link[rel="manifest"]');
|
||||
if (link) {
|
||||
link.href = link.href + '?v=' + Date.now();
|
||||
console.log('Manifest reloaded');
|
||||
}
|
||||
|
||||
console.log('PWA update complete. Please reload the page.');
|
@@ -1,10 +1,11 @@
|
||||
// Service Worker for Talk2Me PWA
|
||||
|
||||
const CACHE_NAME = 'voice-translator-v1';
|
||||
const CACHE_NAME = 'talk2me-v4'; // Increment version to force update
|
||||
const ASSETS_TO_CACHE = [
|
||||
'/',
|
||||
'/static/manifest.json',
|
||||
'/static/css/styles.css',
|
||||
'/static/js/dist/app.js',
|
||||
'/static/js/dist/app.bundle.js',
|
||||
'/static/icons/icon-192x192.png',
|
||||
'/static/icons/icon-512x512.png',
|
||||
'/static/icons/favicon.ico',
|
||||
|
Reference in New Issue
Block a user