پست

اضافه کردن PWA به وبسایت

اگر یک وب سایت دارید که می‌خواهید به صورت اپلیکیشن روی موبایل و دسکتاپ هم قابل استفاده باشد، می‌تونید از PWA یا Progressive Web App استفاده کنید. در اینجا یک نمونه کامل از فایل‌های مورد نیاز برای اضافه کردن PWA به وبسایت قرار داده شده است که می‌توانید از آن استفاده کنید. همچنین در این کدها بخش نوتیفیکیشن و سینک در پس‌زمینه هم اضافه شده است.
اگر نسخه جدیدی از سرویس‌ورکر منتشر شد، به کاربر اطلاع داده می‌شود و با کلیک روی دکمه به‌روزرسانی، کش‌ها پاک شده و نسخه جدید بارگذاری می‌شود. برای این کار کافی است در فایل sw ورژن خط اول را تغییر دهید

sw.js:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
const CACHE_NAME = 'web-tools-v1.0.0';
const urlsToCache = [
    '/',
    '/index.html',
    '/style.css',
    '/script.js',
    '/favicon.png',
    '/favicon.ico',
    '/manifest.json'
];

self.addEventListener('install', (event) => {
    event.waitUntil(
        caches.open(CACHE_NAME)
            .then((cache) => {
                console.log('Opened cache');
                return cache.addAll(urlsToCache);
            })
    );
});

// Send message to all clients when new version is ready
self.addEventListener('activate', (event) => {
    const cacheWhitelist = [CACHE_NAME];

    event.waitUntil(
        caches.keys().then((cacheNames) => {
            return Promise.all(
                cacheNames.map((cacheName) => {
                    if (cacheWhitelist.indexOf(cacheName) === -1) {
                        return caches.delete(cacheName);
                    }
                })
            );
        }).then(() => {
            // Notify all clients about the update
            return self.clients.matchAll().then((clients) => {
                clients.forEach(client => {
                    client.postMessage({
                        type: 'SW_UPDATED',
                        message: 'نسخه جدید در دسترس است'
                    });
                });
            });
        })
    );

    return self.clients.claim();
});

// Listen for skip waiting message from page
self.addEventListener('message', (event) => {
    if (event.data && event.data.type === 'SKIP_WAITING') {
        self.skipWaiting();
    }
});

// Fetch and cache strategy
self.addEventListener('fetch', (event) => {
    event.respondWith(
        caches.match(event.request)
            .then((response) => {
                if (response) {
                    return response;
                }

                const fetchRequest = event.request.clone();

                return fetch(fetchRequest).then((response) => {
                    if (!response || response.status !== 200 || response.type !== 'basic') {
                        return response;
                    }

                    const responseToCache = response.clone();

                    caches.open(CACHE_NAME)
                        .then((cache) => {
                            cache.put(event.request, responseToCache);
                        });

                    return response;
                });
            })
    );
});

// Background sync event
self.addEventListener('sync', (event) => {
    if (event.tag === 'sync-data') {
        event.waitUntil(syncData());
    }
});

// Example function to sync data
async function syncData() {
    console.log('Syncing data...');
}

// Push notification event
self.addEventListener('push', (event) => {
    const options = {
        body: event.data ? event.data.text() : 'اعلان جدید',
        icon: '/favicon.png',
        badge: '/favicon.png',
        vibrate: [100, 50, 100],
        data: {
            dateOfArrival: Date.now(),
            primaryKey: 1
        }
    };

    event.waitUntil(
        self.registration.showNotification('ابزارهای وب', options)
    );
});

// Notification click event
self.addEventListener('notificationclick', (event) => {
    event.notification.close();

    event.waitUntil(
        clients.openWindow('/')
    );
});

script.js:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
// PWA Install Prompt
let deferredPrompt;
const installPromptDismissed = localStorage.getItem('installPromptDismissed');

window.addEventListener('beforeinstallprompt', (e) => {
    e.preventDefault();
    deferredPrompt = e;

    if (!installPromptDismissed) {
        showInstallPrompt();
    }
});

function showInstallPrompt() {
    const prompt = document.createElement('div');
    prompt.className = 'install-prompt';
    prompt.innerHTML = `
        <div class="install-prompt-text">
            <div class="install-prompt-title">📱 نصب اپلیکیشن</div>
        </div>
        <button class="install-btn" id="installBtn">نصب</button>
        <button class="close-install" id="closeInstall">✕</button>
    `;
    document.body.appendChild(prompt);

    document.getElementById('installBtn').addEventListener('click', async () => {
        if (!deferredPrompt) return;

        deferredPrompt.prompt();
        const {outcome} = await deferredPrompt.userChoice;

        if (outcome === 'accepted') {
            console.log('User accepted the install prompt');
        }

        deferredPrompt = null;
        prompt.remove();
    });

    document.getElementById('closeInstall').addEventListener('click', () => {
        localStorage.setItem('installPromptDismissed', 'true');
        prompt.remove();
    });
}

