Project

General

Profile

« Previous | Next » 

Revision 659f532e

Added by Wojtek Kosior about 2 years ago

add import/export functionality

View differences:

TODOS.org
10 10
  those scripts are already free, as is often the case)
11 11
  - also, find some convenient way to automatically re-add "on" events ("onclick" & friends)
12 12
- add some good, sane error handling
13
- make it possible to export page settings in some format -- CRUCIAL
14 13
- get rid of those warnings and exceptions in console (many are not even related to this extension;
15 14
  who invented this thing?) (gecko-only)
16 15
- make page settings easily and conveniently editable in popup -- CRUCIAL
......
46 45
- besides blocking scripts through csp, also block connections that needlessly
47 46
  fetch those scripts
48 47
- make extension's all html files proper XHTML
48
- split options_main.js into several smaller files
49
- find out what causes storage sometimes not to get initialized under IceCat 60
50
- validate settings data on import
49 51

  
50 52
DONE:
53
- make it possible to export page settings in some format -- DONE 2021-06-19
51 54
- make it possible to use wildcard urls in settings -- DONE 2021-05-14
52 55
- port to gecko-based browsers -- DONE 2021-05-13
53 56
- find a way to additionally block all other scripts using CSP -- DONE 2021-05-13
html/options.html
89 89
	  background-color: white;
90 90
	  width: 50vw;
91 91
      }
92

  
93
      input[type="radio"]:not(:checked)+.import_window_content {
94
	  display: none;
95
      }
92 96
    </style>
93 97
  </head>
94 98
  <body>
95 99
    <!-- The invisible div below is for elements that will be cloned. -->
96
    <div style="display: none;">
100
    <div class="hide">
97 101
      <li id="item_li_template">
98 102
	<span></span>
99 103
	<button> Edit </button>
100 104
	<button> Remove </button>
105
	<button> Export </button>
101 106
      </li>
102 107
      <li id="bag_component_li_template">
103 108
	<span></span>
......
111 116
	<input type="radio" style="display: inline;" name="page_components"></input>
112 117
	<span></span>
113 118
      </li>
119
      <li id="import_li_template">
120
	<span></span>
121
	<input type="checkbox" style="display: inline;" checked></input>
122
	<span></span>
123
      </li>
114 124
    </div>
115 125

  
116 126
    <input type="radio" name="tabs" id="show_pages" checked></input>
......
186 196
      <button id="add_script_but" type="button"> Add script </button>
187 197
    </div>
188 198

  
199
    <button id="import_but" style="margin-top: 40px;"> Import </button>
200

  
189 201
    <div id="chbx_components_window" class="hide popup" position="absolute">
190 202
      <div class="popup_frame">
191 203
	<ul id="chbx_components_ul">
......
210 222
      </div>
211 223
    </div>
212 224

  
225
    <div id="import_window" class="hide popup" position="absolute">
226
      <div class="popup_frame">
227
	<h2> Settings import </h2>
228
	<input id="import_loading_radio" type="radio" name="import_window_content"></input>
229
	<span class="import_window_content"> Loading... </span>
230
	<input id="import_failed_radio" type="radio" name="import_window_content"></input>
231
	<div class="import_window_content">
232
	  <span> Bad file :( </span>
233
	  <pre id="bad_file_errormsg"></pre>
234
	  <button id="import_failok_but"> OK </button>
235
	</div>
236
	<input id="import_selection_radio" type="radio" name="import_window_content"></input>
237
	<div class="import_window_content">
238
	  <button id="check_all_import_but"> Check all </button>
239
	  <button id="uncheck_all_import_but"> Uncheck all </button>
240
	  <button id="uncheck_colliding_import_but"> Uncheck existing </button>
241
	  <ul id="import_ul">
242
	  </ul>
243
	  <button id="commit_import_but"> OK </button>
244
	  <button id="cancel_import_but"> Cancel </button>
245
	</div>
246
      </div>
247
    </div>
248

  
249
    <a id="file_downloader" class="hide"></a>
250
    <form id="file_opener_form" style="visibility: hidden;">
251
      <input type="file" id="file_opener"></input>
252
    </form>
253

  
213 254
    <script src="/common/connection_types.js"></script>
214 255
    <script src="/common/stored_types.js"></script>
215 256
    <script src="/common/once.js"></script>
html/options_main.js
37 37
	return document.getElementById(id);
38 38
    }
