Skip to content

Commit c0c753a

Browse files
authored
Import and export individual addon's settings (ScratchAddons#8472)
* Import and export individual addon's settings * Remove useless Object.assign * Don't show in popup * Preload new icons * Replace save icon with download * Replace upload icon with existing import one * Tell the user which addon a file is for * Split button dropdown * Fix closing dropdown and element hierarchy * Format code * More generic dropdown classes * Listen for cancel event * Format code * Replace closeResetDropdowns with closeDropdowns * Rename event as well * Remove flex-start * Re-add flex-start in addon-setting * Extract settings from full extension file * Consistency with more settings --------- Co-authored-by: Samq64 <Samq64@users.noreply.github.com>
1 parent 468284d commit c0c753a

9 files changed

Lines changed: 164 additions & 69 deletions

File tree

_locales/en/messages.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -334,6 +334,9 @@
334334
"importFailed": {
335335
"message": "The settings file is invalid. Please try again."
336336
},
337+
"incorrectAddonImport": {
338+
"message": "The selected file is for the \"$1\" addon."
339+
},
337340
"confirmImport": {
338341
"message": "Confirm"
339342
},

webpages/settings/components/addon-body.html

Lines changed: 66 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,25 @@
1919
<div
2020
v-show="expanded && addon._enabled"
2121
v-if="addon.settings"
22-
class="addon-buttons"
23-
title="{{ msg('resetToDefault') }}"
24-
@click="loadDefaults"
22+
class="addon-split-button dropdown-parent"
23+
:class="{open: isDropdownOpen}"
2524
>
26-
<img src="../../images/icons/undo.svg" class="icon-type" draggable="false" />
25+
<button
26+
class="addon-buttons addon-split-button-button"
27+
title="{{ msg('resetToDefault') }}"
28+
@click="loadDefaults"
29+
>
30+
<img src="../../images/icons/undo.svg" class="icon-type" draggable="false" />
31+
</button>
32+
<div v-click-outside="closeDropdowns">
33+
<button class="addon-buttons addon-split-button-dropdown" @click="toggleDropdown">
34+
<img src="../../images/icons/expand.svg" class="icon-type" draggable="false" />
35+
</button>
36+
<ul class="dropdown-list">
37+
<li @click="exportPreset">{{ msg('export') }}</li>
38+
<li @click="importPreset">{{ msg('import') }}</li>
39+
</ul>
40+
</div>
2741
</div>
2842
<div class="switch" :state="addon._enabled ? 'on' : 'off'" @click="toggleAddonRequest"></div>
2943
</div>
@@ -253,6 +267,9 @@
253267
[disabled] {
254268
cursor: initial !important;
255269
}
270+
.addon-split-button {
271+
display: flex;
272+
}
256273
.addon-split-button-button {
257274
margin: 0;
258275
border-radius: 4px 0 0 4px;
@@ -273,6 +290,50 @@
273290
border-bottom-right-radius: 0;
274291
}
275292

