@preference-sl/pref-viewer 2.11.0-beta.1 → 2.11.0-beta.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,39 @@
1
+ /* Variables */
2
+ pref-viewer-2d {
3
+ --pref-viewer-2d-bg-color: #ffffff;
4
+ --pref-viewer-2d-svg-padding: 10px;
5
+ }
6
+
7
+ pref-viewer-2d[visible="true"] {
8
+ display: block;
9
+ }
10
+
11
+ pref-viewer-2d[visible="false"] {
12
+ display: none;
13
+ }
14
+
15
+ pref-viewer-2d {
16
+ grid-column: 1;
17
+ grid-row: 1;
18
+ overflow: hidden;
19
+ min-width: 0;
20
+ min-height: 0;
21
+ align-self: stretch;
22
+ justify-self: stretch;
23
+ background: var(--pref-viewer-2d-bg-color);
24
+ }
25
+
26
+ pref-viewer-2d,
27
+ pref-viewer-2d>div,
28
+ pref-viewer-2d>div>svg {
29
+ width: 100%;
30
+ height: 100%;
31
+ display: block;
32
+ position: relative;
33
+ outline: none;
34
+ box-sizing: border-box;
35
+ }
36
+
37
+ pref-viewer-2d>div>svg {
38
+ padding: var(--pref-viewer-2d-svg-padding);
39
+ }
@@ -0,0 +1,28 @@
1
+ pref-viewer-3d[visible="true"] {
2
+ display: block;
3
+ }
4
+
5
+ pref-viewer-3d[visible="false"] {
6
+ display: none;
7
+ }
8
+
9
+ pref-viewer-3d {
10
+ grid-column: 1;
11
+ grid-row: 1;
12
+ overflow: hidden;
13
+ min-width: 0;
14
+ min-height: 0;
15
+ align-self: stretch;
16
+ justify-self: stretch;
17
+ }
18
+
19
+ pref-viewer-3d,
20
+ pref-viewer-3d>div,
21
+ pref-viewer-3d>div>canvas {
22
+ width: 100%;
23
+ height: 100%;
24
+ display: block;
25
+ position: relative;
26
+ outline: none;
27
+ box-sizing: border-box;
28
+ }
@@ -0,0 +1,105 @@
1
+ /* Variables */
2
+ pref-viewer-dialog {
3
+ --brand-color: #ff6700;
4
+ --dialog-general-space: 16px;
5
+ --dialog-bg-color: #ffffff;
6
+ --dialog-backdrop-color: rgba(0, 0, 0, 0.25);
7
+ --dialog-border-color: #e7e7e7;
8
+ --dialog-border-radius: 8px;
9
+ --dialog-box-shadow: 0 4px 24px rgba(0, 0, 0, 0.25);
10
+ --button-default-bg-color: #bbbbbb;
11
+ --button-default-bg-color-hover: #a1a1a1;
12
+ --button-primary-bg-color: color-mix(in oklab, var(--brand-color), white 25%);
13
+ --button-primary-bg-color-hover: var(--brand-color);
14
+ --button-border-radius: 4px;
15
+ --button-padding-horizontal: 16px;
16
+ --button-padding-vertical: 8px;
17
+ }
18
+
19
+ pref-viewer-dialog:not {
20
+ display: none;
21
+ }
22
+
23
+ pref-viewer-dialog[open] {
24
+ font-family: 'Roboto', ui-sans-serif, system-ui, sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol, Noto Color Emoji;
25
+ grid-row: 1;
26
+ grid-column: 1;
27
+ overflow: hidden;
28
+ min-width: 0;
29
+ min-height: 0;
30
+ align-self: stretch;
31
+ justify-self: stretch;
32
+ display: flex;
33
+ align-items: center;
34
+ justify-content: center;
35
+ background-color: var(--dialog-backdrop-color);
36
+ position: relative;
37
+ z-index: 1000;
38
+ }
39
+
40
+ pref-viewer-dialog>.dialog-wrapper {
41
+ display: flex;
42
+ flex-direction: column;
43
+ align-items: stretch;
44
+ background: var(--dialog-bg-color);
45
+ border: 1px solid var(--dialog-border-color);
46
+ border-radius: var(--dialog-border-radius);
47
+ box-shadow: var(--dialog-box-shadow);
48
+ padding: 0;
49
+ min-width: 320px;
50
+ max-width: 90%;
51
+ max-height: 90%;
52
+ overflow: auto;
53
+ }
54
+
55
+ pref-viewer-dialog .dialog-header {
56
+ padding: var(--dialog-general-space);
57
+ border-bottom: 1px solid var(--dialog-border-color);
58
+ }
59
+
60
+ pref-viewer-dialog .dialog-header h3 {
61
+ margin: 0;
62
+ font-weight: 500;
63
+ font-size: 1.1em;
64
+ }
65
+
66
+ pref-viewer-dialog .dialog-content {
67
+ padding: var(--dialog-general-space);
68
+ }
69
+
70
+ pref-viewer-dialog .dialog-content h4 {
71
+ margin: 0;
72
+ font-weight: 500;
73
+ font-size: 1.05em;
74
+ }
75
+
76
+ pref-viewer-dialog .dialog-footer {
77
+ border-top: 1px solid var(--dialog-border-color);
78
+ padding: var(--dialog-general-space);
79
+ display: flex;
80
+ gap: var(--dialog-general-space);
81
+ justify-content: stretch;
82
+ }
83
+
84
+ pref-viewer-dialog .dialog-footer button {
85
+ width: 100%;
86
+ font-size: 1em;
87
+ padding: var(--button-padding-vertical) var(--button-padding-horizontal);
88
+ border-radius: var(--button-border-radius);
89
+ border: none;
90
+ background: var(--button-default-bg-color);
91
+ cursor: pointer;
92
+ transition: background 0.2s;
93
+ }
94
+
95
+ pref-viewer-dialog .dialog-footer button.primary {
96
+ background: var(--button-primary-bg-color);
97
+ }
98
+
99
+ pref-viewer-dialog .dialog-footer button:hover {
100
+ background: var(--button-default-bg-color-hover);
101
+ }
102
+
103
+ pref-viewer-dialog .dialog-footer button.primary:hover {
104
+ background: var(--button-primary-bg-color-hover);
105
+ }
@@ -0,0 +1,11 @@
1
+ :host .pref-viewer-wrapper {
2
+ display: grid !important;
3
+ position: relative;
4
+ width: 100%;
5
+ height: 100%;
6
+ grid-template-columns: 1fr;
7
+ grid-template-rows: 1fr;
8
+ grid-gap: 0;
9
+ min-width: 0;
10
+ min-height: 0;
11
+ }
@@ -1,12 +1,119 @@
1
- /**
2
- * FileStore class to manage file storage in IndexedDB using a promise-based wrapper around the IndexedDB API.
3
- * @see {@link https://github.com/jakearchibald/idb|GitHub - jakearchibald/idb: IndexedDB, but with promises}
4
- * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API|IndexedDB API - MDN}
5
- * @see {@link https://www.npmjs.com/package/idb|idb - npm}
6
- */
7
-
8
1
  import { openDB } from "idb";