39 39

  
40
    function nice_name(prefix, name)
41
    {
42
	return `${name} (${TYPE_NAME[prefix]})`;
43
    }
44

  
40 45
    const item_li_template = by_id("item_li_template");
41 46
    const bag_component_li_template = by_id("bag_component_li_template");
42 47
    const chbx_component_li_template = by_id("chbx_component_li_template");
43 48
    const radio_component_li_template = by_id("radio_component_li_template");
49
    const import_li_template = by_id("import_li_template");
44 50
    /* Make sure they are later cloned without id. */
45 51
    item_li_template.removeAttribute("id");
46 52
    bag_component_li_template.removeAttribute("id");
47 53
    chbx_component_li_template.removeAttribute("id");
48 54
    radio_component_li_template.removeAttribute("id");
55
    import_li_template.removeAttribute("id");
49 56

  
50 57
    function item_li_id(prefix, item)
51 58
    {
......
69 76
	remove_button.addEventListener("click",
70 77
				       () => storage.remove(prefix, item));
71 78

  
79
	let export_button = remove_button.nextElementSibling;
80
	export_button.addEventListener("click",
81
				       () => export_item(prefix, item));
82

  
72 83
	if (!at_the_end) {
73 84
	    for (let element of ul.ul.children) {
74 85
		if (element.id < li.id || element.id.startsWith("work_"))
......
110 121
	let chbx = li.firstElementChild;
111 122
	let span = chbx.nextElementSibling;
112 123

  
113
	span.textContent = `${name} (${TYPE_NAME[prefix]})`;
124
	span.textContent = nice_name(prefix, name);
114 125

  
115 126
	chbx_components_ul.appendChild(li);
116 127
    }
......
130 141
	let radio = li.firstElementChild;
131 142
	let span = radio.nextElementSibling;
132 143

  
133
	span.textContent = `${name} (${TYPE_NAME[prefix]})`;
144
	span.textContent = nice_name(prefix, name);
134 145

  
135 146
	radio_components_ul.insertBefore(li, radio_component_none_li);
136 147
    }
......
147 158
	    let [prefix, name] = components;
148 159
	    page_payload_span.setAttribute("data-prefix", prefix);
149 160
	    page_payload_span.setAttribute("data-name", name);
150
	    page_payload_span.textContent = `${name} (${TYPE_NAME[prefix]})`;
161
	    page_payload_span.textContent = nice_name(prefix, name);
151 162
	}
152 163
    }
153 164

  
......
197 208
	    li.setAttribute("data-prefix", prefix);
198 209
	    li.setAttribute("data-name", name);
199 210
	    let span = li.firstElementChild;
200
	    span.textContent = `${name} (${TYPE_NAME[prefix]})`;
211
	    span.textContent = nice_name(prefix, name);
201 212
	    let remove_but = span.nextElementSibling;
202 213
	    remove_but.addEventListener("click", () =>
203 214
					bag_components_ul.removeChild(li));
......
319 330
	ul.edited_item = item;
320 331
    }
321 332

  
333
    const file_downloader = by_id("file_downloader");
334

  
335
    function recursively_export_item(prefix, name, added_items, items_data)