293+
.dropdown-parent {
294+
position: relative;
295+
}
296+
.dropdown-parent.open .dropdown-list {
297+
display: block;
298+
}
299+
.dropdown-list {
300+
position: absolute;
301+
top: calc(100% - 1px);
302+
right: 0;
303+
margin: 0;
304+
padding: 6px 0;
305+
display: none;
306+
z-index: 3;
307+
border-radius: 4px;
308+
border-top-right-radius: 0;
309+
background: var(--button-background);
310+
color: var(--content-text);
311+
border: 1px solid var(--control-border);
312+
}
313+
.dropdown-list li {
314+
padding: 6px 12px;
315+
list-style: none;
316+
white-space: nowrap;
317+
text-align: start;
318+
transition: 0.2s ease;
319+
user-select: none;
320+
}
321+
.dropdown-list li:hover {
322+
background: var(--button-hover-background);
323+
}
324+
.dropdown-list.align-start {
325+
right: auto;
326+
left: 0;
327+
border-radius: 4px;
328+
border-top-left-radius: 0;
329+
}
330+
[dir="rtl"] .dropdown-list.align-start {
331+
left: auto;
332+
right: 0;
333+
border-top-left-radius: 4px;
334+
border-top-right-radius: 0;
335+
}
336+
276337
.btn-dropdown {
277338
display: flex;
278339
align-items: center;
@@ -406,6 +467,7 @@
406467
.addon-check {
407468
margin-inline-start: auto;
408469
padding: 5px;
470+
gap: 8px;
409471
display: flex;
410472
align-items: center;
411473
}

webpages/settings/components/addon-body.js

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import downloadBlob from "../../../libraries/common/cs/download-blob.js";
12
const isIframe = window.parent !== window;
23

34
export default async function ({ template }) {
@@ -11,6 +12,7 @@ export default async function ({ template }) {
1112
everExpanded: this.getDefaultExpanded(),
1213
hoveredSettingId: null,
1314
highlightedSettingId: null,
15+
isDropdownOpen: false,
1416
};
1517
},
1618
computed: {
@@ -54,6 +56,66 @@ export default async function ({ template }) {
5456
console.log(`Loaded preset ${preset.id} for ${this.addon._addonId}`);
5557
}
5658
},
59+
importPreset() {
60+
const inputElem = Object.assign(document.createElement("input"), {
61+
hidden: true,
62+
type: "file",
63+
accept: "application/json",
64+
});
65+
inputElem.addEventListener(
66+
"change",
67+
async (e) => {
68+
const text = await inputElem.files[0].text();
69+
inputElem.remove();
70+
let obj;
71+
try {
72+
obj = JSON.parse(text);
73+
if (!obj.addonId) {
74+
// Check if it's a full extension settings file
75+
const settings = obj?.addons?.[this.addon._addonId]?.settings;
76+
if (settings) {
77+
this.loadPreset({ id: "extracted-settings", values: settings });
78+
return;
79+
} else {
80+
throw "Missing addon ID";
81+
}
82+
}
83+
if (obj.addonId !== this.addon._addonId) {
84+
console.warn(`Incorrect addon ID: ${obj.addonId}`);
85+
alert(this.msg("incorrectAddonImport", this.$root.manifestsById[obj.addonId].name));
86+
return;
87+
}
88+
} catch (e) {
89+
console.warn(`Error importing settings file for ${this.addon._addonId}:`, e);
90+
alert(chrome.i18n.getMessage("importFailed"));
91+
return;
92+
}
93+
this.loadPreset(obj);
94+
},
95+
{ once: true }
96+
);
97+
inputElem.addEventListener(
98+
"cancel",
99+
() => {
100+
inputElem.remove();
101+
},
102+
{ once: true }
103+
);
104+
document.body.appendChild(inputElem);
105+
inputElem.click();
106+
this.toggleDropdown();
107+
},
108+
exportPreset() {
109+
const preset = {
110+
addonId: this.addon._addonId,
111+
id: "custom-preset",
112+
values: this.addonSettings,
113+
};
114+
const blob = new Blob([JSON.stringify(preset)], { type: "application/json" });
115+
const name = this.addon.name.replaceAll(" ", "-").toLowerCase();
116+
downloadBlob(`${name}.json`, blob);
117+
this.toggleDropdown();
118+
},
57119
loadDefaults() {
58120
if (window.confirm(chrome.i18n.getMessage("confirmReset"))) {
59121
for (const property of this.addon.settings) {
@@ -124,6 +186,13 @@ export default async function ({ template }) {
124186
this.$root.openRelatedAddons(this.addon);
125187
this.$root.blinkAddon(clickedAddon._addonId);
126188
},
189+
toggleDropdown() {
190+
this.isDropdownOpen = !this.isDropdownOpen;
191+
this.$root.closePickers({ isTrusted: true }, null, {
192+
callCloseDropdowns: false,
193+
});
194+
this.$root.closeDropdowns({ isTrusted: true }, this); // close other dropdowns
195+
},
127196
},
128197
watch: {
129198
groupId(newValue) {
@@ -138,7 +207,17 @@ export default async function ({ template }) {
138207
if (newValue === true) this.everExpanded = true;
139208
},
140209
},
210+
events: {
211+
closeDropdowns(...params) {
212+
return this.$root.closeDropdowns(...params);
213+
},
214+
},
141215
ready() {
216+
this.$root.$on("close-dropdowns", (except) => {
217+
if (this.isDropdownOpen && this !== except) {
218+
this.isDropdownOpen = false;
219+
}
220+
});
142221
const onHashChange = () => {
143222
if (location.hash.replace(/^#addon-/, "") === this.addon._addonId) {
144223
this.expanded = true;

webpages/settings/components/addon-setting.html

Lines changed: 4 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -38,19 +38,19 @@
3838
</div>
3939
</div>
4040
</div>
41-
<div class="addon-split-button setting-table-dropdown" :class="{open: rowDropdownOpen}">
41+
<div class="addon-split-button setting-table-dropdown dropdown-parent" :class="{open: rowDropdownOpen}">
4242
<button :disabled="!addon._enabled" class="addon-buttons addon-split-button-button" @click="addTableRow()">
4343
<img class="icon-type" src="../../images/icons/plus.svg" draggable="false" />
4444
</button>
45-
<div v-click-outside="closeResetDropdowns">
45+
<div v-click-outside="closeDropdowns">
4646
<button
4747
:disabled="!addon._enabled"
4848
class="addon-buttons addon-split-button-dropdown"
4949
@click="toggleRowDropdown"
5050
>
5151
<img class="icon-type" src="../../images/icons/expand.svg" draggable="false" />
5252
</button>
53-
<ul>
53+
<ul class="dropdown-list align-start">
5454
<li v-for="preset of setting.presets" @click="addTableRow(preset.values)">{{ preset.name }}</li>
5555
</ul>
5656
</div>
@@ -139,7 +139,7 @@
139139
:setting="setting"
140140
:enabled="addon._enabled"
141141
:presets="addon.presets"
142-
v-click-outside="closeResetDropdowns"
142+
v-click-outside="closeDropdowns"
143143
></reset-dropdown
144144
></template>
145145
<template v-if="!tableChild && !showResetDropdown"
@@ -267,10 +267,6 @@
267267
min-width: 70px;
268268
}
269269

270-
.setting-dropdown,
271-
.setting-table-dropdown {
272-
position: relative;
273-
}
274270
.setting-dropdown.open .clear-button {
275271
border-bottom-right-radius: 0;
276272
background: var(--button-hover-background);
@@ -284,21 +280,6 @@
284280
.iframe[dir="rtl"] .setting-dropdown.open .clear-button {
285281
border-bottom-left-radius: 4px;
286282
}
287-
.setting-dropdown ul,
288-
.setting-table-dropdown ul {
289-
position: absolute;
290-
top: calc(100% - 1px);
291-
right: 0;
292-
margin: 0;
293-
padding: 6px 0;
294-
display: none;
295-
z-index: 3;
296-
border-radius: 4px;
297-
border-top-right-radius: 0;
298-
background: var(--button-background);
299-
color: var(--content-text);
300-
border: 1px solid var(--control-border);
301-
}
302283
.iframe .setting-dropdown ul {
303284
right: auto;
304285
left: -100px;
@@ -315,23 +296,6 @@
315296
right: -100px;
316297
border-top-left-radius: 4px;
317298
}
318-
.setting-dropdown.open ul,
319-
.setting-table-dropdown.open ul {
320-
display: block;
321-
}
322-
.setting-dropdown li,
323-
.setting-table-dropdown li {
324-
padding: 6px 12px;
325-
list-style: none;
326-
white-space: nowrap;
327-
text-align: start;
328-
transition: 0.2s ease;
329-
user-select: none;
330-
}
331-
.setting-dropdown li:hover,
332-
.setting-table-dropdown li:hover {
333-
background: var(--button-hover-background);
334-
}
335299
.setting-dropdown li > * {
336300
vertical-align: middle;
337301
}
@@ -397,19 +361,6 @@
397361
}
398362
.setting-table-dropdown {
399363
align-self: flex-start;
400-
display: flex;
401-
}
402-
.setting-table-dropdown ul {
403-
right: auto;
404-
left: 0;
405-
border-radius: 4px;
406-
border-top-left-radius: 0;
407-
}
408-
[dir="rtl"] .setting-table-dropdown ul {
409-
left: auto;
410-
right: 0;
411-
border-top-left-radius: 4px;
412-
border-top-right-radius: 0;
413364
}
414365

415366
@media (max-width: 700px) {

webpages/settings/components/addon-setting.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ export default async function ({ template }) {
99
};
1010
},
1111
ready() {
12-
this.$root.$on("close-reset-dropdowns", (except) => {
12+
this.$root.$on("close-dropdowns", (except) => {
1313
if (this.rowDropdownOpen && this !== except) {
1414
this.rowDropdownOpen = false;
1515
}
@@ -135,7 +135,7 @@ export default async function ({ template }) {
135135
this.$root.closePickers({ isTrusted: true }, null, {
136136
callCloseDropdowns: false,
137137
});
138-
this.$root.closeResetDropdowns({ isTrusted: true }, this); // close other dropdowns
138+
this.$root.closeDropdowns({ isTrusted: true }, this); // close other dropdowns
139139
},
140140
msg(...params) {
141141
return this.$root.msg(...params);
@@ -153,8 +153,8 @@ export default async function ({ template }) {
153153
closePickers(...params) {
154154
return this.$root.closePickers(...params);
155155
},
156-
closeResetDropdowns(...params) {
157-
return this.$root.closeResetDropdowns(...params);
156+
closeDropdowns(...params) {
157+
return this.$root.closeDropdowns(...params);
158158
},
159159
},
160160
directives: {

webpages/settings/components/picker-component.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ export default async function ({ template }) {
5252
this.$root.closePickers({ isTrusted: true }, this, {
5353
callCloseDropdowns: false,
5454
});
55-
if (callCloseDropdowns) this.$root.closeResetDropdowns({ isTrusted: true }); // close other dropdowns
55+
if (callCloseDropdowns) this.$root.closeDropdowns({ isTrusted: true }); // close other dropdowns
5656
this.opening = false;
5757
this.$els.pickr._valueChanged();
5858
this.color = "#" + this.$els.pickr.hex8;

webpages/settings/components/reset-dropdown.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
<template>
2-
<div :class="{'setting-dropdown': true, open: isOpen}">
2+
<div class="setting-dropdown dropdown-parent" :class="{open: isOpen}">
33
<button type="button" class="large-button clear-button" :disabled="!enabled" @click="toggle" :title="msg('reset')">
44
<img src="../../images/icons/expand.svg" class="icon-type" draggable="false" />
55
</button>
6-
<ul>
6+
<ul class="dropdown-list">
77
<li @click="resetToDefault">
88
<span v-if="setting.type === 'color'" class="color-preview"
99
><span :style="{backgroundColor: setting.default}"></span

webpages/settings/components/reset-dropdown.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ export default async function ({ template }) {
99
};
1010
},
1111
ready() {
12-
this.$root.$on("close-reset-dropdowns", (except) => {
12+
this.$root.$on("close-dropdowns", (except) => {
1313
if (this.isOpen && this !== except) {
1414
this.isOpen = false;
1515
}
@@ -21,7 +21,7 @@ export default async function ({ template }) {
2121
this.$root.closePickers({ isTrusted: true }, null, {
2222
callCloseDropdowns: false,
2323
});
24-
this.$root.closeResetDropdowns({ isTrusted: true }, this); // close other dropdowns
24+
this.$root.closeDropdowns({ isTrusted: true }, this); // close other dropdowns
2525
},
2626
resetToDefault() {
2727
this.$parent.addonSettings[this.setting.id] = this.setting.default;

0 commit comments

Comments
 (0)