// Register Service Worker
if ('serviceWorker' in navigator) {
    let newWorker;

    window.addEventListener('load', () => {
        navigator.serviceWorker.register('/sw.js')
            .then(registration => {
                console.log('SW registered:', registration);

                // Check for updates periodically
                setInterval(() => {
                    registration.update();
                }, 60000); // Check every minute

                // Listen for waiting worker
                registration.addEventListener('updatefound', () => {
                    newWorker = registration.installing;

                    newWorker.addEventListener('statechange', () => {
                        if (newWorker.state === 'installed' && navigator.serviceWorker.controller) {
                            // New service worker is ready
                            showUpdateNotification();
                        }
                    });
                });
            })
            .catch(err => {
                console.log('SW registration failed:', err);
            });

        // Listen for messages from service worker
        navigator.serviceWorker.addEventListener('message', (event) => {
            if (event.data && event.data.type === 'SW_UPDATED') {
                showUpdateNotification();
            }
        });

        // Handle controller change
        navigator.serviceWorker.addEventListener('controllerchange', () => {
            window.location.reload();
        });
    });

    // Show update notification
    function showUpdateNotification() {
        const notification = document.getElementById('updateNotification');
        if (notification) {
            notification.classList.remove('hidden');
            notification.classList.add('show');
        }
    }

    // Handle update button click
    document.addEventListener('DOMContentLoaded', () => {
        const updateButton = document.getElementById('updateButton');
        const dismissButton = document.getElementById('dismissUpdate');
        const notification = document.getElementById('updateNotification');

        if (updateButton) {
            updateButton.addEventListener('click', () => {
                // Clear all caches and reload
                if ('caches' in window) {
                    caches.keys().then(names => {
                        names.forEach(name => {
                            caches.delete(name);
                        });
                    }).then(() => {
                        // Tell the service worker to skip waiting
                        if (newWorker) {
                            newWorker.postMessage({type: 'SKIP_WAITING'});
                        } else {
                            window.location.reload();
                        }
                    });
                } else {
                    window.location.reload();
                }
            });
        }

        if (dismissButton) {
            dismissButton.addEventListener('click', () => {
                notification.classList.remove('show');
                notification.classList.add('hidden');
            });
        }
    });
}

// Handle app installation
window.addEventListener('appinstalled', () => {
    console.log('App installed successfully');
    deferredPrompt = null;
});