9
2
 
3
+ /**
4
+ * FileStorage - Class for managing file storage in IndexedDB with server synchronization.
5
+ *
6
+ * Overview:
7
+ * - Provides a promise-based wrapper around the IndexedDB API for file caching.
8
+ * - Automatically manages cache versioning by comparing server and cached file timestamps.
9
+ * - Falls back to direct server access when IndexedDB is unavailable.
10
+ * - Supports file blob retrieval, timestamp checking, and size queries.
11
+ * - Uses the idb library for simplified IndexedDB operations.
12
+ *
13
+ * Dependencies:
14
+ * - idb (IndexedDB wrapper library)
15
+ * - Modern browser with IndexedDB support
16
+ *
17
+ * Constructor:
18
+ * - FileStorage(dbName, osName): Creates a new FileStorage instance.
19
+ * Parameters:
20
+ * - dbName: Name of the IndexedDB database (default: "FilesDB")
21
+ * - osName: Name of the object store (default: "FilesObjectStore")
22
+ *
23
+ * Private Methods:
24
+ * - #createObjectStore(db, osName): Creates the object store if it doesn't exist.
25
+ * - #openDB(): Opens or creates the IndexedDB database.
26
+ * - #getServerFile(uri): Downloads file blob and timestamp from server.
27
+ * - #getServerFileTimeStamp(uri): Retrieves Last-Modified timestamp via HEAD request.
28
+ * - #getServerFileSize(uri): Retrieves Content-Length via HEAD request.
29
+ * - #putFile(file, uri): Stores file record in IndexedDB.
30
+ * - #getFile(uri): Retrieves file record from IndexedDB.
31
+ *
32
+ * Public Methods:
33
+ * - getURL(uri): Gets an object URL for cached blob or original URI.
34
+ * - getTimeStamp(uri): Retrieves cached or server Last-Modified timestamp.
35
+ * - getSize(uri): Gets cached or server file size in bytes.
36
+ * - getBlob(uri): Retrieves file blob from cache or server.
37
+ * - get(uri): Gets file from cache with automatic server sync and cache versioning.
38
+ * - put(uri): Stores file from server in IndexedDB cache.
39
+ *
40
+ * Features:
41
+ * - Automatic Cache Versioning: Compares server and cached timestamps to update cache.
42
+ * - Fallback Support: Uses direct server access when IndexedDB is unavailable.
43
+ * - Object URL Generation: Creates efficient object URLs for cached blobs.
44
+ * - HEAD Request Optimization: Uses HEAD requests to check metadata without downloading full files.
45
+ * - Promise-Based API: All operations return promises for async/await usage.
46
+ *
47
+ * Usage Example:
48
+ * const storage = new FileStorage("MyFilesDB", "CachedFiles");
49
+ *
50
+ * // Get file blob with automatic caching
51
+ * const file = await storage.get("https://example.com/model.glb");
52
+ *
53
+ * // Get object URL for cached file
54
+ * const url = await storage.getURL("https://example.com/model.glb");
55
+ *
56
+ * // Check file timestamp
57
+ * const timestamp = await storage.getTimeStamp("https://example.com/model.glb");
58
+ *
59
+ * // Get file size
60
+ * const size = await storage.getSize("https://example.com/model.glb");
61
+ *
62
+ * // Manually cache a file
63
+ * await storage.put("https://example.com/model.glb");
64
+ *
65
+ * Cache Behavior:
66
+ * - First call: Downloads from server and caches in IndexedDB
67
+ * - Subsequent calls: Returns cached version if timestamp matches
68
+ * - Updated file: Detects timestamp change and re-downloads automatically
69
+ * - No server access: Returns cached version if available
70
+ * - IndexedDB unavailable: Falls back to direct server access
71
+ *
72
+ * Return Values:
73
+ * - get(uri): Blob | undefined | false
74
+ * - Blob: File successfully retrieved
75
+ * - undefined: Download failed but no cached copy exists
76
+ * - false: URI is falsy
77
+ *
78
+ * - getURL(uri): string | boolean
79
+ * - string: Object URL or original URI
80
+ * - false: File not available
81
+ *
82
+ * - getTimeStamp(uri): string | null
83
+ * - string: ISO 8601 timestamp
84
+ * - null: Timestamp unavailable
85
+ *
86
+ * - getSize(uri): number | null
87
+ * - number: File size in bytes
88
+ * - null: Size unavailable
89
+ *
90
+ * - put(uri): boolean
91
+ * - true: Successfully cached
92
+ * - false: Failed to cache
93
+ *
94
+ * Storage Limits:
95
+ * - Chrome/Edge: ~50GB per origin
96
+ * - Firefox: ~1GB per origin
97
+ * - Safari: ~1GB per origin
98
+ *
99
+ * Notes:
100
+ * - Requires CORS headers for cross-origin file access
101
+ * - Uses XMLHttpRequest for file downloads and HEAD requests
102
+ * - Supports both HTTPS and HTTP (HTTP not recommended for production)
103
+ * - Object URLs should be revoked after use to free memory
104
+ * - IndexedDB quota management should be implemented for long-term storage
105
+ *
106
+ * Error Handling:
107
+ * - Network failures: Returns undefined (get) or false (other methods)
108
+ * - IndexedDB errors: Falls back to server access or returns false
109
+ * - CORS errors: Treated as download failures
110
+ * - Invalid URIs: Returns false or undefined
111
+ *
112
+ * References:
113
+ * - MDN IndexedDB API: https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API
114
+ * - idb Library: https://github.com/jakearchibald/idb
115
+ * - npm idb: https://www.npmjs.com/package/idb
116
+ */
10
117
  export class FileStorage {
11
118
  #supported = "indexedDB" in window && openDB; // true if IndexedDB is available in the browser and the idb helper is loaded
12
119
  #dbVersion = 1; // single DB version; cache is managed per-file
@@ -19,19 +126,22 @@ export class FileStorage {
19
126
 
20
127
  /**
21
128
  * Create the object store if it does not exist.
129
+ * @private
22
130
  * @param {IDBDatabase} db IndexedDB database instance
131
+ * @param {String} osName Object store name
23
132
  */
24
- #createObjectStore = (db, osName) => {
133
+ #createObjectStore(db, osName) {
25
134
  if (!db.objectStoreNames.contains(osName)) {
26
135
  db.createObjectStore(osName);
27
136
  }
28
- };
137
+ }
29
138
 
