diff --git a/.env.example b/.env.example index 4b0fa8ab..21b9748e 100644 --- a/.env.example +++ b/.env.example @@ -1,3 +1,9 @@ GOOGLE_DRIVE_CLIENT_ID= GOOGLE_DRIVE_API_KEY= API_HOST=http://localhost:3001 +FIREBASE_API_KEY= +FIREBASE_AUTH_DOMAIN= +FIREBASE_PROJECT_ID= +FIREBASE_STORAGE_BUCKET= +FIREBASE_MESSAGING_SENDER_ID= +FIREBASE_APP_ID= diff --git a/package.json b/package.json index 978a6688..06216b23 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "scripts": { "dev": "sapper dev", "build": "sapper build --legacy", - "export": "sapper export --legacy", + "export": "sapper export --legacy && cp __sapper__/build/firebase-messaging-sw.js __sapper__/export", "start": "node __sapper__/build" }, "dependencies": { diff --git a/rollup.config.js b/rollup.config.js index 9ebc6f53..587ee176 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -23,6 +23,11 @@ const onwarn = (warning, onwarn) => (warning.code === 'CIRCULAR_DEPENDENCY' && /[/\\]@sapper[/\\]/.test(warning.message)) || onwarn(warning); +const envData = {}; +Object.entries(envConfig().parsed).forEach(([key, val]) => { + envData[`__paimon.env.${key}`] = `'${val}'`; +}); + export default { client: { input: config.client.input(), @@ -34,9 +39,7 @@ export default { replace({ 'process.browser': true, 'process.env.NODE_ENV': JSON.stringify(mode), - __paimon: JSON.stringify({ - env: envConfig().parsed, - }), + ...envData, }), svelte({ compilerOptions: { @@ -98,9 +101,7 @@ export default { replace({ 'process.browser': false, 'process.env.NODE_ENV': JSON.stringify(mode), - __paimon: JSON.stringify({ - env: envConfig().parsed, - }), + ...envData, }), svelte({ compilerOptions: { @@ -127,6 +128,23 @@ export default { onwarn, }, + serviceworker: { + input: config.serviceworker.input().replace('service-worker', 'firebase-messaging-sw'), + output: { + ...config.serviceworker.output(), + file: config.serviceworker.output().file.replace('service-worker', 'firebase-messaging-sw'), + }, + plugins: [ + replace(envData), + resolve(), + commonjs(), + !dev && terser(), + ], + + preserveEntrySignatures: false, + onwarn, + }, + // serviceworker: { // input: config.serviceworker.input(), // output: config.serviceworker.output(), diff --git a/scripts/createEnv.js b/scripts/createEnv.js index 1e4bd59c..0573fb55 100644 --- a/scripts/createEnv.js +++ b/scripts/createEnv.js @@ -4,5 +4,11 @@ let envString = ''; envString += `GOOGLE_DRIVE_CLIENT_ID=${process.env.GOOGLE_DRIVE_CLIENT_ID}\n`; envString += `GOOGLE_DRIVE_API_KEY=${process.env.GOOGLE_DRIVE_API_KEY}\n`; envString += `API_HOST=${process.env.API_HOST}\n`; +envString += `FIREBASE_API_KEY=${process.env.FIREBASE_API_KEY}\n` +envString += `FIREBASE_AUTH_DOMAIN=${process.env.FIREBASE_AUTH_DOMAIN}\n` +envString += `FIREBASE_PROJECT_ID=${process.env.FIREBASE_PROJECT_ID}\n` +envString += `FIREBASE_STORAGE_BUCKET=${process.env.FIREBASE_STORAGE_BUCKET}\n` +envString += `FIREBASE_MESSAGING_SENDER_ID=${process.env.FIREBASE_MESSAGING_SENDER_ID}\n` +envString += `FIREBASE_APP_ID=${process.env.FIREBASE_APP_ID}\n` fs.writeFileSync('.env', envString); diff --git a/src/components/Input.svelte b/src/components/Input.svelte index 5e0531d0..32fb5912 100644 --- a/src/components/Input.svelte +++ b/src/components/Input.svelte @@ -6,6 +6,7 @@ export let placeholder = ''; export let step = undefined; export let type = 'text'; + export let pattern = undefined; export let min = Math.min(); export let max = Math.max(); @@ -32,6 +33,7 @@ {min} {max} {step} + {pattern} on:change on:input={handleInput} class={`w-full ${icon ? 'pl-12' : 'pl-4'} min-h-full pr-4 text-white placeholder-gray-500 leading-none bg-transparent border-none focus:outline-none`} /> diff --git a/src/firebase-messaging-sw.js b/src/firebase-messaging-sw.js new file mode 100644 index 00000000..39b9fb2d --- /dev/null +++ b/src/firebase-messaging-sw.js @@ -0,0 +1,40 @@ +importScripts('https://www.gstatic.com/firebasejs/8.3.2/firebase-app.js'); +importScripts('https://www.gstatic.com/firebasejs/8.3.2/firebase-messaging.js'); + +const firebaseConfig = { + apiKey: __paimon.env.FIREBASE_API_KEY, + authDomain: __paimon.env.FIREBASE_AUTH_DOMAIN, + projectId: __paimon.env.FIREBASE_PROJECT_ID, + storageBucket: __paimon.env.FIREBASE_STORAGE_BUCKET, + messagingSenderId: __paimon.env.FIREBASE_MESSAGING_SENDER_ID, + appId: __paimon.env.FIREBASE_APP_ID, +}; + +firebase.initializeApp(firebaseConfig); + +const messaging = firebase.messaging(); + +messaging.onBackgroundMessage((payload) => { + console.log('Received background message ', payload); + + const { title, body, url } = payload.data; + + const notificationTitle = title; + const notificationOptions = { + body, + icon: '/favicon.png', + data: { + url, + }, + }; + + self.registration.showNotification(notificationTitle, notificationOptions); +}); + +self.addEventListener('notificationclick', function (event) { + event.notification.close(); + + if (clients.openWindow) { + clients.openWindow(event.notification.data.url); + } +}); diff --git a/src/locales/en.json b/src/locales/en.json index 6a700770..c3d98f7e 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -469,8 +469,22 @@ "trakteer": "Support me on Trakteer" }, "reminder": { - "title": "Reminder Notification", "notSupported": "Your browser does not support push notification, please try other browser!", - "blocked": "Notification is blocked, the reminder notification will not work! Please enable it on your browser." + "blocked": "Notification is blocked, the reminder notification will not work! Please enable it on your browser.", + "desktop": "Desktop browser cannot receive notification if the browser is not running!", + "early": "The notification may arrive earlier (about 1-10 minutes) because of the way we send the notification", + "allowNotification": "Please allow the notification prompt that shows up!", + "transformer": "Parametric Transformer Reminder", + "last": "Enter when you last used the Parametric Transformer", + "countdown": "Enter countdown time of the Parametric Transformer (If you don't remember the exact time, you need to approximate it)", + "useLast": "Use last used time instead", + "useCountdown": "Use countdown time instead", + "set": "Set Reminder", + "checking": "Checking saved reminder...", + "errorSelect": "Please select the day and hour!", + "errorSaving": "Something wrong when saving the reminder 🙁", + "current": "Current reminder", + "hoyolab": "Hoyolab Daily Check-In Reminder", + "comingsoon": "Coming Soon!" } } \ No newline at end of file diff --git a/src/locales/id.json b/src/locales/id.json index 117b5b09..83046ae4 100644 --- a/src/locales/id.json +++ b/src/locales/id.json @@ -466,5 +466,23 @@ ], "kofi": "Support me on Ko-fi", "trakteer": "Support me on Trakteer" + }, + "reminder": { + "notSupported": "Browser mu tidak mensupport notifikasi, silahkan coba browser lain!", + "blocked": "Notifikasi di blokir, notifikasi tidak akan bisa terkirim! Silahkan nyalakan notifikasi di browser mu.", + "desktop": "Browser dekstop tidak bisa menerima notifikasi jika browser tidak berjalan!", + "early": "Notifikasi mungkin akan muncul lebih awal (sekitar 1-10 menit) karena limitasi dalam pengiriman notifikasi.", + "transformer": "Reminder Parametric Transformer", + "last": "Masukkan kapan kamu terakhir menggunakan Parametric Transformer", + "countdown": "Masukkan waktu countdown Parametric Transformer (jika kamu tidak ingat waktu pasti nya, kamu perlu mengira-ngira-kannya)", + "useLast": "Gunakan waktu terakhir menggunakan", + "useCountdown": "Gunakan waktu countdown", + "set": "Set Reminder", + "checking": "Mengecek reminder tersimpan...", + "errorSelect": "Silahkan pilih sisa hari dan jam nya!", + "errorSaving": "Error saat menyimpan reminder 🙁", + "current": "Reminder saat ini", + "hoyolab": "Reminder Hoyolab Daily Check-In", + "comingsoon": "Coming Soon!" } } \ No newline at end of file diff --git a/src/routes/_index/reminder.svelte b/src/routes/_index/reminder.svelte index bd64976d..fa7e6242 100644 --- a/src/routes/_index/reminder.svelte +++ b/src/routes/_index/reminder.svelte @@ -7,11 +7,13 @@