336
    {
337
	let key = prefix + name;
338

  
339
	if (added_items.has(key))
340
	    return;
341

  
342
	let data = storage.get(prefix, name);
343
	if (data === undefined) {
344
	    console.log(`${TYPE_NAME[prefix]} '${name}' for export not found`);
345
	    return;
346
	}
347

  
348
	if (prefix !== TYPE_PREFIX.SCRIPT) {
349
	    let components = prefix === TYPE_PREFIX.BAG ?
350
		data : [data.components];
351

  
352
	    for (let [comp_prefix, comp_name] of components) {
353
		recursively_export_item(comp_prefix, comp_name,
354
					added_items, items_data);
355
	    }
356
	}
357

  
358
	items_data.push({[key]: data});
359
	added_items.add(key);
360
    }
361

  
362
    function export_item(prefix, name)
363
    {
364
	let added_items = new Set();
365
	let items_data = [];
366
	recursively_export_item(prefix, name, added_items, items_data);
367
	let file = new Blob([JSON.stringify(items_data)],
368
			    {type: "application/json"});
369
	let url = URL.createObjectURL(file);
370
	file_downloader.setAttribute("href", url);
371
	file_downloader.setAttribute("download", prefix + name + ".json");
372
	file_downloader.click();
373
	file_downloader.removeAttribute("href");
374
	URL.revokeObjectURL(url);
375
    }
376

  
322 377
    function add_new_item(prefix)
323 378
    {
324 379
	cancel_work(prefix);
......
452 507
	}
453 508
    }
454 509

  
510
    const import_window = by_id("import_window");
511
    const import_loading_radio = by_id("import_loading_radio");
512
    const import_failed_radio = by_id("import_failed_radio");
513
    const import_selection_radio = by_id("import_selection_radio");
514
    const bad_file_errormsg = by_id("bad_file_errormsg");
515

  
516
    /*
517
     * Newer browsers could utilise `text' method of File objects.
518
     * Older ones require FileReader.
519
     */
520

  
521
    function _read_file(file, resolve, reject)
522
    {
523
	let reader = new FileReader();
524

  
525
	reader.onload = () => resolve(reader.result);
526
	reader.onerror = () => reject(reader.error);
527
	reader.readAsText(file);
528
    }
529

  
530
    function read_file(file)
531
    {
532
	return new Promise((resolve, reject) =>
533
			   _read_file(file, resolve, reject));
534
    }
535

  
536
    async function import_from_file(event)
537
    {
538
	let files = event.target.files;
539
	if (files.length < 1)
540
	    return;
541

  
542
	import_window.classList.remove("hide");
543
	import_loading_radio.checked = true;
544

  
545
	let result = undefined;
546

  
547
	try {
548
	    result = JSON.parse(await read_file(files[0]));
549
	} catch(e) {
550
	    bad_file_errormsg.textContent = "" + e;
551
	    import_failed_radio.checked = true;
552
	    return;
553
	}
554

  
555
	let errormsg = validate_settings(result);
556
	if (errormsg !== false) {
557
	    bad_file_errormsg.textContent = errormsg;
558
	    import_failed_radio.checked = true;
559
	    return;
560
	}
561

  
562
	populate_import_list(result);
563
	import_selection_radio.checked = true;
564
    }
565

  
566
    function validate_settings(settings)
567
    {
568
	// TODO
569
	return false;
570
    }
571

  
572
    function import_li_id(prefix, item)
573
    {
574
	return `ili_${prefix}_${item}`;
575
    }
576

  
577
    let import_ul = by_id("import_ul");
578
    let import_chbxs_colliding = undefined;
579
    let settings_import_map = undefined;
580

  
581
    function populate_import_list(settings)
582
    {
583
	let old_children = import_ul.children;
584
	while (old_children[0] !== undefined)
585
	    import_ul.removeChild(old_children[0]);
586

  
587
	import_chbxs_colliding = [];
588
	settings_import_map = new Map();
589

  
590
	for (let setting of settings) {
591
	    let [key, value] = Object.entries(setting)[0];
592
	    let prefix = key[0];
593
	    let name = key.substring(1);
594
	    add_import_li(prefix, name);
595
	    settings_import_map.set(key, value);
596
	}
597
    }