30
139
  /**
31
140
  * Open the IndexedDB database (creating it if needed) and ensure the object store for files exists.
141
+ * @private
32
142
  * @returns {Promise<IDBDatabase|null>} Resolves with the opened database or null if IndexedDB is not supported or opening fails.
33
143
  */
34
- #openDB = async () => {
144
+ async #openDB() {
35
145
  if (!this.#supported) {
36
146
  return null;
37
147
  }
@@ -45,16 +155,17 @@ export class FileStorage {
45
155
  } catch (error) {
46
156
  return null;
47
157
  }
48
- };
158
+ }
49
159
 
50
160
  /**
51
161
  * Download the file from the server (forced download).
162
+ * @private
52
163
  * @param {String} uri File URI
53
164
  * @returns {Promise<{blob: Blob, timeStamp: string}|undefined>} Resolves with:
54
165
  * - { blob, timeStamp }: object with the downloaded Blob and the Last-Modified timestamp (ISO string) on success
55
166
  * - undefined: if the download fails (non-200 status or network error)
56
167
  */
57
- #getServerFile = async (uri) => {
168
+ async #getServerFile(uri) {
58
169
  let file = undefined;
59
170
  return new Promise((resolve) => {
60
171
  const xhr = new XMLHttpRequest();
@@ -75,16 +186,17 @@ export class FileStorage {
75
186
  };
76
187
  xhr.send();
77
188
  });