{$t('home.reminder.message')}

-
{$t('home.reminder.detail')} -
+
diff --git a/src/routes/index.svelte b/src/routes/index.svelte index ef568a14..2ded283e 100644 --- a/src/routes/index.svelte +++ b/src/routes/index.svelte @@ -16,7 +16,6 @@ let refreshLayout; const onDone = debounce(() => { - console.log('refresh'); refreshLayout(); }, 100); @@ -42,10 +41,10 @@ - + + - diff --git a/src/routes/reminder.svelte b/src/routes/reminder.svelte new file mode 100644 index 00000000..3e94eaf8 --- /dev/null +++ b/src/routes/reminder.svelte @@ -0,0 +1,263 @@ + + + + Paimon.moe + + + +
+
+ {#if $loadingFirst} + + {:else if !$notificationSupported} +
+ {$t('reminder.notSupported')} +
+ {:else if !$notificationAllowed} +
+ {$t('reminder.blocked')} +
+ {:else} +
+
+
+ parametric transformer +

{$t('reminder.transformer')}

+
+
+ {#if loadingCurrent} +
+ + {$t('reminder.checking')} +
+ {/if} + {#if !loadingCurrent && currentReminder !== null} +
+

{$t('reminder.current')}

+

{currentReminder.format('YYYY-MM-DD HH:mm')} ({currentReminder.fromNow()})

+ +
+ {/if} +

+ {$t(useType === 'last' ? 'reminder.last' : 'reminder.countdown')} +

+ {#if useType === 'last'} + + {:else} +
+ +
+ {/if} + + + {#if $loading} +

{$t('reminder.allowNotification')}

+ {/if} +
+
+
+
+ +

{$t('reminder.hoyolab')}

+
+
+ {$t('reminder.comingsoon')} +
+
+
+
+ {$t('reminder.early')} +
+ +
+
+ {/if} +
+
+ + diff --git a/src/routes/wish/tally/_item.svelte b/src/routes/wish/tally/_item.svelte index d429b490..71e921d1 100644 --- a/src/routes/wish/tally/_item.svelte +++ b/src/routes/wish/tally/_item.svelte @@ -8,6 +8,7 @@ import relativeTime from 'dayjs/plugin/relativeTime'; import 'dayjs/locale/id'; import 'dayjs/locale/en'; + import 'dayjs/locale/ru'; dayjs.extend(duration); dayjs.extend(relativeTime); diff --git a/src/server.js b/src/server.js index 688096a3..07aec8c2 100644 --- a/src/server.js +++ b/src/server.js @@ -2,19 +2,50 @@ import sirv from 'sirv'; import polka from 'polka'; import compression from 'compression'; import * as sapper from '@sapper/server'; +import fs from 'fs'; +import path from 'path'; import { i18nMiddleware } from './i18n.js'; const { PORT, NODE_ENV } = process.env; const dev = NODE_ENV === 'development'; +function serve(pathname) { + const filter = (req) => req.path === pathname; + const read = (file) => fs.readFileSync(path.join('__sapper__/dev', file)); + + return (req, res, next) => { + if (filter(req)) { + try { + const file = path.posix.normalize(decodeURIComponent(req.path)); + const data = read(file); + res.setHeader('Content-Type', 'text/javascript'); + res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate'); + res.end(data); + } catch (err) { + if (err.code === 'ENOENT') { + next(); + } else { + console.error(err); + res.statusCode = 500; + res.end('an error occurred while reading a static file from disk'); + } + } + } else { + next(); + } + }; +} + polka() // You can also use Express - .use( - compression({ threshold: 0 }), - sirv('static', { dev }), + .use( + compression({ threshold: 0 }), + sirv('static', { dev }), i18nMiddleware(), - sapper.middleware() - ) - .listen(PORT, err => { - if (err) console.log('error', err); - }); + serve('/firebase-messaging-sw.js'), + serve('/firebase-messaging-sw.js.map'), + sapper.middleware(), + ) + .listen(PORT, (err) => { + if (err) console.log('error', err); + }); diff --git a/src/stores/firebase.js b/src/stores/firebase.js new file mode 100644 index 00000000..e1f23b7b --- /dev/null +++ b/src/stores/firebase.js @@ -0,0 +1,117 @@ +import { writable } from 'svelte/store'; + +export const firebaseToken = writable(''); +export const loadingFirst = writable(true); +export const loading = writable(false); +export const notificationSupported = writable(false); +export const notificationAllowed = writable(true); + +const firebaseConfig = { + apiKey: __paimon.env.FIREBASE_API_KEY, + authDomain: __paimon.env.FIREBASE_AUTH_DOMAIN, + projectId: __paimon.env.FIREBASE_PROJECT_ID, + storageBucket: __paimon.env.FIREBASE_STORAGE_BUCKET, + messagingSenderId: __paimon.env.FIREBASE_MESSAGING_SENDER_ID, + appId: __paimon.env.FIREBASE_APP_ID, +}; + +let messaging; + +export async function firstLoadNotification() { + console.log('first load notification'); + + const isSupported = 'Notification' in window; + notificationSupported.set(isSupported); + loadingFirst.set(false); + + if (!isSupported) { + return; + } + + if (Notification.permission === 'granted') { + await initFirebase(); + } else if (Notification.permission === 'denied') { + notificationAllowed.set(false); + } +} + +export async function requestPermission() { + console.log('request permission'); + + if (Notification.permission === 'granted') { + if (messaging) { + await getToken(); + } else { + await initFirebase(); + } + } else { + loading.set(true); + + const result = await Notification.requestPermission(); + + const allowed = result === 'granted'; + notificationAllowed.set(allowed); + + if (allowed) { + await initFirebase(); + } else { + loading.set(false); + } + } +} + +async function initFirebase() { + console.log('init firebase'); + + if (!messaging) { + firebase.initializeApp(firebaseConfig); + messaging = firebase.messaging(); + } + + await getToken(); +} + +async function getToken() { + console.log('request token'); + + try { + const token = await messaging.getToken({ + vapidKey: 'BA6niiIWa_QP2SXMTjS8gBtM3M7m0q0n0_ZWjECw3Z_iEFujzPG2VdAAvNFJ5btbgpEiRe2B80M4QKxRSxtmvDw', + }); + + if (token) { + firebaseToken.set(token); + loading.set(false); + console.log(token); + handleForegroundNotification(); + return token; + } else { + await requestPermission(); + } + } catch (err) { + console.error(err); + } +} + +function handleForegroundNotification() { + console.log('handle foreground notification'); + + messaging.onMessage((payload) => { + console.log('Received foreground message ', payload); + + navigator.serviceWorker.getRegistration('/firebase-cloud-messaging-push-scope').then((registration) => { + const { title, body, url } = payload.data; + + const notificationTitle = title; + const notificationOptions = { + body, + icon: '/favicon.png', + data: { + url, + }, + }; + + registration.showNotification(notificationTitle, notificationOptions); + }); + }); +} diff --git a/src/template.html b/src/template.html index 412b18c0..00f9764d 100644 --- a/src/template.html +++ b/src/template.html @@ -21,6 +21,9 @@ + + +