Никита Дубко, HR Tech Яндекса
Никита Дубко, HR Tech Яндекса
// app.manifest
"related_applications": [
{
"platform": "webapp",
"url": "https://my.app/app.webmanifest"
}
],
if ('getInstalledRelatedApps' in navigator) {
const relatedApps =
await navigator.getInstalledRelatedApps();
const PWAisInstalled = relatedApps.length > 0;
if (PWAisInstalled) {
installButton.classList.add('hidden');
}
}
PWA Detection
@media (display-mode: standalone),
(display-mode: window-controls-overlay) {
.pwa-install-button {
display: none;
}
}
// app.manifest
"short_name": "D&D Tokenizer",
"description": "Some description.",
"screenshots": [
{
"src": "./screens/desktop.jpg",
"type": "image/jpeg",
"sizes": "800x583",
"form_factor": "wide"
},
{
"src": "./screens/mobile.jpg",
"type": "image/jpeg",
"sizes": "530x700",
"form_factor": "narrow"
}
],
// app.manifest
"share_target": {
"action": "/?share-target",
"method": "POST",
"enctype": "multipart/form-data",
"params": {
"files": [
{
"name": "image",
"accept": ["image/*"]
}
]
}
},
// sw.js
self.addEventListener('fetch', (fetchEvent) => {
const url = new URL(fetchEvent.request.url);
if (
url.pathname === '/' &&
url.searchParams.has('share-target') &&
fetchEvent.request.method === 'POST'
) {
return fetchEvent.respondWith(
(async () => {
const formData = await fetchEvent.request.formData();
const image = formData.get('image');
const keys = await caches.keys();
const sharedCache = await caches.open(
keys.filter((key) => key.startsWith('share-target'))[0]
);
await sharedCache.put('shared-image', new Response(image));
return Response.redirect('./?share-target', 303);
})()
);
}
});
Receiving shared data with the Web Share Target API
// main.js
window.addEventListener('load', async () => {
if (location.search.includes('share-target')) {
const keys = await caches.keys();
const sharedCache = await caches.open(
keys.filter((key) => key.startsWith('share-target'))[0]
);
const image = await sharedCache.match('shared-image');
if (image) {
const blob = await image.blob();
await sharedCache.delete('shared-image');
// do something with blob
}
}
});
Receiving shared data with the Web Share Target API
const shareButton = document.querySelector('.button');
if (navigator.canShare) {
shareButton.addEventListener('click', async () => {
const blob = getBlobFromImage();
const file = new File([blob], 'token.png', {
type: 'image/png',
});
const data = {
files: [file],
};
if (navigator.canShare(data)) {
try {
await navigator.share(data);
} catch (err) {
if (err.name !== 'AbortError') {
console.error(err.name, err.message);
}
}
}
});
}
Integrate with the OS sharing UI with the Web Share API
document.addEventListener('paste', async (e) => {
e.preventDefault();
const clipboardItems = typeof navigator?.clipboard?.read === 'function'
? await navigator.clipboard.read()
: e.clipboardData.files;
for (const clipboardItem of clipboardItems) {
let blob;
if (clipboardItem.type?.startsWith('image/')) {
blob = clipboardItem;
// do something with blob
} else {
const imageTypes = clipboardItem.types?.filter((type) =>
type.startsWith('image/')
);
for (const imageType of imageTypes) {
blob = await clipboardItem.getType(imageType);
// do something with blob
}
}
}
});
Unblocking clipboard access
// app.manifest
"display": "standalone",
"display_override": ["window-controls-overlay"],
/* style.css */
@media (display-mode: window-controls-overlay) {
.header {
padding-right: calc(2 * env(titlebar-area-x));
padding-left: env(titlebar-area-x);
height: calc(env(titlebar-area-height) + 10px);
}
}
.header__title {
-webkit-app-region: drag;
}
Window Controls Overlay for Installed Desktop Web Apps
const supportsFileSystemAccess =
'showOpenFilePicker' in window &&
(() => {
try {
return window.self === window.top;
} catch {
return false;
}
})();
if (supportsFileSystemAccess) {
let fileHandle = undefined;
try {
[fileHandle] = await showOpenFilePicker();
return await fileHandle.getFile();
} catch (err) {
if (err.name !== 'AbortError') {
console.error(err.name, err.message);
}
}
}
Reading and writing files and directories with the browser-fs-access library
let promise = navigator.runOnOsLogin.set({
mode: "windowed",
});
promise.then(function() {
// Пользователь дал добро
},
function(reason) {
// Что-то пошло не так
});
Run on OS Login
const eyeDropper = new EyeDropper();
try {
const result = await eyeDropper.open();
// Пользователь выбрал пиксель с цветом:
const colorHexValue = result.sRGBHex;
} catch (err) {
// Пользователь выключил пипетку
}
// app.manifest
"protocol_handlers": [
{
"protocol": "web+tokenizer",
"url": "/?from=%s"
}
],
URL protocol handler registration for PWAs
// app.manifest
"capture_links": "existing_client_event",
"url_handlers": [
{
"origin": "https://dnd-tokenizer-41471e.netlify.app"
}
]
Test
PWAs as URL Handlers
// app.manifest
"shortcuts": [
{
"name": "Create new token",
"url": "/"
}
],
"launch_handler": {
"client_mode": "focus-existing"
},
Shortcuts Explainer
// app.manifest
"file_handlers": [
{
"action": "/",
"accept": {
"image/*": [
".jpg", ".jpeg", ".webp",
".png", ".avif", ".gif"
]
}
}
],
Handle files in Progressive Web Apps
// main.js
if ('launchQueue' in window) {
launchQueue.setConsumer((launchParams) => {
const files = launchParams.files;
for (const file of files) {
const blob = await file.getFile();
blob.handle = file;
// Сделать что-то с файлом
}
});
} else {
console.error('File Handling API is not supported!');
}
const opfsRoot = await navigator.storage.getDirectory();
const fileHandle = await opfsRoot
.getFileHandle('file', { create: true });
const directoryHandle = await opfsRoot
.getDirectoryHandle('folder', { create: true });
const nestedFileHandle = await directoryHandle
.getFileHandle('nested file', { create: true });
const nestedDirectoryHandle = await directoryHandle
.getDirectoryHandle('nested folder', { create: true });
The origin private file system
try {
const availableFonts = await window.queryLocalFonts();
const list = document.querySelector('select.fonts');
for (const fontData of availableFonts) {
const option = document.createElement('option');
option.text = fontData.fullName;
option.value = fontData.postscriptName;
if (fontData.fullName === 'Times New Roman') {
option.selected = true;
}
list?.appendChild(option);
}
} catch (err) {
console.error(err.name, err.message);
}
Use advanced typography with local fonts