78
- };
189
+ }
79
190
 
80
191
  /**
81
192
  * Get the Last-Modified timestamp of the file on the server without downloading its content.
193
+ * @private
82
194
  * @param {String} uri File URI
83
195
  * @returns {Promise<string|null>} Resolves with:
84
196
  * - ISO 8601 string from the Last-Modified header if available
85
197
  * - null if the header is not present or on error
86
198
  */
87
- #getServerFileTimeStamp = async (uri) => {
199
+ async #getServerFileTimeStamp(uri) {
88
200
  let timeStamp = null;
89
201
  return new Promise((resolve) => {
90
202
  const xhr = new XMLHttpRequest();
@@ -103,16 +215,17 @@ export class FileStorage {
103
215
  };
104
216
  xhr.send();
105
217
  });
106
- };
218
+ }
107
219
 
108
220
  /**
109
221
  * Get the size of the file on the server without downloading its content.
222
+ * @private
110
223
  * @param {String} uri File URI
111
224
  * @returns {Promise<number>} Promise that resolves with:
112
225
  * - number: Content-Length in bytes when the HEAD request succeeds and the header is present
113
226
  * - 0: when the header is missing or the request fails
114
227
  */
115
- #getServerFileSize = async (uri) => {
228
+ async #getServerFileSize(uri) {
116
229
  let size = 0;