598

  
599
    function add_import_li(prefix, name)
600
    {
601
	let li = import_li_template.cloneNode(true);
602
	let name_span = li.firstElementChild;
603
	let chbx = name_span.nextElementSibling;
604
	let warning_span = chbx.nextElementSibling;
605

  
606
	li.setAttribute("data-prefix", prefix);
607
	li.setAttribute("data-name", name);
608
	li.id = import_li_id(prefix, name);
609
	name_span.textContent = nice_name(prefix, name);
610

  
611
	if (storage.get(prefix, name) !== undefined) {
612
	    import_chbxs_colliding.push(chbx);
613
	    warning_span.textContent = "(will overwrite existing setting!)";
614
	}
615

  
616
	import_ul.appendChild(li);
617
    }
618

  
619
    function check_all_imports()
620
    {
621
	for (let li of import_ul.children)
622
	    li.firstElementChild.nextElementSibling.checked = true;
623
    }
624

  
625
    function uncheck_all_imports()
626
    {
627
	for (let li of import_ul.children)
628
	    li.firstElementChild.nextElementSibling.checked = false;
629
    }
630

  
631
    function uncheck_colliding_imports()
632
    {
633
	for (let chbx of import_chbxs_colliding)
634
	    chbx.checked = false;
635
    }
636

  
637
    const file_opener_form = by_id("file_opener_form");
638

  
639
    function hide_import_window()
640
    {
641
	import_window.classList.add("hide");
642
	/* Let GC free some memory */
643
	import_chbxs_colliding = undefined;
644
	settings_import_map = undefined;
645

  
646
	/*
647
	 * Reset file <input>. Without this, a second attempt to import the same
648
	 * file would result in "change" event on happening on <input> element.
649
	 */
650
	file_opener_form.reset();
651
    }
652

  
653
    function commit_import()
654
    {
655
	for (let li of import_ul.children) {
656
	    let chbx = li.firstElementChild.nextElementSibling;
657

  
658
	    if (!chbx.checked)
659
		continue;
660

  
661
	    let prefix = li.getAttribute("data-prefix");
662
	    let name = li.getAttribute("data-name");
663
	    let key = prefix + name;
664
	    let value = settings_import_map.get(key);
665
	    storage.set(prefix, name, value);
666
	}
667

  
668
	hide_import_window();
669
    }
670

  
671
    function initialize_import_facility()
672
    {
673
	let import_but = by_id("import_but");
674
	let file_opener = by_id("file_opener");
675
	let import_failok_but = by_id("import_failok_but");
676
	let check_all_import_but = by_id("check_all_import_but");
677
	let uncheck_all_import_but = by_id("uncheck_all_import_but");
678
	let uncheck_existing_import_but = by_id("uncheck_existing_import_but");
679
	let commit_import_but = by_id("commit_import_but");
680
	let cancel_import_but = by_id("cancel_import_but");
681
	import_but.addEventListener("click", () => file_opener.click());
682
	file_opener.addEventListener("change", import_from_file);
683
	import_failok_but.addEventListener("click", hide_import_window);
684
	check_all_import_but.addEventListener("click", check_all_imports);
685
	uncheck_all_import_but.addEventListener("click", uncheck_all_imports);
686
	uncheck_colliding_import_but
687
	    .addEventListener("click", uncheck_colliding_imports);
688
	commit_import_but.addEventListener("click", commit_import);
689
	cancel_import_but.addEventListener("click", hide_import_window);
690
    }
691

  
455 692
    async function main()
456 693
    {
457 694
	storage = await get_storage();
......
489 726
	    cancel_components_but.addEventListener("click", cancel_components);
490 727
	}
491 728

  
729
	initialize_import_facility();
730

  
492 731
	storage.add_change_listener(handle_change);
493 732
    }
494 733

  

Also available in: Unified diff