manifest.json:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
{
  "id": "/",
  "scope": "/",
  "name": "Web Tools",
  "short_name": "Web",
  "description": "my description",
  "start_url": "/",
  "display": "fullscreen",
  "background_color": "#0f1730",
  "theme_color": "#7c5cff",
  "orientation": "portrait-primary",
  "lang": "fa",
  "dir": "rtl",
  "display_override": ["fullscreen", "standalone"],
  "prefer_related_applications": false,
  "related_applications": [],
  "icons": [
    {
      "src": "/favicon.png",
      "sizes": "48x48",
      "type": "image/png"
    },
    {
      "src": "/favicon.png",
      "sizes": "192x192",
      "type": "image/png",
      "purpose": "any maskable"
    },
    {
      "src": "/favicon.png",
      "sizes": "512x512",
      "type": "image/png",
      "purpose": "any maskable"
    }
  ],
  "categories": [
    "utilities",
    "productivity",
    "tools"
  ],
  "screenshots": [
    {
      "src": "/screenshots/01.jpg",
      "sizes": "1280x720",
      "type": "image/jpeg",
      "form_factor": "wide",
      "label": "صفحه اصلی"
    },
    {
      "src": "/screenshots/02.jpg",
      "sizes": "540x720",
      "type": "image/jpeg",
      "form_factor": "narrow",
      "label": "لیست ابزارها"
    },
    {
      "src": "/screenshots/03.jpg",
      "sizes": "540x720",
      "type": "image/jpeg",
      "form_factor": "narrow",
      "label": "جزئیات ابزار"
    }
  ],
  "shortcuts": [
    {
      "name": "ابزارهای کاربردی",
      "short_name": "ابزارها",
      "description": "دسترسی سریع به ابزارهای کاربردی",
      "url": "/",
      "icons": [
        {
          "src": "/favicon.png",
          "sizes": "192x192"
        }
      ]
    }
  ],
  "share_target": {
    "action": "/",
    "method": "GET",
    "params": {
      "title": "title",
      "text": "text",
      "url": "url"
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
<!doctype html>
<html lang="fa" dir="rtl">
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
    <meta name="mobile-web-app-capable" content="yes">
    <meta name="apple-mobile-web-app-capable" content="yes">
    <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
    <meta name="theme-color" content="#7c5cff" />
    <meta name="apple-mobile-web-app-capable" content="yes" />
    <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
    <meta name="apple-mobile-web-app-title" content="Web Tools" />
    <link rel="icon" type="image/x-icon" href="/favicon.ico">
    <link rel="shortcut icon" type="image/x-icon" href="/favicon.ico">
    <link rel="apple-touch-icon" href="/favicon.ico" />
    <link rel="manifest" href="/manifest.json" />
</head>

<body>
    <!-- Update Notification -->
    <div id="updateNotification" class="update-notification hidden">
        <div class="update-content">
            <span class="update-icon">🔄</span>
            <div class="update-text">
                <strong>نسخه جدید موجود است!</strong>
                <p>برای استفاده از آخرین نسخه، صفحه را به‌روزرسانی کنید.</p>
            </div>
            <button id="updateButton" class="update-button">به‌روزرسانی</button>
            <button id="dismissUpdate" class="dismiss-button" title="بستن"></button>
        </div>
    </div>

    <script src="script.js"></script>
</body>

</html>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
.install-prompt {
    position: fixed;
    bottom: 20px;
    left: 50%;
    transform: translateX(-50%);
    background: linear-gradient(135deg, var(--accent), var(--accent2));
    color: white;
    padding: 14px 20px;
    border-radius: 16px;
    box-shadow: 0 8px 30px rgba(124, 92, 255, .4);
    display: flex;
    align-items: center;
    gap: 12px;
    z-index: 1000;
    animation: slideUp .3s ease;
    max-width: calc(100% - 30px);
}

@keyframes slideUp {
    from {
        transform: translateX(-50%) translateY(100px);
        opacity: 0;
    }
    to {
        transform: translateX(-50%) translateY(0);
        opacity: 1;
    }
}

.install-prompt-text {
    flex: 1;
    min-width: 0;
}

.install-prompt-title {
    font-weight: 600;
    font-size: 15px;
    margin-bottom: 2px;
}

.install-prompt-desc {
    font-size: 13px;
    opacity: .9;
}

.install-btn {
    background: rgba(255, 255, 255, .25);
    border: 1px solid rgba(255, 255, 255, .3);
    color: white;
    padding: 8px 16px;
    border-radius: 10px;
    cursor: pointer;
    font-family: 'Vazirmatn', sans-serif;
    font-weight: 600;
    font-size: 14px;
    transition: all .2s ease;
    white-space: nowrap;
}

.install-btn:hover {
    background: rgba(255, 255, 255, .35);
    transform: scale(1.05);
}

.close-install {
    background: none;
    border: none;
    color: white;
    cursor: pointer;
    font-size: 20px;
    padding: 4px;
    line-height: 1;
    opacity: .8;
    transition: opacity .2s;
}

.close-install:hover {
    opacity: 1;
}

.update-notification {
    position: fixed;
    bottom: 24px;
    left: 50%;
    transform: translateX(-50%) translateY(150%);
    z-index: 1000;
    width: min(500px, calc(100% - 32px));
    opacity: 0;
    transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
}

.update-notification.show {
    transform: translateX(-50%) translateY(0);
    opacity: 1;
}

.update-notification.hidden {
    display: block;
}

.update-content {
    background: linear-gradient(135deg, var(--accent) 0%, #9b7fff 100%);
    backdrop-filter: blur(20px);
    border-radius: var(--radius);
    padding: 20px 24px;
    box-shadow: 0 12px 40px rgba(124, 92, 255, 0.5),
                0 0 0 1px rgba(255, 255, 255, 0.1);
    display: flex;
    align-items: center;
    gap: 16px;
    position: relative;
    color: white;
}

.update-icon {
    font-size: 32px;
    flex-shrink: 0;
    animation: rotate 2s linear infinite;
}

@keyframes rotate {
    from {
        transform: rotate(0deg);
    }
    to {
        transform: rotate(360deg);
    }
}

.update-text {
    flex: 1;
}

.update-text strong {
    display: block;
    font-size: 16px;
    font-weight: 600;
    margin-bottom: 4px;
}

.update-text p {
    margin: 0;
    font-size: 14px;
    opacity: 0.95;
    line-height: 1.4;
}

.update-button {
    background: white;
    color: var(--accent);
    border: none;
    padding: 10px 20px;
    border-radius: 12px;
    font-family: 'Vazirmatn', sans-serif;
    font-size: 14px;
    font-weight: 600;
    cursor: pointer;
    transition: all 0.2s;
    flex-shrink: 0;
}

.update-button:hover {
    transform: scale(1.05);
    box-shadow: 0 4px 12px rgba(255, 255, 255, 0.3);
}

.update-button:active {
    transform: scale(0.98);
}

.dismiss-button {
    position: absolute;
    top: 8px;
    right: 8px;
    background: rgba(255, 255, 255, 0.2);
    border: none;
    width: 28px;
    height: 28px;
    border-radius: 8px;
    color: white;
    font-size: 16px;
    cursor: pointer;
    display: flex;
    align-items: center;
    justify-content: center;
    transition: all 0.2s;
    padding: 0;
    line-height: 1;
}

.dismiss-button:hover {
    background: rgba(255, 255, 255, 0.3);
}

برای استفاده کافی است این کدها را به سایت فعلی خود اضافه کنید و نام اپ و آیکون را در فایل manifest تغییر دهید.
نمونه اپلیکیشن:

ابزارهای وب - Web Tools