117
230
  return new Promise((resolve) => {
118
231
  const xhr = new XMLHttpRequest();
@@ -131,10 +244,11 @@ export class FileStorage {
131
244
  };
132
245
  xhr.send();
133
246
  });
134
- };
247
+ }
135
248
 
136
249
  /**
137
250
  * Stores a file record in IndexedDB under the provided key (uri).
251
+ * @private
138
252
  * @param {Object|Blob} file The value to store (for example an object {blob, timeStamp} or a Blob).
139
253
  * @param {String} uri Key to use for storage (the original URI).
140
254
  * @returns {Promise<any|undefined>} Resolves with the value returned by the underlying idb store.put (usually the record key — string or number)
@@ -142,7 +256,7 @@ export class FileStorage {
142
256
  * - resolves to undefined if the database could not be opened
143
257
  * - rejects if the underlying put operation throws
144
258
  */
145
- #putFile = async (file, uri) => {
259
+ async #putFile(file, uri) {
146
260
  if (this.#db === undefined) {
147
261
  this.#db = await this.#openDB();
148
262
  }
@@ -152,17 +266,18 @@ export class FileStorage {
152
266
  const transation = this.#db.transaction(this.osName, "readwrite");
153
267
  const store = transation.objectStore(this.osName);
154
268
  return store.put(file, uri);
155
- };
269
+ }
156
270
 
157
271
  /**
158
272
  * Retrieve a file record from IndexedDB.
273
+ * @private
159
274
  * @param {String} uri File URI
160
275
  * @returns {Promise<{blob: Blob, timeStamp: string}|undefined|false>} Resolves with:
161
276
  * - {blob, timeStamp}: object with the Blob and ISO timestamp if the entry exists in IndexedDB
162
277
  * - undefined: if there is no stored entry for that URI
163
278
  * - false: if the database could not be opened (IndexedDB unsupported or open failure)
164
279
  */
165
- #getFile = async (uri) => {
280
+ async #getFile(uri) {
166
281
  if (this.#db === undefined) {
167
282
  this.#db = await this.#openDB();
168
283
  }
@@ -172,75 +287,86 @@ export class FileStorage {
172
287
  const transation = this.#db.transaction(this.osName, "readonly");
173
288
  const store = transation.objectStore(this.osName);
174
289
  return store.get(uri);
175
- };
290
+ }
291
+
292
+ /**
293
+ * ---------------------------
294
+ * Public methods
295
+ * ---------------------------
296
+ */
176
297
 
177
298
  /**
178
299
  * Get a URL to the file: either an object URL for the cached Blob, or the original URI if the server copy is available and IndexedDB isn't supported.
179
- * @param {String} uri File URI
300
+ * @public
301
+ * @param {String} uri - File URI
180
302
  * @returns {Promise<string|boolean>} Resolves with:
181
303
  * - object URL string for the cached Blob
182
304
  * - original URI string if IndexedDB unsupported but server file exists
183
305
  * - false if the file is not available on the server
184
306
  */
185
- getURL = async (uri) => {
307
+ async getURL(uri) {
186
308
  if (!this.#supported) {
187
309
  return (await this.#getServerFileTimeStamp(uri)) ? uri : false;
188
310
  }
189
311
  const storedFile = await this.get(uri);
190
312
  return storedFile ? URL.createObjectURL(storedFile.blob) : (await this.#getServerFileTimeStamp(uri)) ? uri : false;
191
- };
313
+ }
192
314
 
193
315
  /**
194
316
  * Get the cached or server Last-Modified timestamp for a file.
195
- * @param {String} uri File URI
317
+ * @public
318
+ * @param {String} uri - File URI
196
319
  * @returns {Promise<string|null>} Resolves with:
197
320
  * - ISO 8601 string: Last-Modified timestamp from the cached record (when using IndexedDB) or from the server HEAD response
198
321
  * - null: when no timestamp is available or on error
199
322
  */
200
- getTimeStamp = async (uri) => {
323
+ async getTimeStamp(uri) {
201
324
  if (!this.#supported) {
202
325
  const serverFileTimeStamp = await this.#getServerFileTimeStamp(uri);
203
326
  return serverFileTimeStamp ? serverFileTimeStamp : null;
204
327
  }
205
328
  const storedFile = await this.get(uri);
206
329
  return storedFile ? storedFile.timeStamp : null;
207
- };
330
+ }
208
331
 
209
332
  /**
210
333
  * Get the cached or server size of a file in bytes.
211
- * @param {String} uri File URI
334
+ * @public
335
+ * @param {String} uri - File URI
212
336
  * @returns {Promise<number|null>} Resolves with:
213
337
  * - number: size in bytes when available (from cache or server)
214
338
  * - null: when size is not available or on error
215
339
  */
216
- getSize = async (uri) => {
340
+ async getSize(uri) {
217
341
  if (!this.#supported) {
218
342
  const serverFileSize = await this.#getServerFileSize(uri);
219
343
  return serverFileSize ? serverFileSize : null;
220
344
  }
221
345
  const storedFile = await this.get(uri);
222
346
  return storedFile ? storedFile.blob.size : null;
223
- };
347
+ }
224
348
 
225
349
  /**
226
350
  * Retrieve the Blob for a file from cache or server.
227
- * @param {String} uri File URI
351
+ * @public
352
+ * @param {String} uri - File URI
228
353
  * @returns {Promise<Blob|false>} Resolves with:
229
354
  * - Blob: the file blob when available (from cache or server)
230
355
  * - false: when the file cannot be retrieved
231
356
  */
232
- getBlob = async (uri) => {
357
+ async getBlob(uri) {
233
358
  if (!this.#supported) {
234
359
  const serverFile = await this.#getServerFile(uri);
235
360
  return serverFile ? serverFile.blob : false;
236
361
  }
237
362
  const storedFile = await this.get(uri);
238
363
  return storedFile ? storedFile.blob : false;
239
- };
364
+ }
240
365
 
241
366
  /**
242
367
  * Get the file Blob from IndexedDB if stored; otherwise download and store it, then return the Blob.
243
- * @param {String} uri File URI
368
+ * @public
369
+ * @param {String} uri - File URI
244
370
  * @returns {Promise<Blob|undefined|false>} Resolves with:
245
371
  * - Blob of the stored file
246
372
  * - undefined if the download fails
@@ -250,7 +376,7 @@ export class FileStorage {
250
376
  * the file is re-downloaded from the server and stored again in IndexedDB.
251
377
  * If the server file is not available at that time to check its age but is stored, the stored file is returned.
252
378
  */
253
- get = async (uri) => {
379
+ async get(uri) {
254
380
  if (!uri) {
255
381
  return false;
256
382
  }
@@ -269,20 +395,21 @@ export class FileStorage {
269
395
  }
270
396
  }
271
397
  return storedFile;
272
- };
398
+ }
273
399
 
274
400
  /**
275
401
  * Store the file in IndexedDB.
276
- * @param {String} uri File URI
402
+ * @public
403
+ * @param {String} uri - File URI
277
404
  * @returns {Promise<boolean>} Resolves with:
278
405
  * - true if the file was stored successfully
279
406
  * - false if IndexedDB is not supported or storing failed
280
407
  */
281
- put = async (uri) => {
408
+ async put(uri) {
282
409
  if (!this.#supported) {
283
410
  return false;
284
411
  }
285
412
  const fileToStore = await this.#getServerFile(uri);
286
413
  return fileToStore ? !!(await this.#putFile(fileToStore, uri)) : false;
287
- };
414
+ }
288
415
  }